活动公告

系统通知
05-18 21:22
系统通知
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,资源失效请在帖子内回复要求补档,会尽快处理!
10-23 09:31

Redis分布式锁获取释放全解析避开那些坑实现高效并发控制

SunJu_FaceMall

3万

主题

2860

科技点

3万

积分

白金月票

碾压王

积分
32872

塔罗立华奏

<font color=白金月票" /> 发表于 2025-9-27 12:40:00 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

在分布式系统中,多个节点同时访问共享资源时,为了保证数据的一致性和正确性,我们需要使用分布式锁来协调各个节点的行为。Redis作为一个高性能的内存数据库,提供了实现分布式锁的理想工具。本文将深入探讨Redis分布式锁的获取和释放机制,分析实现过程中的常见问题,并提供避开这些坑的方法,最终实现高效的并发控制。

Redis分布式锁基础

分布式锁是一种在分布式环境下控制多个进程或线程对共享资源访问的同步机制。Redis分布式锁利用Redis的原子操作特性,可以在分布式系统中实现高效的锁机制。

基本原理

Redis分布式锁的基本原理是利用Redis的SETNX(SET if Not eXists)命令,该命令在指定的key不存在时设置key的值,如果key已经存在,则不做任何操作。通过这个特性,我们可以实现一个简单的互斥锁:

1. 当一个客户端尝试获取锁时,它执行SETNX命令,尝试设置一个特定的key。
2. 如果SETNX返回1,表示key不存在,客户端成功获取了锁。
3. 如果SETNX返回0,表示key已经存在,客户端获取锁失败。

简单实现

下面是一个简单的Redis分布式锁实现:
  1. public class SimpleRedisLock {
  2.     private Jedis jedis;
  3.     private String lockKey;
  4.     private int expireTime = 30; // 默认锁过期时间为30秒
  5.    
  6.     public SimpleRedisLock(Jedis jedis, String lockKey) {
  7.         this.jedis = jedis;
  8.         this.lockKey = lockKey;
  9.     }
  10.    
  11.     public boolean tryLock() {
  12.         // SETNX命令尝试获取锁
  13.         long result = jedis.setnx(lockKey, String.valueOf(System.currentTimeMillis() + expireTime * 1000));
  14.         return result == 1;
  15.     }
  16.    
  17.     public boolean unlock() {
  18.         // 释放锁,直接删除key
  19.         long result = jedis.del(lockKey);
  20.         return result == 1;
  21.     }
  22. }
复制代码

然而,这种简单的实现存在很多问题,比如锁没有设置过期时间可能导致死锁,以及锁的释放可能不是由锁的持有者执行等。在后面的章节中,我们将逐步完善这个实现。

Redis分布式锁的获取

获取Redis分布式锁需要考虑多个方面,包括锁的原子性、锁的唯一标识、锁的过期时间等。下面我们将详细介绍如何正确地获取Redis分布式锁。

SET命令的扩展

在早期版本的Redis中,我们通常使用SETNX命令来实现分布式锁,但SETNX有一个缺点,它不能同时设置key的过期时间。为了解决这个问题,Redis 2.6.12版本开始,SET命令支持了一系列选项,可以原子性地完成设置key和过期时间的操作:
  1. SET lock_key unique_value NX PX 30000
复制代码

这个命令的含义是:

• lock_key:锁的key
• unique_value:唯一值,用于标识锁的持有者
• NX:只有当key不存在时才设置
• PX 30000:设置key的过期时间为30000毫秒(30秒)

使用这个命令,我们可以原子性地完成锁的获取和过期时间的设置,避免了先SETNX再EXPIRE可能导致的死锁问题。

获取锁的完整实现

