• Redis分布式锁最牛逼的实现(Java 版,最牛逼的实现方式)


    写在前面的话

    分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。

    本篇博客将介绍第二种方式,基于Redis实现分布式锁

    为什么需要分布式锁?

    在单机环境下编写多线程程序时,为了避免多个线程同时操作同一个资源,我们往往会通过加锁来实现互斥,以保证同一时间只有一个操作者对某个资源执行操作,在单机多进程的情况下,如果们想操作同一个共享资源,我们也可以通过操作系统提供的文件锁和信号量来实现互斥,这些都是单台机器上的操作。

    而在分布式环境下,如果不同机器上的不同进程需要同时操作某一个共享资源,我们同样也需要这样一个统一的锁来实现互斥。这个时候,我们就需要一个平台来提供这样一个互斥的能力,通常我们会采用一些能够提供一致性的服务,比如 ZooKeeper、 etcd 来满足对一致性要求较高的场景下的互斥需求,当然,也有些服务会用数据库,比如 MySQL,来实现互斥,然而在某些高并发业务场景下,我们通常会采用 Redis来实现。

    Redis分布式锁的实现方式

    Redis分布式锁主要有以下几种实现方式:

    1. SETNX命令

    SETNX命令可以实现在键不存在的情况下设置键的值,利用这一特性可以实现分布式锁的功能。代码如下:

    SETNX lock_key 1

    上述命令会尝试将键名为lock_key的键的值设置为1,只有当该键不存在时,才会进行设置,并返回1;如果该键已存在,则不进行设置,并返回0。利用这个特性,可以实现分布式锁的加锁操作,代码如下:

    1. boolean lock = redis.setnx("lock_key", "1");
    2. if (lock) {
    3. // 获取锁成功
    4. // ...
    5. } else {
    6. // 获取锁失败,需要重试
    7. // ...
    8. }

    解锁操作可以通过DEL命令删除锁对应的键来实现,代码如下:

    redis.del("lock_key");

    2. SET命令带过期时间

    SET命令可以设置键的值以及过期时间。利用这一特性,可以实现分布式锁的自动释放。代码如下:

    SET lock_key 1 PX 30000

    上述命令会将键名为lock_key的键的值设置为1,并设置该键30秒后自动过期。利用这个特性,可以实现分布式锁的加锁操作,代码如下:

    1. boolean lock = redis.set("lock_key", "1", "PX", 30000, "NX");
    2. if (lock != null && lock.equalsIgnoreCase("OK")) {
    3. // 获取锁成功
    4. // ...
    5. } else {
    6. // 获取锁失败,需要重试
    7. // ...
    8. }

    解锁操作可以通过DEL命令删除锁对应的键来实现。

    3. Redlock算法

    Redlock算法是Redis官方推荐的分布式锁算法,它使用多个独立的Redis实例来实现分布式锁。具体实现可参考以下步骤:

    1. 客户端获取当前时间戳作为请求的开始时间;
    2. 客户端依次尝试在多个独立的Redis实例上加锁,每个实例都需要设定相同的过期时间和相同的随机字符串(nonce);
    3. 客户端计算加锁操作的总时间,并将其与指定的超时时间进行比较,如果总时间小于超时时间,则认为加锁成功。否则,客户端需要在所有实例上尝试解锁操作。

    Redlock算法的实现相对复杂,下文单独展开说明。

    Redis分布式锁的底层原理

    Redis分布式锁主要依赖于Redis的单线程模型和原子性操作特性。

    Redis是一个单线程模型,它通过队列来实现多个客户端的请求排队执行。这意味着,在Redis中执行的每个命令都是原子性的,不会存在线程安全问题。

    Redis提供了多个命令可以实现原子性的操作,如SETNX、GETSET等,它们都是通过Redis的事务机制以及WATCH命令来实现的。在Redis执行这些命令时,会对这些命令进行加锁,确保它们的原子性。

    代码实践

    下面通过Java代码演示如何使用Redis实现分布式锁。我们可以使用Jedis客户端来连接Redis服务器并进行操作。

    1. // 初始化Redis客户端
    2. Jedis redis = new Jedis("localhost", 6379);
    3. // 加锁操作
    4. boolean lock = redis.set("lock_key", "1", "PX", 30000, "NX");
    5. if (lock != null && lock.equalsIgnoreCase("OK")) {
    6. // 获取锁成功
    7. // ...
    8. } else {
    9. // 获取锁失败,需要重试
    10. // ...
    11. }
    12. // 解锁操作
    13. redis.del("lock_key");

    上述代码使用SET命令在Redis上加锁,并设置锁的过期时间为30秒。解锁操作通过DEL命令删除对应的键来实现。

    在使用Redis分布式锁时,需要注意加锁、解锁的顺序以及超时时间的设置,以避免出现死锁等问题。

    集群环境下Redis分布式锁的实现方式

    集群环境下,Redis分布式锁的实现方式主要有以下两种:

    1. 基于Redlock算法的实现方式

    在集群环境下,为了保证分布式锁的可靠性和正确性,可以采用Redlock算法来实现。该算法主要包括以下步骤:

    • 客户端获取当前时间戳作为请求的开始时间;
    • 客户端尝试在多个独立的Redis节点上加锁,每个节点都需要设置相同的过期时间和随机字符串(nonce);
    • 如果在大部分Redis节点上加锁成功,并且在指定的超时时间内完成了加锁操作,则认为加锁成功。否则,客户端需要在所有节点上尝试解锁操作。

    通过这种方式,可以有效地避免因某一个节点失效导致的分布式锁失效问题。但需要注意的是,使用Redlock算法的开销比其他方式要高,并且并不是绝对的可靠,因此需要根据具体场景进行选择。下文单独示例说明。

    1. 基于Lua脚本的实现方式

    除了Redlock算法,还可以使用基于Lua脚本的方式来实现Redis分布式锁。该方式的主要思路是通过执行一段Lua脚本来保证加锁和解锁的原子性,避免了由于网络延迟等因素导致的加锁和解锁不一致的问题。

    以下是基于Lua脚本实现Redis分布式锁的代码示例:

    1. public class RedisLock {
    2. private Jedis jedis;
    3. private String lockKey;
    4. private String requestId;
    5. private int expireTime;
    6. public RedisLock(Jedis jedis, String lockKey, int expireTime) {
    7. this.jedis = jedis;
    8. this.lockKey = lockKey;
    9. this.expireTime = expireTime;
    10. this.requestId = UUID.randomUUID().toString();
    11. }
    12. /**
    13. * 尝试获取锁
    14. */
    15. public boolean tryLock() {
    16. String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
    17. if ("OK".equals(result)) {
    18. return true;
    19. }
    20. return false;
    21. }
    22. /**
    23. * 释放锁
    24. */
    25. public void unlock() {
    26. // 使用Lua脚本确保原子性
    27. String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    28. jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    29. }
    30. }

    在该示例中,使用tryLock()方法来尝试加锁,并使用unlock()方法来释放锁。其中,加锁的实现方式为执行SET命令,同时设置NX(只在键不存在时设置)和PX(设置过期时间)选项,并将请求ID作为值存入Redis中。解锁采用了使用Lua脚本执行DEL命令的方式,以保证加锁和解锁的原子性。

    通过这种方式,我们可以简单地实现Redis分布式锁的使用,并且在多个节点之间也可以正确地工作。

    Redlock 实现的分布式锁以及对应的代码实现细节

    Redlock是一种分布式锁算法,由Redis官方推出,并用于解决在分布式系统中实现分布式锁的问题。Redlock算法采用多个节点之间互斥的方式获取分布式锁,可以保证在大部分节点正常情况下分布式锁的可靠性,并允许在某些更繁忙或网络质量较差的节点上失败,从而确保分布式锁的稳定性。

    下面是Redlock的JAVA代码实现细节:

    首先,定义一个名为RedisLock的JAVA类,该类实现了Lock接口并包含了以下几个属性:

    1. private JedisPool[] jedisPools; // Redis连接池
    2. private int quorum; // 在多少个节点上加锁或解锁成功
    3. private int retryCount; // 重试次数
    4. private int retryDelay; // 每次重试之间的延迟时间

    接着,实现加锁方法lock():

    1. @Override
    2. public void lock() {
    3. String nonce = generateNonce();
    4. for (int i = 0; i < retryCount; i++) {
    5. int count = 0;
    6. long start = System.currentTimeMillis();
    7. for (JedisPool pool : jedisPools) {
    8. try (Jedis jedis = pool.getResource()) {
    9. String result = jedis.set(lockKey, nonce, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    10. if (LOCK_SUCCESS.equals(result)) {
    11. count++;
    12. }
    13. } catch (Exception e) {
    14. // ignore
    15. }
    16. }
    17. if (count >= quorum && System.currentTimeMillis() - start <= expireTime) {
    18. return;
    19. }
    20. unlock(nonce);
    21. try {
    22. Thread.sleep(retryDelay);
    23. } catch (InterruptedException e) {
    24. // ignore
    25. }
    26. }
    27. throw new LockException("Unable to acquire lock.");
    28. }

    在加锁方法中,我们首先生成了一个随机字符串nonce作为锁的值,并在每个Redis实例上进行原子性的set操作,返回成功加锁的实例数。如果获取到锁的实例数大于等于quorum(即多数节点),并且加锁操作完成的时间小于锁的过期时间expireTime,则表示加锁成功,否则认为加锁失败,触发重试机制。

    实现解锁方法unlock()

    1. private void unlock(String nonce) {
    2. for (JedisPool pool : jedisPools) {
    3. try (Jedis jedis = pool.getResource()) {
    4. String result = jedis.get(lockKey);
    5. if (nonce.equals(result)) {
    6. jedis.del(lockKey);
    7. }
    8. } catch (Exception e) {
    9. // ignore
    10. }
    11. }
    12. }

    在解锁方法中,我们遍历所有Redis实例,查询锁的值是否为当前nonce,如果是,则删除该实例上的锁。

    综上所述,Redlock算法的JAVA代码实现主要包括两个方法:加锁方法和解锁方法。在加锁方法中,我们通过多次尝试,在大部分节点上获取到锁时完成加锁操作,并在获取到锁的多数节点上进行解锁操作。同时,我们还可以通过调整retryCount和retryDelay的参数来控制重试机制的次数和间隔。需要注意的是,Redlock算法对应的JAVA实现需要保证多个线程使用同一个Lock对象。

  • 相关阅读:
    8款提高小团队协作效率的app软件,你用过几款?
    CMSC5707-高级人工智能之卷积神经网络CNN
    SpringBoot 常用注解汇总
    121. 买卖股票的最佳时机
    “数字驱动 智领未来”—传化化学项目启动会
    springmvc 的web.xml 中无法解析org.springframework.web.servlet.DispatcherServlet爆红解决方法
    从0开始学汇编第一天:基础知识
    学校食堂厨师帽厨师服佩戴识别系统
    JS生成器的介绍
    CSV 文本过滤查询汇总计算
  • 原文地址:https://blog.csdn.net/weixin_48890074/article/details/133816871