分布式锁需要解决的问题
1.互斥性:任一时刻是有一个客户端获取锁,不能两个客户端获取到锁
2.安全性:锁只能被持有该客户端的删除,不能由其他客户端删除
3.死锁:一个客户端获取到锁,导致宕机,而其他客户端无法获取到资源
4.容错:一些节点宕机,客户端任然能获取锁和释放锁
分布式锁思路
基于Redis实现的分布式锁,Redis单机部署的场景
(存在问题是如果处理时间长,锁自动失效可能会出现问题)
加锁
public static boolean rightGetLock(Jedis jedis, String lockKey, String requestId,
Integer expireTime) {
//传requestId的原因:这样可以知道这把锁是哪个请求加的,在解锁的时候就有依据,只能解锁自己加的锁,
requestId可以使用UUID.randomUUID().toString()方法生成
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST,
SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
String set(String key, String value, String nxxx, String expx, long time)该方法是: 存储数据到缓存中,
并制定过期时间和当Key存在时是否覆盖。
nxxx: 只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
expx: 只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
time: 过期时间,单位是expx所代表的单位。
第一个为key
第二个为value,这里传的是requestId;它的意义在于可以区分这把锁是哪个请求加的
第三个为nxxx,这里传的是NX
第四个为expx,这里传的是PX,意思是给key加一个过期设置,具体时间由第5个参数决定
第五个为time,与第四个参数相呼应,代表key的过期时间
上面这种实现方式满足了可靠性里描述的三个条件:
首先,set加入NX参数可以保证如果key已存在则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性
其次,对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间自动解锁,不会发
生死锁
将value赋值为requestId,代表加锁的客户端请求标识,在客户端解锁的时候就可以校验是否是同一个客户端
解锁
public static boolean rightReleaseLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEY[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
解锁只需要两行代码就搞定了,该方式可以确保上述操作是原子性的
一个简单的Lua脚本代码,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁
第二行是将Lua代码传到jedis.eval的方法里,并使参数KEY[1]赋值为lockKey,ARGV[1]
赋值为requestId,然后交给Redis服务端执行
执行eval方法可以确保原子性,源于Redis的特性,官网对eval命令的部分解释如下:
eval命令执行Lua代码将被当成一个命令去执行,直到eval命令执行完成Redis才会去执行其他命令
多机部署可以尝试使用Redisson实现
//工具类可以先忽略
@Configuration
@EnableConfigurationProperties(RedisModel.class)
public class RedissonConfig {
private final RedisModel redisModel;
public RedissonConfig(RedisModel redisModel) {
this.redisModel = redisModel;
}
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
String [] nodes = redisModel.getSentinel().getNodes().split(",");
List newNodes = new ArrayList<>(nodes.length);
newNodes.addAll(Arrays.asList(nodes));
SentinelServersConfig serverConfig = config.useSentinelServers()
.addSentinelAddress(newNodes.toArray(new String[0]))
.setMasterName(redisModel.getSentinel().getMaster())
.setReadMode(ReadMode.SLAVE)
.setTimeout(redisModel.getTimeout());
// 设置密码
if(StringUtils.isNotBlank(redisModel.getPassword())){
serverConfig.setPassword(redisModel.getPassword());
}
// 设置database
if (redisModel.getDatabase()!=0){
serverConfig.setDatabase(redisModel.getDatabase());
}
return Redisson.create(config);
}
}
具体参考如下:
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.0.1:5378")
.setPassword("a123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.0.1:5379")
.setPassword("a123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.0.1:5380")
.setPassword("a123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
String resourceName = "REDLOCK_KEY";
RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
// 向3个redis实例尝试加锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
// isLock = redLock.tryLock();
// 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
System.out.println("isLock = "+isLock);
if (isLock) {
//TODO if get lock success, do something;
}
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
redLock.unlock();
}```