下面是一个更完善的Redis分布式锁获取实现:
  1. public class RedisDistributedLock {
  2.     private Jedis jedis;
  3.     private String lockKey;
  4.     private String requestId; // 唯一标识,用于标识锁的持有者
  5.     private int expireTime = 30; // 默认锁过期时间为30秒
  6.    
  7.     public RedisDistributedLock(Jedis jedis, String lockKey) {
  8.         this.jedis = jedis;
  9.         this.lockKey = lockKey;
  10.         this.requestId = UUID.randomUUID().toString(); // 生成唯一标识
  11.     }
  12.    
  13.     /**
  14.      * 尝试获取锁
  15.      * @return 是否获取成功
  16.      */
  17.     public boolean tryLock() {
  18.         // 使用SET命令尝试获取锁
  19.         String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime * 1000);
  20.         return "OK".equals(result);
  21.     }
  22.    
  23.     /**
  24.      * 带超时时间的获取锁
  25.      * @param timeout 获取锁的超时时间(毫秒)
  26.      * @return 是否获取成功
  27.      */
  28.     public boolean tryLock(long timeout) {
  29.         long startTime = System.currentTimeMillis();
  30.         long end = startTime + timeout;
  31.         
  32.         while (System.currentTimeMillis() < end) {
  33.             if (tryLock()) {
  34.                 return true;
  35.             }
  36.             try {
  37.                 Thread.sleep(100); // 短暂休眠,避免CPU空转
  38.             } catch (InterruptedException e) {
  39.                 Thread.currentThread().interrupt();
  40.                 return false;
  41.             }
  42.         }
  43.         return false;
  44.     }
  45.    
  46.     // 其他方法...
  47. }
复制代码

在这个实现中,我们:

1. 使用SET命令原子性地设置锁和过期时间
2. 为每个锁生成一个唯一标识(requestId),用于标识锁的持有者
3. 提供了带超时时间的获取锁方法,在超时时间内不断尝试获取锁

锁的可重入性

可重入锁是指同一个线程可以多次获取同一个锁而不会造成死锁。要实现Redis分布式锁的可重入性,我们需要在锁中记录获取锁的次数和线程信息:
  1. public class ReentrantRedisLock {
  2.     private Jedis jedis;
  3.     private String lockKey;
  4.     private String requestId; // 唯一标识,用于标识锁的持有者
  5.     private int expireTime = 30; // 默认锁过期时间为30秒
  6.     private ThreadLocal<Integer> lockCount = new ThreadLocal<>(); // 记录当前线程获取锁的次数
  7.    
  8.     public ReentrantRedisLock(Jedis jedis, String lockKey) {
  9.         this.jedis = jedis;
  10.         this.lockKey = lockKey;
  11.         this.requestId = Thread.currentThread().getId() + ":" + UUID.randomUUID().toString();
  12.     }
  13.    
  14.     /**
  15.      * 尝试获取锁
  16.      * @return 是否获取成功
  17.      */
  18.     public boolean lock() {
  19.         // 检查当前线程是否已经持有锁
  20.         if (lockCount.get() != null && lockCount.get() > 0) {
  21.             // 已经持有锁,增加计数
  22.             lockCount.set(lockCount.get() + 1);
  23.             return true;
  24.         }
  25.         
  26.         // 使用SET命令尝试获取锁
  27.         String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime * 1000);
  28.         if ("OK".equals(result)) {
  29.             lockCount.set(1); // 初始化计数
  30.             return true;
  31.         }
  32.         return false;
  33.     }
  34.    
  35.     // 其他方法...
  36. }
复制代码

在这个实现中,我们使用ThreadLocal来记录当前线程获取锁的次数,如果线程已经持有锁,则增加计数而不是重新获取锁。

Redis分布式锁的释放

释放Redis分布式锁同样需要考虑多个方面,包括确保只有锁的持有者才能释放锁,以及避免误删其他客户端的锁等。

简单释放的问题

最简单的释放锁方法是直接删除锁的key:
  1. public boolean unlock() {
  2.     long result = jedis.del(lockKey);
  3.     return result == 1;
  4. }
复制代码

然而,这种方法存在一个严重的问题:如果一个客户端获取锁后,由于某些原因(如GC暂停、网络延迟等)导致锁过期,然后另一个客户端获取了同一个锁,这时第一个客户端执行unlock方法,会错误地释放第二个客户端的锁。

