• Redis分布式锁一文全攻略


    分布式锁概念

    分布式锁其实就是,控制分布式系统的不同进程共同访问共享资源的一种锁的实现。如果不同系统或同一个系统的不同主机去访问一个共享的临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
    分布式锁应该具备以下条件:

    1. 互斥性:任意时刻,只允许一个客户端访问。
    2. 锁超时:持有锁超时,可以释放,避免资源浪费,也可以防止死锁。
    3. 可重入性:一个线程获取锁之后,还可以再次请求加锁。
    4. 高可用和高性能:加锁和释放锁的开销要尽可能的低,同时保证高可用,防止分布式锁失效。
    5. 安全性:锁只能被持有的客户端删除,不能被其他客户端删除。

    分布式锁实现方式

    1,基于数据库锁
    2,基于Redis锁
    3,基于Zookeeper

    Redis分布式锁实现方案

    本文以基于Redis来实现分布式锁,描述多个实现方案,并分析利弊。

    方案一:SETNX + EXPIRE

    Redis中有一个 SETNX 命令,命令格式是 SETNX key value,如果key不存在,则SETNX返回1,如果已存在,则返回0。
    那么可以使用 SETNX + EXPIRE命令,即SETNX抢到锁,在用EXPIRE给锁一个过期时间,防止锁忘记释放或者服务奔溃而没有释放锁。
    伪代码如下:

    if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
        expire(key_resource_id,100); //设置过期时间
        try {
            do something  //业务请求
        }catch(){
      }
      finally {
           jedis.del(key_resource_id); //释放锁
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个方案的问题是,setnx和expire两个命令操作不是原子性的,那么有可能在setnx加完锁之后,进程发生异常等原因,设置过期时间还没有执行,那么就会导致这个锁一直得不到释放,其他请求就会一直在等待,造成死锁问题。

    方案二:使用lua脚本(包含SETNX + EXPIRE两条指令)

    针对方案一的问题,那么可以使用lua脚本来保证原子性(包含SETNXX + EXPIRE)。
    lua脚本如下:

    if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
       redis.call('expire',KEYS[1],ARGV[2])
    else
       return 0
    end;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    加锁代码如下:

     String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
                " redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";   
    Object result = jedis.eval(lua_scripts, Collections.singletonList(key_resource_id), Collections.singletonList(values));
    //判断是否成功
    return result.equals(1L);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    方案三:SET的扩展命令(SET NX EX PX)

    我们可以使用lua脚本来保证SETNX + EXPIRE两条命令的原子性,也可以使用Redis的SET指令扩展参数来实现。

    语法:SET key value NX|XX EX|PX  expire_time
    参数:
    NX:只有健key不存在的时候,才会去设置健key的值
    PX:只有健key存在的时候,才会去设置健key的值
    EX:设置健key的过期时间,单位为秒
    PX:设置健key的过期时间,单位为毫秒
    expire_time:过期时间,整数类型
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    伪代码demo如下:

    if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加锁
        try {
            do something  //业务处理
        }catch(){
      }
      finally {
           jedis.del(key_resource_id); //释放锁
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    方案二和方案三的还可能存在问题:
    问题1,锁过期释放了,业务还没有执行完。比如a线程拿到锁并执行业务代码,但是过期时间到了,业务代码还没有执行完成,那么b线程进来,就能拿到锁,此时就不是同步串行执行的。
    问题2,锁被别的线程误删。比如a线程拿到锁,然后准备去释放锁,但有可能过期时间到了,b线程进来拿到锁,此时a线程去释放锁,就会把b线程的锁给释放带,但b线程的业务代码还没有执行完成,后续又有新线程来拿锁,导致问题的发生。

    方案四:SET NX EX|PX + 校验唯一随机值,再删除

    既然锁可能被别的线程误删除,那么给value值设置一个标记当前线程的唯一随机值,在删除的时候校验一下。

    加锁和释放锁的参考代码如下:

    /**
     * 尝试获取分布式锁
     * @param lockKey 锁
     * @param requestId 请求标识,唯一ID, 可以使用UUID.randomUUID().toString();
     * @param expireTime 超期时间,毫秒
     * @return 是否获取成功
     */
    public  boolean redisLockByKey(String lockKey, String requestId, int expireTime) {
        Jedis jedis = jedisPool.getResource();
        try {
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        } finally {
            jedis.close();
        }
    }
    
    /**
     * 释放分布式锁
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public  boolean redisUnlockByKey(String lockKey, String requestId) {
        Jedis jedis = jedisPool.getResource();
        try {
            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;
        } finally {
            jedis.close();
        }
    
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    方案四解决了误删除的问题,但还是没有解决【锁过期释放,业务代码还没有执行完】的问题。

    方案五:Redisson框架

    其实以上的方案,还只是基于单机版的Redis来讨论的,但生产环境大多都是集群模式部署的。

    如果线程a在Redis的master节点上拿到锁,但是加锁的key还没有同步到slave节点。恰好此时,master节点发生故障,一个slave节点会升级为master,这时候线程b就能拿到同个key的锁,而线程a也同样拿到锁,这样就不能保证安全性了。

    针对这个问题,Redis作者antirez提出一个高级的分布式锁算法,Redlock。
    Redlock核心思想是这样的:

    搞多个Redis master部署,以保证它们不会同时宕掉。并且这些master节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个master实例上,是与在Redis单实例,使用相同方法来获取和释放锁。

    Redlock的实现步骤如下:

    1.获取当前时间,以毫秒为单位。
    
    2.按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒,则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时,跳过该master节点,尽快去尝试下一个master节点。
    
    3.客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间),得到获取锁使用的时间。当且仅当超过一半(N/2+1,这里是5/2+1=3个节点)的Redis master节点都获得锁,并且使用的时间小于锁失效时间时,锁才算获取成功。(如上图,10s> 30ms+40ms+50ms+4m0s+50ms)
    
    4,如果取到了锁,key的真正有效时间就变啦,需要减去获取锁所使用的时间。
    
    5,如果获取锁失败(没有在至少N/2+1个master实例取到锁,有或者获取锁时间已经超过了有效时间),客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功,也需要解锁,以防止有些漏网之鱼)。
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Redisson框架实现了Redlock版本的锁。

    使用Redisson框架就可以解决以上方案的问题,并且是基于集群模式的,是一个目前较完美的分布式锁解决方案。

  • 相关阅读:
    Neuron Newsletter 2022-07|新增非 A11 驱动、即将支持 OPC DA
    Hive 开窗函数如何运用?简单例子说明
    RK3399平台开发系列讲解(PCI/PCI-E)5.55、PCIE RC枚举EP过程
    PHP的四层架构
    多表操作-内连接查询
    【c ++ primer 笔记】第3章 字符串、向量和数组
    Springboot专利申请服务平台 毕业设计-附源码260839
    Java框架 Spring5--事务
    FPGA学习笔记(六)Modelsim单独仿真和Quartus联合仿真
    数字化营销到底是什么?与传统营销有什么区别?
  • 原文地址:https://blog.csdn.net/weixin_44143114/article/details/126613106