redis分布式锁
介绍
-
- setnx(lockkey,currenttime+timeout), setex , getset
-
- request_id , lua
-
- Redisson lock
-
- 集群下的分布式锁
常用方式
缺点
- 第1种方式多台机器可能出现 value(时间)相同,没有锁所属者
- 可以用UUID代替当前时间,作为客户端请求标识
- 可能死锁
- 没有设置expire 有效期
- tomcat挂掉,没有及时清除锁 就会发生死锁
/**
* 锁定
* @param key
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key ,String value){
if(redisTemplate.opsForValue().setIfAbsent(key, value)){
return true;
}
String currentValue = redisTemplate.opsForValue().get(key);
//如果锁过期
if(!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()){
//获取上一个锁的时间 (为了防止调用者异常导致锁没有释放,所以获取旧的值进行比较)
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
//分布式调用的时候oldValue和currentValue不一定相等,因为执行过程中其它请求可能进来
if(oldValue == null
|| (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue))){
return true;
}
return false;
}
/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key ,String value){
try{
String currentValue = redisTemplate.opsForValue().get(key);
if(!StringUtils.isEmpty(currentValue) && value.equals(currentValue)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
}
}
// 测试
public static void main(String[] args) {
RedisLock redisLock = new RedisLock();
long current_timestamp = System.currentTimeMillis();
long time_out = 10*1000;
boolean lockResult =
redisLock.lock("product_id", String.valueOf(current_timestamp + time_out));
if(!lockResult){
throw new SellException(900,"加锁失败");
}
redisLock.setExpire("product_id",1000*time);
//dosomething
redisLock.unlock("product_id",String.valueOf(current_timestamp + time_out));
}
-
还可以在获取到锁后 执行业务开始前 设置锁的过期时间,还可以在执行业务方法完成时释放锁
-
以上代码的问题所在
-
设想下面一个场景: 1. C1成功获取到了锁,之后C1因为GC进入等待或者未知原因导致任务执行过长,最后在锁失效前C1没有主动释放锁 2. C2在C1的锁超时后获取到锁,并且开始执行,这个时候C1和C2都同时在执行,会因重复执行造成数据不一致等未知情况 3. C1如果先执行完毕,则会释放C2的锁,此时可能导致另外一个C3进程获取到了锁
-
由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
- 不能保证分布式环境下的物理时钟一致性,也可能存在UnixTimestamp重复问题,只不过极少情况下会遇到
-
当锁过期的时候,假如多个客户端同时执行
getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。 -
锁不具备拥有者标识,即任何客户端都可以解锁。
public static void wrongReleaseLock(Jedis jedis, String lockKey, String requestId) { // 判断加锁与解锁是不是同一个客户端 if (requestId.equals(jedis.get(lockKey))) { // 若在此时,这把锁突然不是这个客户端的,则会误解锁 // (因为判断和删除是分开的不是原子操作,在判断过程中,这把锁可能被其它客户端获取了) jedis.del(lockKey); } } -
正确姿势-单机
来源于网络
public class RedisLock {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[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;
}
- 规避上面时间戳解决方案提到的时钟问题。
- 但是如果在Redis集群环境下依然存在问题,由于Redis集群数据同步为异步,假设在Master节点获取到锁后未完成数据同步情况下Master节点crash,此时在新的Master节点依然可以获取锁,所以多个Client同时获取到了锁
Redisson
-
Based on high-performance async and lock-free Java Redis client and Netty framework.
-
可以优雅实现分布式锁
-
Redisson Demo
我们可以通过查看redis存储发现,Redisson实现锁的类型是一个hash,key为UUID,value为1,主要通过ttl来控制
<!-- JDK 1.8+ compatible --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.8.1</version> </dependency>/* redissonManager.java */ // 1. Create config object Config = ... // 2. Create Redisson instance RedissonClient redisson = Redisson.create(config); // -------------------------------------------------- String lockKey = "REDIS_LOCK.SCHEDULED_TASK_LOCK"; RLock lock = redissonManager.getRedisson().getLock(lockKey); // 是否获取到锁 boolean getLock = false; try { // waitTime(等待时间),leaseTime(释放时间),时间单位 if(getLock = lock.tryLock(0,5, TimeUnit.SECONDS)){ log.info( "Redisson获取到分布式锁:{},ThreadName:{}",lockKey,Thread.currentThread().getName()); // 处理业务方法 }else{ log.info("Redisson没有获取到分布式锁:{},ThreadName:{}",lockKey,Thread.currentThread().getName()); } } catch (InterruptedException e) { log.error("Redisson分布式锁获取异常",e); } finally { if(!getLock){ return; } lock.unlock(); log.info("Redisson分布式锁释放锁"); }- waitTime :如果执行任务非常快 时间少于waitTime就可能出现多个客户端现时获取锁
集群下的分布式锁
会出现问题
- 单点的方式很好理解,但在redis集群上会出现问题,如:
- 客户端A在主节点获得了一个锁。
- 主节点挂了,而到从节点的写同步还没完成。
- 从节点被提升为主节点。
- 客户端B获得和A相同的锁
意思就是:如果A往Master放入了一把锁,然后再数据同步到Slave之前,Master crash,Slave被提拔为Master,这时候Master上面就没有锁了,这样其他进程也可以拿到锁,违法了锁的互斥性。
redlock算法
算法描述
Redlock算法并不复杂,假设有三个Master的节点,这三个Master,又各自有一个Slave 。现在有一个客户端想获取一把分布式锁流程算法流程是这样:
- 记下开始获取锁的时间 startTime
- 按照A->B->C的顺序,依次向这三台Master发送获取锁的命令。客户端在等待每台Master回响应时,都有超时时间timeout。举个例子,客户端向A发送获取锁的命令,在等了timeout时间之后,都没收到响应,就会认为获取锁失败,继续尝试获取下一把锁
- 如果获取到超过半数的锁,也就是 3/2+1 = 2把锁,这时候还没完,要记下当前时间endTime
- 计算拿到这些锁花费的时间 costTime = endTime - startTime,如果costTime小于锁的过期时间expireTime,则认为获取锁成功
- 如果获取不到超过一半的锁,或者拿到超过一半的锁时,计算出costTime>=expireTime,这两种情况下,都视为获取锁失败
- 如果获取锁失败,需要向全部Master节点,都发生释放锁的命令
马丁博士对Redlock的质疑
马丁·克莱普曼指出Redlock是个强依赖系统时间的算法,这样就可能带来很多不一致问题,他给出了个例子一起看下:
假设多节点Redis系统有五个节点A/B/C/D/E和两个客户端C1和C2,如果其中一个Redis节点上的时钟向前跳跃会发生什么?
- 客户端C1获得了对节点A、B、c的锁定,由于网络问题,法到达节点D和节点E
- 节点C上的时钟向前跳,导致锁提前过期
- 客户端C2在节点C、D、E上获得锁定,由于网络问题,无法到达A和B
- 客户端C1和客户端C2现在都认为他们自己持有锁
分布式异步模型:
上面这种情况之所以有可能发生,本质上是因为Redlock的安全性对Redis节点系统时钟有强依赖,一旦系统时钟变得不准确,算法的安全性也就无法保证。
作者:后端技术指南针
链接:https://juejin.im/post/5e12c44fe51d45413b7b7bd2
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
zookeeper分布式锁
- 并发支持 < redis , 选主时不可用
- 临时有序节点可以实现公平锁
redis,zk 分布式锁的对比
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
- zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
-
另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。
-
Redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等......zk 的分布式锁语义清晰实现简单。
-
我个人实践认为 zk 的分布式锁比 Redis 的分布式锁牢靠、而且模型简单易用。

浙公网安备 33010602011771号