正确的释放方式

为了避免上述问题,我们需要在释放锁时验证锁的唯一标识,确保只有锁的持有者才能释放锁:
  1. public boolean unlock() {
  2.     // 使用Lua脚本确保操作的原子性
  3.     String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  4.     Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
  5.     return Long.parseLong(result.toString()) == 1;
  6. }
复制代码

在这个实现中,我们使用Lua脚本确保获取锁的值和删除锁的操作是原子性的。只有当锁的值与当前客户端的requestId匹配时,才会删除锁。

可重入锁的释放

对于可重入锁,我们需要在释放锁时减少计数,只有当计数减到0时才真正释放锁:
  1. public boolean unlock() {
  2.     // 检查当前线程是否持有锁
  3.     if (lockCount.get() == null || lockCount.get() <= 0) {
  4.         return false;
  5.     }
  6.    
  7.     // 减少计数
  8.     int newCount = lockCount.get() - 1;
  9.     lockCount.set(newCount);
  10.    
  11.     // 如果计数为0,则真正释放锁
  12.     if (newCount == 0) {
  13.         // 使用Lua脚本确保操作的原子性
  14.         String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  15.         Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
  16.         return Long.parseLong(result.toString()) == 1;
  17.     }
  18.    
  19.     return true;
  20. }
复制代码

锁的续期

在某些场景下,业务逻辑的执行时间可能超过锁的过期时间,这时我们需要在锁过期前对其进行续期,以避免锁自动释放导致的问题。下面是一个实现锁续期的例子:
  1. public class RedisDistributedLockWithRenewal {
  2.     private Jedis jedis;
  3.     private String lockKey;
  4.     private String requestId;
  5.     private int expireTime = 30; // 默认锁过期时间为30秒
  6.     private ScheduledExecutorService scheduler;
  7.     private Future<?> renewalTask;
  8.    
  9.     public RedisDistributedLockWithRenewal(Jedis jedis, String lockKey) {
  10.         this.jedis = jedis;
  11.         this.lockKey = lockKey;
  12.         this.requestId = UUID.randomUUID().toString();
  13.         this.scheduler = Executors.newScheduledThreadPool(1);
  14.     }
  15.    
  16.     /**
  17.      * 尝试获取锁
  18.      * @return 是否获取成功
  19.      */
  20.     public boolean tryLock() {
  21.         // 使用SET命令尝试获取锁
  22.         String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime * 1000);
  23.         if ("OK".equals(result)) {
  24.             // 获取锁成功,启动续期任务
  25.             startRenewalTask();
  26.             return true;
  27.         }
  28.         return false;
  29.     }
  30.    
  31.     /**
  32.      * 启动续期任务
  33.      */
  34.     private void startRenewalTask() {
  35.         // 锁过期时间的1/3时间后开始续期
  36.         int renewalInterval = expireTime * 1000 / 3;
  37.         renewalTask = scheduler.scheduleAtFixedRate(() -> {
  38.             // 使用Lua脚本续期
  39.             String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
  40.             Object result = jedis.eval(script, Collections.singletonList(lockKey),
  41.                                      Arrays.asList(requestId, String.valueOf(expireTime * 1000)));
  42.             if (Long.parseLong(result.toString()) == 0) {
  43.                 // 续期失败,取消续期任务
  44.                 renewalTask.cancel(false);
  45.             }
  46.         }, renewalInterval, renewalInterval, TimeUnit.MILLISECONDS);
  47.     }
  48.    
  49.     /**
  50.      * 释放锁
  51.      * @return 是否释放成功
  52.      */
  53.     public boolean unlock() {
  54.         // 取消续期任务
  55.         if (renewalTask != null) {
  56.             renewalTask.cancel(false);
  57.         }
  58.         
  59.         // 使用Lua脚本释放锁
  60.         String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  61.         Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
  62.         return Long.parseLong(result.toString()) == 1;
  63.     }
  64.    
  65.     /**
  66.      * 关闭锁,释放资源
  67.      */
  68.     public void close() {
  69.         // 取消续期任务
  70.         if (renewalTask != null) {
  71.             renewalTask.cancel(false);
  72.         }
  73.         // 关闭线程池
  74.         scheduler.shutdown();
  75.     }
  76. }
