在实际开发中,经常会用到redis来实现redis锁,来应对共享资源的并发访问。经常用的就是setnx+expire,释放锁用lua脚本来实现
//获取锁,value唯一,这样便于释放锁
set key value ex expireTime NX
//释放锁,比较value来确认谁持有锁,谁就来释放锁
if redis.call("get", keys[1]) == argv[1] then
return redis.call("del", keys[1])
else
return 0
end
这种实现方式的关键点:
- setnx和expire要在一个事务中执行,否则setnx成功,链接断开了expire没执行,就会出现死锁
- value要唯一,这样在释放锁的时候就可以验证,谁持有锁谁就来释放锁
- 释放锁用lua脚本,是为了保证命令在一个事务中执行
具体java代码实现:
public class RedisService {
@Resource(name="redisTemplate")
protected ValueOperations<String, String> valueOperations;
public void setValue(String key, String value){
valueOperations.set(key, value, TimeUnit.MILLISECONDS);
}
public String getValue(String key){
return valueOperations.get(key);
}
public Boolean lock(String key, String value, long lockTime) {
return valueOperations.setIfAbsent(key, value, lockTime, TimeUnit.MILLISECONDS);
}
public Long releaseLock(String key, String value) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> stringRedisScript = new DefaultRedisScript<>();
((DefaultRedisScript<Long>) stringRedisScript).setResultType(Long.class);
((DefaultRedisScript<Long>) stringRedisScript).setScriptText(luaScript);
Long result = valueOperations.getOperations().execute(stringRedisScript, Collections.singletonList(key), value);
return result;
}
}
这样实现的优点就是很简单高效,在单机、主从、哨兵和集群环境一般情况都可以,但是在master发生主从切换的时候,就会出现锁丢失的情况,情况如下:
- 客户端在master节点上拿到了锁
- 但是这个时候master宕机了,并且发生了故障转移,slave自动升级为master节点
- 这个节点没有及时从master同步到slave,这个时候就发生了锁丢失,主从复制异步
基于上面的问题,又出现了Redlock,它的实现思路如下:
- 获取当前时间
- 依次尝试从奇数个实例(一般不小于3个),用相同的key和唯一的value获取锁。向redis请求获取锁时,客户端设置一个请求超时时间,这个超时时间远小于锁的有效时间。这样可以避免在redis宕机,客户端还在等待获取锁。如果没有在规定时间内从redis服务器获取锁,客户端立即尝试去另外一个redis服务器获取锁。
- 当且仅当从大多数的redis节点都获取到锁,并且锁的使用时间(当前时间减去步骤1中的时间)小于锁的有效时间,这样锁才算获取成功,锁的真正有效时间是有效时间减去获取锁使用时间。
- 锁没有获取成功,就释放部分获取到redis实例锁的客户端释放锁。防止这轮没获取锁成功,导致下一轮也没有获取到锁。
Redlock具体实现:
public class RedisRedlock {
public void lock() {
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://127.0.0.1:6379")
.setPassword("123456").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://127.0.0.1:6380")
.setPassword("123456").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://127.0.0.1:6371")
.setPassword("123456").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);
String resourceName = "RLOCK";
RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
// 50ms拿不到锁,就认为获取锁失败,10s是锁失效时间。
isLock = redLock.tryLock(50, 10000, TimeUnit.MILLISECONDS);
System.out.println("isLock = "+isLock);
if (isLock) {
//business work
}
} catch (Exception e) {
} finally {
//释放锁
redLock.unlock();
}
}
}
Redlock的实现,他对实例环境有点苛刻,它需要完全相互独立,不存在主从复制并且也不是存在集群协调机制。奇数个redis实例,奇数个sentinel集群,奇数个cluster集群。Redlock的实现仍然存在锁的有效时间失效的问题,也仍然存在主从复制不及时,故障主从转移的问题,只不过是它把问题出现的概率最小化了,除非是同时出现大多数节点同时宕机,同时出现主从不同步的问题,这样就会出现获取锁冲突问题。
所以Redlock相较于前面的实现,它是把获取锁冲突的问题出现的概率降低了,但是仍然没有避免出现这个问题,同时Redlock带来的问题是,部署环境要求就要复杂点。所以在实际的选用方案就是在两者之间的权衡取舍。
网友评论