redis分布式锁

介绍

    1. setnx(lockkey,currenttime+timeout), setex , getset
    1. request_id , lua
    1. Redisson lock
    1. 集群下的分布式锁

常用方式

graph TB   setnx{setnx lockkey,<br/>currenttime+timeout}-->|1 True 获取锁成功<br/>设置过期时间|expire(expire lockkey);   expire-->|2 执行业务|del(del lockkey);   del-->|3 释放锁|结束True;   setnx-->|-1 False 获取锁失败|get(get lockkey);   get-->|-2 判断是否过期|lockvalueA{lockvalueA!=null &&<br/>currenttime>lockvalueA};   lockvalueA-->|-3 True 过期 |getset(getset lockkey,currenttime+timeout);   lockvalueA-->|-4 False 没有过期 |结束False;   getset-->|-5 判断getset结果 |lockvalueB{lockvalueB==null 或者 <br/>lockvalueA==lockvalueB};   lockvalueB-->|-6 True 已经没有这个key<br/>或者锁没有变化 |获取到锁-进入第1步;   lockvalueB-->|-7 False |结束False;
缺点
  • 第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

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集群上会出现问题,如:
  1. 客户端A在主节点获得了一个锁。
  2. 主节点挂了,而到从节点的写同步还没完成。
  3. 从节点被提升为主节点。
  4. 客户端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 的分布式锁牢靠、而且模型简单易用。

相关文章

brige4you

github-Docs-distributedlock

posted @ 2021-06-27 17:30  沉梦匠心  阅读(81)  评论(0)    收藏  举报