复制代码

在这个实现中,我们使用ScheduledExecutorService来定期续期锁。续期的时间间隔是锁过期时间的1/3,这样可以确保在锁过期前至少有两次续期机会。续期操作同样使用Lua脚本确保原子性。

常见问题和坑

在实现和使用Redis分布式锁的过程中,有很多常见的问题和坑需要注意。下面我们将分析这些问题,并提供解决方案。

问题1:锁过期但业务未完成

这是一个经典的问题:客户端A获取了锁,但业务逻辑的执行时间超过了锁的过期时间,导致锁自动释放。然后客户端B获取了锁,这时客户端A完成了业务逻辑,尝试释放锁,结果错误地释放了客户端B的锁。

解决方案:

1. 为每个锁生成唯一标识,并在释放锁时验证这个标识。
2. 使用Lua脚本确保获取锁的值和删除锁的操作是原子性的。
3. 考虑实现锁的自动续期机制。

问题2:Redis单点故障

如果使用单个Redis实例作为分布式锁服务,当这个实例发生故障时,所有的锁服务都会不可用。

解决方案:

1. 使用Redis集群,确保高可用性。
2. 使用多个独立的Redis实例,实现RedLock算法。

问题3:时钟漂移

Redis分布式锁依赖过期时间来自动释放锁,如果不同Redis实例的时钟存在漂移,可能导致锁提前或延迟释放。

解决方案:

1. 尽量使用NTP同步所有服务器的时间。
2. 在设置锁的过期时间时,考虑时钟漂移的可能性,适当延长过期时间。
3. 实现锁的自动续期机制,减少对过期时间的依赖。

问题4:GC暂停导致的锁过期

在Java应用中,长时间的GC暂停可能导致线程无法及时续期锁,从而导致锁过期。

解决方案:

1. 优化JVM参数,减少长时间GC暂停的可能性。
2. 使用多个续期线程,确保即使一个线程被GC暂停,其他线程也能继续续期。
3. 考虑使用更短的锁过期时间和更频繁的续期操作。

问题5:网络分区

在网络分区的情况下,客户端可能无法与Redis服务器通信,导致无法获取锁或释放锁。

解决方案:

1. 实现重试机制,在网络恢复后自动重试。
2. 设置合理的超时时间,避免无限等待。
3. 考虑使用本地锁作为后备方案,在网络分区时提供有限的并发控制。

问题6:锁的误删

在实现不完善的情况下,可能会出现客户端错误地删除其他客户端的锁。

解决方案:

1. 为每个锁生成唯一标识,并在释放锁时验证这个标识。
2. 使用Lua脚本确保获取锁的值和删除锁的操作是原子性的。

高效并发控制

Redis分布式锁不仅仅是获取和释放锁,还需要考虑如何实现高效的并发控制。下面我们将介绍一些优化策略和技术。

1. 锁分段

当并发量很大时,单个锁可能成为性能瓶颈。这时可以考虑使用锁分段技术,将资源分成多个段,每个段使用一个独立的锁。
  1. public class SegmentedRedisLock {
  2.     private Jedis jedis;
  3.     private String baseKey;
  4.     private int segmentCount;
  5.     private RedisDistributedLock[] locks;
  6.    
  7.     public SegmentedRedisLock(Jedis jedis, String baseKey, int segmentCount) {
  8.         this.jedis = jedis;
  9.         this.baseKey = baseKey;
  10.         this.segmentCount = segmentCount;
  11.         this.locks = new RedisDistributedLock[segmentCount];
  12.         
  13.         // 初始化分段锁
  14.         for (int i = 0; i < segmentCount; i++) {
  15.             locks[i] = new RedisDistributedLock(jedis, baseKey + ":" + i);
  16.         }
  17.     }
  18.    
  19.     /**
  20.      * 根据资源的key获取对应的锁
  21.      * @param resourceKey 资源key
  22.      * @return 对应的锁
  23.      */
  24.     public RedisDistributedLock getLock(String resourceKey) {
  25.         int hash = Math.abs(resourceKey.hashCode());
  26.         int segment = hash % segmentCount;
  27.         return locks[segment];
  28.     }
  29.    
  30.     /**
  31.      * 锁定资源
  32.      * @param resourceKey 资源key
  33.      * @return 是否锁定成功
  34.      */
  35.     public boolean lock(String resourceKey) {
  36.         return getLock(resourceKey).tryLock();
  37.     }
  38.    
  39.     /**
  40.      * 释放资源锁
  41.      * @param resourceKey 资源key
  42.      * @return 是否释放成功
  43.      */
  44.     public boolean unlock(String resourceKey) {
  45.         return getLock(resourceKey).unlock();
  46.     }
  47. }
复制代码

在这个实现中,我们将资源分成多个段,每个资源根据其key的哈希值分配到不同的段上,从而减少锁竞争。

2. 读写锁

在读多写少的场景下,使用互斥锁会导致并发性能下降。这时可以考虑实现读写锁,允许多个读操作同时进行,但写操作是互斥的。
  1. public class RedisReadWriteLock {
  2.     private Jedis jedis;
  3.     private String lockKey;
  4.     private String readLockKey;
  5.     private String writeLockKey;
  6.     private String requestId;
  7.     private int expireTime = 30; // 默认锁过期时间为30秒
  8.    
  9.     public RedisReadWriteLock(Jedis jedis, String lockKey) {
  10.         this.jedis = jedis;
  11.         this.lockKey = lockKey;
  12.         this.readLockKey = lockKey + ":read";
  13.         this.writeLockKey = lockKey + ":write";
  14.         this.requestId = UUID.randomUUID().toString();
  15.     }
  16.    
  17.     /**
  18.      * 获取读锁
  19.      * @return 是否获取成功
  20.      */
  21.     public boolean readLock() {
  22.         // 检查是否有写锁
  23.         String writeLockValue = jedis.get(writeLockKey);
  24.         if (writeLockValue != null) {
  25.             return false;
  26.         }
  27.         
  28.         // 获取读锁
  29.         long result = jedis.incr(readLockKey);
  30.         if (result == 1) {
  31.             // 第一个读锁,设置过期时间
  32.             jedis.expire(readLockKey, expireTime);
  33.         }
  34.         return true;
  35.     }
  36.    
  37.     /**
  38.      * 释放读锁
  39.      * @return 是否释放成功
  40.      */
  41.     public boolean readUnlock() {
  42.         // 释放读锁
  43.         long result = jedis.decr(readLockKey);
  44.         if (result == 0) {
  45.             // 没有读锁了,删除key
  46.             jedis.del(readLockKey);
  47.         }
  48.         return true;
  49.     }
  50.    
  51.     /**
  52.      * 获取写锁
  53.      * @return 是否获取成功
  54.      */
  55.     public boolean writeLock() {
  56.         // 检查是否有写锁
  57.         String writeLockValue = jedis.get(writeLockKey);
  58.         if (writeLockValue != null) {
  59.             return false;
  60.         }
  61.         
  62.         // 检查是否有读锁
  63.         String readLockValue = jedis.get(readLockKey);
  64.         if (readLockValue != null && Long.parseLong(readLockValue) > 0) {
  65.             return false;
  66.         }
  67.         
  68.         // 获取写锁
  69.         String result = jedis.set(writeLockKey, requestId, "NX", "PX", expireTime * 1000);
  70.         return "OK".equals(result);
  71.     }
  72.    
  73.     /**
  74.      * 释放写锁
  75.      * @return 是否释放成功
  76.      */
  77.     public boolean writeUnlock() {
  78.         // 使用Lua脚本确保操作的原子性
  79.         String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  80.         Object result = jedis.eval(script, Collections.singletonList(writeLockKey), Collections.singletonList(requestId));
  81.         return Long.parseLong(result.toString()) == 1;
  82.     }
  83. }
复制代码

在这个实现中,我们使用两个key来实现读写锁:一个用于读锁(readLockKey),一个用于写锁(writeLockKey)。读锁使用计数器来记录当前有多少个读操作,写锁使用SET命令来确保互斥性。

3. 锁优化策略

除了锁分段和读写锁,还有一些其他的优化策略可以提高并发性能:

尽量使用细粒度的锁,而不是粗粒度的锁。例如,如果可能的话,锁定单个记录而不是整个表。

在需要获取多个锁的情况下,始终按照相同的顺序获取锁,以避免死锁。

设置合理的锁获取超时时间,并在获取失败时进行适当的重试,避免无限等待。

对于频繁获取和释放的锁,可以考虑在本地缓存锁的状态,减少对Redis的访问。
  1. public class CachedRedisLock {
  2.     private Jedis jedis;
  3.     private String lockKey;
  4.     private String requestId;
  5.     private int expireTime = 30; // 默认锁过期时间为30秒
  6.     private ConcurrentHashMap<String, Boolean> localLocks = new ConcurrentHashMap<>();
  7.    
  8.     public CachedRedisLock(Jedis jedis, String lockKey) {
  9.         this.jedis = jedis;
  10.         this.lockKey = lockKey;
  11.         this.requestId = UUID.randomUUID().toString();
  12.     }
  13.    
  14.     /**
  15.      * 尝试获取锁
  16.      * @return 是否获取成功
  17.      */
  18.     public boolean tryLock() {
  19.         // 首先检查本地缓存
  20.         if (localLocks.containsKey(lockKey)) {
  21.             return false;
  22.         }
  23.         
  24.         // 尝试从Redis获取锁
  25.         String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime * 1000);
  26.         if ("OK".equals(result)) {
  27.             // 获取成功,更新本地缓存
  28.             localLocks.put(lockKey, true);
  29.             return true;
  30.         }
  31.         return false;
  32.     }
  33.    
  34.     /**
  35.      * 释放锁
  36.      * @return 是否释放成功
  37.      */
  38.     public boolean unlock() {
  39.         // 检查本地缓存
  40.         if (!localLocks.containsKey(lockKey)) {
  41.             return false;
  42.         }
  43.         
  44.         // 使用Lua脚本释放锁
  45.         String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  46.         Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
  47.         if (Long.parseLong(result.toString()) == 1) {
  48.             // 释放成功,更新本地缓存
  49.             localLocks.remove(lockKey);
  50.             return true;
  51.         }
  52.         return false;
  53.     }
  54. }
复制代码

在这个实现中,我们使用本地缓存来记录锁的状态,减少对Redis的访问。这种方法适用于锁竞争激烈的场景,但需要注意本地缓存和Redis之间的同步问题。

完整示例

下面是一个完整的Redis分布式锁实现,包含了前面讨论的各种特性:
  1. import redis.clients.jedis.Jedis;
  2. import redis.clients.jedis.params.SetParams;
  3. import java.util.Collections;
  4. import java.util.UUID;
  5. import java.util.concurrent.*;
  6. public class CompleteRedisLock implements AutoCloseable {
  7.     private Jedis jedis;
  8.     private String lockKey;
  9.     private String requestId;
  10.     private int expireTime = 30; // 默认锁过期时间为30秒
  11.     private ThreadLocal<Integer> lockCount = new ThreadLocal<>(); // 记录当前线程获取锁的次数
  12.     private ScheduledExecutorService scheduler;
  13.     private Future<?> renewalTask;
  14.     private boolean locked = false;
  15.    
  16.     public CompleteRedisLock(Jedis jedis, String lockKey) {
  17.         this.jedis = jedis;
  18.         this.lockKey = lockKey;
  19.         this.requestId = Thread.currentThread().getId() + ":" + UUID.randomUUID().toString();
  20.         this.scheduler = Executors.newScheduledThreadPool(1);
  21.     }
  22.    
  23.     /**
  24.      * 尝试获取锁
  25.      * @return 是否获取成功
  26.      */
  27.     public boolean tryLock() {
  28.         // 检查当前线程是否已经持有锁
  29.         if (lockCount.get() != null && lockCount.get() > 0) {
  30.             // 已经持有锁,增加计数
  31.             lockCount.set(lockCount.get() + 1);
  32.             return true;
  33.         }
  34.         
  35.         // 使用SET命令尝试获取锁
  36.         SetParams params = SetParams.setParams().nx().px(expireTime * 1000);
  37.         String result = jedis.set(lockKey, requestId, params);
  38.         if ("OK".equals(result)) {
  39.             // 获取锁成功,启动续期任务
  40.             startRenewalTask();
  41.             lockCount.set(1);
  42.             locked = true;
  43.             return true;
  44.         }
  45.         return false;
  46.     }
  47.    
  48.     /**
  49.      * 带超时时间的获取锁
  50.      * @param timeout 获取锁的超时时间(毫秒)
  51.      * @return 是否获取成功
  52.      */
  53.     public boolean tryLock(long timeout) {
  54.         long startTime = System.currentTimeMillis();
  55.         long end = startTime + timeout;
  56.         
  57.         while (System.currentTimeMillis() < end) {
  58.             if (tryLock()) {
  59.                 return true;
  60.             }
  61.             try {
  62.                 Thread.sleep(100); // 短暂休眠,避免CPU空转
  63.             } catch (InterruptedException e) {
  64.                 Thread.currentThread().interrupt();
  65.                 return false;
  66.             }
  67.         }
  68.         return false;
  69.     }
  70.    
  71.     /**
  72.      * 启动续期任务
  73.      */
  74.     private void startRenewalTask() {
  75.         // 锁过期时间的1/3时间后开始续期
  76.         int renewalInterval = expireTime * 1000 / 3;
  77.         renewalTask = scheduler.scheduleAtFixedRate(() -> {
  78.             // 使用Lua脚本续期
  79.             String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end";
  80.             Object result = jedis.eval(script, Collections.singletonList(lockKey),
  81.                                      Arrays.asList(requestId, String.valueOf(expireTime * 1000)));
  82.             if (Long.parseLong(result.toString()) == 0) {
  83.                 // 续期失败,取消续期任务
  84.                 renewalTask.cancel(false);
  85.             }
  86.         }, renewalInterval, renewalInterval, TimeUnit.MILLISECONDS);
  87.     }
  88.    
  89.     /**
  90.      * 释放锁
  91.      * @return 是否释放成功
  92.      */
  93.     public boolean unlock() {
  94.         // 检查当前线程是否持有锁
  95.         if (lockCount.get() == null || lockCount.get() <= 0) {
  96.             return false;
  97.         }
  98.         
  99.         // 减少计数
  100.         int newCount = lockCount.get() - 1;
  101.         lockCount.set(newCount);
  102.         
  103.         // 如果计数为0,则真正释放锁
  104.         if (newCount == 0) {
  105.             // 取消续期任务
  106.             if (renewalTask != null) {
  107.                 renewalTask.cancel(false);
  108.             }
  109.             
  110.             // 使用Lua脚本释放锁
  111.             String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  112.             Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
  113.             if (Long.parseLong(result.toString()) == 1) {
  114.                 locked = false;
  115.                 return true;
  116.             }
  117.             return false;
  118.         }
  119.         
  120.         return true;
  121.     }
  122.    
  123.     /**
  124.      * 检查当前线程是否持有锁
  125.      * @return 是否持有锁
  126.      */
  127.     public boolean isLocked() {
  128.         return locked;
  129.     }
  130.    
  131.     /**
  132.      * 设置锁的过期时间
  133.      * @param expireTime 过期时间(秒)
  134.      */
  135.     public void setExpireTime(int expireTime) {
  136.         this.expireTime = expireTime;
  137.     }
  138.    
  139.     /**
  140.      * 关闭锁,释放资源
  141.      */
  142.     @Override
  143.     public void close() {
  144.         // 释放锁
  145.         if (locked) {
  146.             unlock();
  147.         }
  148.         
  149.         // 取消续期任务
  150.         if (renewalTask != null) {
  151.             renewalTask.cancel(false);
  152.         }
  153.         
  154.         // 关闭线程池
  155.         scheduler.shutdown();
  156.         try {
  157.             if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
  158.                 scheduler.shutdownNow();
  159.             }
  160.         } catch (InterruptedException e) {
  161.             scheduler.shutdownNow();
  162.             Thread.currentThread().interrupt();
  163.         }
  164.     }
  165. }
复制代码

这个完整的Redis分布式锁实现包含了以下特性:

1. 可重入性:同一个线程可以多次获取同一个锁
2. 锁的自动续期:防止业务逻辑执行时间超过锁的过期时间
3. 原子性操作:使用Lua脚本确保关键操作的原子性
4. 资源管理:实现AutoCloseable接口,支持try-with-resources语法
5. 唯一标识:为每个锁生成唯一标识,避免误删其他客户端的锁

使用示例:
  1. public class RedisLockExample {
  2.     public static void main(String[] args) {
  3.         Jedis jedis = new Jedis("localhost", 6379);
  4.         
  5.         try (CompleteRedisLock lock = new CompleteRedisLock(jedis, "myLock")) {
  6.             // 尝试获取锁,最多等待10秒
  7.             if (lock.tryLock(10000)) {
  8.                 System.out.println("获取锁成功,执行业务逻辑...");
  9.                
  10.                 // 模拟业务逻辑执行
  11.                 try {
  12.                     Thread.sleep(5000);
  13.                 } catch (InterruptedException e) {
  14.                     Thread.currentThread().interrupt();
  15.                 }
  16.                
  17.                 System.out.println("业务逻辑执行完成");
  18.             } else {
  19.                 System.out.println("获取锁失败");
  20.             }
  21.         } catch (Exception e) {
  22.             e.printStackTrace();
  23.         } finally {
  24.             jedis.close();
  25.         }
  26.     }
  27. }
复制代码

总结

Redis分布式锁是分布式系统中实现并发控制的重要工具。在实现和使用Redis分布式锁时,需要注意以下几点:

1. 原子性操作:使用Redis的SET命令和Lua脚本确保关键操作的原子性,避免竞态条件。
2. 锁的唯一标识:为每个锁生成唯一标识,并在释放锁时验证这个标识,避免误删其他客户端的锁。
3. 锁的过期时间:设置合理的锁过期时间,防止死锁,但也要考虑业务逻辑的执行时间。
4. 锁的自动续期:对于可能长时间执行的业务逻辑,实现锁的自动续期机制,防止锁过期。
5. 可重入性:在需要的情况下,实现锁的可重入性,允许同一个线程多次获取同一个锁。
6. 高可用性:考虑使用Redis集群或多个独立的Redis实例,确保锁服务的高可用性。
7. 性能优化:根据实际场景,考虑使用锁分段、读写锁等技术提高并发性能。

原子性操作:使用Redis的SET命令和Lua脚本确保关键操作的原子性,避免竞态条件。

锁的唯一标识:为每个锁生成唯一标识,并在释放锁时验证这个标识,避免误删其他客户端的锁。

锁的过期时间:设置合理的锁过期时间,防止死锁,但也要考虑业务逻辑的执行时间。

锁的自动续期:对于可能长时间执行的业务逻辑,实现锁的自动续期机制,防止锁过期。

可重入性:在需要的情况下,实现锁的可重入性,允许同一个线程多次获取同一个锁。

高可用性:考虑使用Redis集群或多个独立的Redis实例,确保锁服务的高可用性。

性能优化:根据实际场景,考虑使用锁分段、读写锁等技术提高并发性能。

通过遵循这些原则和技巧,我们可以实现一个高效、可靠的Redis分布式锁,为分布式系统提供有效的并发控制。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则