活动公告

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

Redis锁从获取到释放全流程解析掌握分布式并发控制关键技术避免死锁提升系统稳定性实用指南

SunJu_FaceMall

3万

主题

3077

科技点

3万

积分

执行版主

碾压王

积分
32876

塔罗立华奏

执行版主 发表于 2025-9-27 14:40:00 | 显示全部楼层 |阅读模式

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

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

x
1. 引言

在分布式系统中,由于多个节点同时访问共享资源,可能会导致数据不一致的问题。为了确保数据的一致性和完整性,我们需要引入分布式锁机制。Redis作为一个高性能的内存数据库,提供了多种实现分布式锁的方式,因其高性能、简单易用的特点,被广泛应用于分布式系统中。

Redis锁不仅可以有效控制并发访问,还能提高系统的可用性和稳定性。本文将详细解析Redis锁从获取到释放的全流程,帮助读者掌握分布式并发控制的关键技术,避免死锁问题,提升系统稳定性。

2. Redis锁基础概念

2.1 什么是Redis锁

Redis锁是利用Redis的特性实现的一种分布式锁机制。它通过在Redis中设置一个特殊的键值对来表示锁的状态,当多个客户端尝试获取同一个锁时,只有一个客户端能够成功获取锁,其他客户端需要等待或放弃。

2.2 为什么需要Redis锁

在分布式系统中,多个服务实例可能同时访问共享资源,如果没有适当的并发控制机制,可能会导致以下问题:

1. 数据不一致:多个节点同时修改同一份数据,可能导致数据错乱。
2. 资源竞争:多个节点同时竞争有限的资源,可能导致系统性能下降。
3. 重复操作:在某些场景下,如定时任务,多个节点可能同时执行相同的任务,导致重复处理。

Redis锁可以解决这些问题,确保在同一时间只有一个节点能够执行关键操作。

3. Redis锁的实现方式

3.1 基于SETNX的实现

SETNX(SET if Not eXists)是Redis提供的一个原子操作命令,它只在键不存在时设置键的值。这是实现Redis锁最基本的方式。
  1. // 获取锁
  2. public boolean tryLock(String key, String value, long expireTime) {
  3.     return redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS);
  4. }
  5. // 释放锁
  6. public void unlock(String key, String value) {
  7.     String currentValue = redisTemplate.opsForValue().get(key);
  8.     if (value.equals(currentValue)) {
  9.         redisTemplate.delete(key);
  10.     }
  11. }
复制代码

这种简单的实现存在几个问题:

1. 锁无法自动释放:如果获取锁的客户端崩溃,锁将无法释放,导致其他客户端无法获取锁。
2. 可能释放他人的锁:如果一个客户端获取锁后,由于操作时间过长导致锁过期,然后另一个客户端获取了锁,此时第一个客户端释放锁时可能会释放第二个客户端的锁。
3. 无法重入:同一个线程无法多次获取同一个锁。

为了解决上述问题,我们可以进行如下改进:
  1. // 获取锁
  2. public boolean tryLock(String key, String value, long expireTime) {
  3.     // 使用SET命令同时设置NX和EX选项,确保原子性
  4.     Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS);
  5.     return result != null && result;
  6. }
  7. // 释放锁,使用Lua脚本确保原子性
  8. public void unlock(String key, String value) {
  9.     String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  10.     DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
  11.     Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
  12.     // 可以根据result判断是否成功释放锁
  13. }
复制代码

3.2 基于RedLock算法的实现

RedLock是Redis官方提出的一种分布式锁算法,它通过在多个Redis实例上获取锁来提高锁的可靠性和安全性。

RedLock算法的基本思想是:在N个独立的Redis实例上获取锁,只有在大多数实例(N/2 + 1)上成功获取锁,并且获取锁的总时间小于锁的有效期时,才认为锁获取成功。
  1. public class RedLock {
  2.     private final List<RedisTemplate<String, String>> redisTemplates;
  3.     private final long lockValidityTime; // 锁的有效时间,单位毫秒
  4.    
  5.     public RedLock(List<RedisTemplate<String, String>> redisTemplates, long lockValidityTime) {
  6.         this.redisTemplates = redisTemplates;
  7.         this.lockValidityTime = lockValidityTime;
  8.     }
  9.    
  10.     public boolean lock(String resourceId, String value) {
  11.         int successCount = 0;
  12.         long startTime = System.currentTimeMillis();
  13.         
  14.         // 尝试在所有Redis实例上获取锁
  15.         for (RedisTemplate<String, String> template : redisTemplates) {
  16.             Boolean result = template.opsForValue().setIfAbsent(resourceId, value, lockValidityTime, TimeUnit.MILLISECONDS);
  17.             if (Boolean.TRUE.equals(result)) {
  18.                 successCount++;
  19.             }
  20.         }
  21.         
  22.         // 计算获取锁所花费的时间
  23.         long elapsedTime = System.currentTimeMillis() - startTime;
  24.         
  25.         // 检查是否在大多数实例上获取了锁,并且获取锁的时间小于锁的有效期
  26.         if (successCount >= (redisTemplates.size() / 2 + 1) && elapsedTime < lockValidityTime) {
  27.             return true;
  28.         }
  29.         
  30.         // 如果获取锁失败,释放已经获取的锁
  31.         unlock(resourceId, value);
  32.         return false;
  33.     }
  34.    
  35.     public void unlock(String resourceId, String value) {
  36.         String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  37.         
  38.         for (RedisTemplate<String, String> template : redisTemplates) {
  39.             DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
  40.             template.execute(redisScript, Collections.singletonList(resourceId), value);
  41.         }
  42.     }
  43. }
复制代码

3.3 基于Redisson的实现

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式Java常用对象,还提供了许多分布式服务,其中包括分布式锁。
  1. // 创建Redisson客户端
  2. Config config = new Config();
  3. config.useSingleServer().setAddress("redis://127.0.0.1:6379");
  4. RedissonClient redisson = Redisson.create(config);
  5. // 获取锁对象
  6. RLock lock = redisson.getLock("myLock");
  7. try {
  8.     // 尝试获取锁,最多等待100秒,锁自动释放时间为10秒
  9.     boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
  10.     if (res) {
  11.         // 成功获取锁,执行业务逻辑
  12.         doBusiness();
  13.     }
  14. } catch (InterruptedException e) {
  15.     e.printStackTrace();
  16. } finally {
  17.     // 释放锁
  18.     lock.unlock();
  19. }
复制代码

1. 可重入:同一个线程可以多次获取同一个锁。
2. 自动续期:Redisson会自动为锁续期,确保业务逻辑执行完毕前锁不会过期。
3. 等待锁:可以设置获取锁的等待时间,在等待时间内不断尝试获取锁。
4. 锁超时:可以设置锁的自动释放时间,防止死锁。

4. Redis锁获取全流程

4.1 获取锁的基本流程

获取Redis锁的基本流程如下:

1. 客户端向Redis发送SETNX命令,尝试设置一个特定的键值对。
2. 如果设置成功(键之前不存在),则表示获取锁成功。
3. 如果设置失败(键已存在),则表示锁已被其他客户端持有,获取锁失败。
  1. public boolean acquireLock(String lockKey, String requestId, long expireTime) {
  2.     // 使用SET命令同时设置NX和EX选项,确保原子性
  3.     Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
  4.     return result != null && result;
  5. }
复制代码

4.2 锁的超时设置

为了避免锁无法释放导致死锁,我们需要为锁设置一个合理的过期时间。过期时间的设置应该考虑以下因素:

1. 业务逻辑的执行时间:过期时间应该大于业务逻辑的正常执行时间。
2. 系统负载:在高负载情况下,业务逻辑的执行时间可能会延长,过期时间应该适当增加。
3. 锁的重要性:对于关键业务,可以适当延长过期时间,避免锁过早释放导致数据不一致。
  1. // 获取锁,并设置过期时间为30秒
  2. public boolean lockWithExpire(String lockKey, String requestId) {
  3.     long expireTime = 30000; // 30秒
  4.     return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
  5. }
复制代码

4.3 锁的重入机制

锁的重入是指同一个线程可以多次获取同一个锁。实现锁的重入需要记录锁的持有者和重入次数。

1. 使用Redis的Hash结构存储锁信息,包括锁的持有者和重入次数。
2. 当同一个线程再次获取锁时,增加重入次数。
3. 当线程释放锁时,减少重入次数,只有当重入次数为0时才真正释放锁。
  1. // 获取可重入锁
  2. public boolean acquireReentrantLock(String lockKey, String requestId, long expireTime) {
  3.     // 使用Lua脚本确保原子性
  4.     String luaScript =
  5.         "if redis.call('exists', KEYS[1]) == 0 then " +
  6.         "   redis.call('hset', KEYS[1], ARGV[1], 1) " +
  7.         "   redis.call('expire', KEYS[1], ARGV[2]) " +
  8.         "   return 1 " +
  9.         "elseif redis.call('hexists', KEYS[1], ARGV[1]) == 1 then " +
  10.         "   redis.call('hincrby', KEYS[1], ARGV[1], 1) " +
  11.         "   redis.call('expire', KEYS[1], ARGV[2]) " +
  12.         "   return 1 " +
  13.         "else " +
  14.         "   return 0 " +
  15.         "end";
  16.    
  17.     DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
  18.     Long result = redisTemplate.execute(redisScript,
  19.                                        Collections.singletonList(lockKey),
  20.                                        requestId,
  21.                                        String.valueOf(expireTime / 1000));
  22.    
  23.     return result != null && result == 1;
  24. }
  25. // 释放可重入锁
  26. public boolean releaseReentrantLock(String lockKey, String requestId) {
  27.     String luaScript =
  28.         "if redis.call('hexists', KEYS[1], ARGV[1]) == 0 then " +
  29.         "   return 0 " +
  30.         "end " +
  31.         "local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1) " +
  32.         "if counter > 0 then " +
  33.         "   redis.call('expire', KEYS[1], ARGV[2]) " +
  34.         "   return 1 " +
  35.         "else " +
  36.         "   redis.call('del', KEYS[1]) " +
  37.         "   return 1 " +
  38.         "end";
  39.    
  40.     DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
  41.     Long result = redisTemplate.execute(redisScript,
  42.                                        Collections.singletonList(lockKey),
  43.                                        requestId,
  44.                                        String.valueOf(30)); // 30秒过期时间
  45.    
  46.     return result != null && result == 1;
  47. }
复制代码

4.4 等待锁的策略

当获取锁失败时,客户端可以采取以下策略:

1. 立即失败:获取锁失败后立即返回,不进行等待。
2. 阻塞等待:在一段时间内不断尝试获取锁,直到成功或超时。
3. 异步回调:获取锁失败后注册一个回调,当锁可用时自动调用回调函数。
  1. // 带等待时间的锁获取
  2. public boolean lockWithWait(String lockKey, String requestId, long expireTime, long waitTime) throws InterruptedException {
  3.     long startTime = System.currentTimeMillis();
  4.     long remainingTime = waitTime;
  5.    
  6.     while (true) {
  7.         // 尝试获取锁
  8.         if (acquireLock(lockKey, requestId, expireTime)) {
  9.             return true;
  10.         }
  11.         
  12.         // 计算剩余等待时间
  13.         remainingTime = waitTime - (System.currentTimeMillis() - startTime);
  14.         if (remainingTime <= 0) {
  15.             return false;
  16.         }
  17.         
  18.         // 短暂休眠后重试
  19.         Thread.sleep(Math.min(100, remainingTime));
  20.     }
  21. }
复制代码

5. Redis锁释放全流程

5.1 释放锁的基本流程

释放Redis锁的基本流程如下:

1. 客户端检查自己是否是锁的持有者。
2. 如果是锁的持有者,则删除锁的键值对。
3. 如果不是锁的持有者,则不执行任何操作。
  1. // 释放锁
  2. public boolean releaseLock(String lockKey, String requestId) {
  3.     // 使用Lua脚本确保原子性
  4.     String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  5.     DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
  6.     Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId);
  7.     return result != null && result == 1;
  8. }
复制代码

5.2 确保锁的安全性(避免释放他人的锁)

为了避免释放他人的锁,我们需要在释放锁时验证锁的持有者。这可以通过以下方式实现:

1. 在获取锁时,设置一个唯一的值(如UUID)作为锁的值。
2. 在释放锁时,先检查锁的值是否与自己的唯一值匹配,只有匹配时才释放锁。
  1. // 生成唯一请求ID
  2. private String generateRequestId() {
  3.     return UUID.randomUUID().toString();
  4. }
  5. // 安全释放锁
  6. public boolean safeReleaseLock(String lockKey, String requestId) {
  7.     // 使用Lua脚本确保原子性
  8.     String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  9.     DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
  10.     Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId);
  11.     return result != null && result == 1;
  12. }
复制代码

5.3 锁续期机制

在业务逻辑执行时间可能超过锁的过期时间的情况下,我们需要实现锁的续期机制,确保业务逻辑执行完毕前锁不会过期。

1. 在获取锁后启动一个后台线程。
2. 后台线程定期检查锁是否仍然存在,以及是否需要续期。
3. 如果需要续期,则延长锁的过期时间。
  1. public class LockRenewalTask {
  2.     private final RedisTemplate<String, String> redisTemplate;
  3.     private final String lockKey;
  4.     private final String requestId;
  5.     private final long expireTime;
  6.     private final long renewalInterval;
  7.     private ScheduledExecutorService scheduler;
  8.     private ScheduledFuture<?> renewalTask;
  9.    
  10.     public LockRenewalTask(RedisTemplate<String, String> redisTemplate, String lockKey, String requestId, long expireTime, long renewalInterval) {
  11.         this.redisTemplate = redisTemplate;
  12.         this.lockKey = lockKey;
  13.         this.requestId = requestId;
  14.         this.expireTime = expireTime;
  15.         this.renewalInterval = renewalInterval;
  16.         this.scheduler = Executors.newSingleThreadScheduledExecutor();
  17.     }
  18.    
  19.     public void start() {
  20.         // 启动续期任务
  21.         renewalTask = scheduler.scheduleAtFixedRate(() -> {
  22.             // 检查并续期
  23.             String luaScript =
  24.                 "if redis.call('get', KEYS[1]) == ARGV[1] then " +
  25.                 "   redis.call('expire', KEYS[1], ARGV[2]) " +
  26.                 "   return 1 " +
  27.                 "else " +
  28.                 "   return 0 " +
  29.                 "end";
  30.             
  31.             DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
  32.             Long result = redisTemplate.execute(redisScript,
  33.                                               Collections.singletonList(lockKey),
  34.                                               requestId,
  35.                                               String.valueOf(expireTime / 1000));
  36.             
  37.             // 如果续期失败(锁不存在或不属于当前客户端),停止续期任务
  38.             if (result == null || result != 1) {
  39.                 stop();
  40.             }
  41.         }, renewalInterval / 2, renewalInterval, TimeUnit.MILLISECONDS);
  42.     }
  43.    
  44.     public void stop() {
  45.         if (renewalTask != null) {
  46.             renewalTask.cancel(true);
  47.             scheduler.shutdown();
  48.         }
  49.     }
  50. }
  51. // 使用锁续期
  52. public boolean lockWithRenewal(String lockKey, long expireTime, long waitTime, long businessLogicTimeout) {
  53.     String requestId = generateRequestId();
  54.     LockRenewalTask renewalTask = null;
  55.    
  56.     try {
  57.         // 尝试获取锁
  58.         if (!lockWithWait(lockKey, requestId, expireTime, waitTime)) {
  59.             return false;
  60.         }
  61.         
  62.         // 启动锁续期任务
  63.         renewalTask = new LockRenewalTask(redisTemplate, lockKey, requestId, expireTime, expireTime / 3);
  64.         renewalTask.start();
  65.         
  66.         // 执行业务逻辑,设置超时
  67.         ExecutorService executor = Executors.newSingleThreadExecutor();
  68.         Future<?> future = executor.submit(() -> {
  69.             doBusiness();
  70.             return null;
  71.         });
  72.         
  73.         try {
  74.             future.get(businessLogicTimeout, TimeUnit.MILLISECONDS);
  75.             return true;
  76.         } catch (TimeoutException e) {
  77.             future.cancel(true);
  78.             return false;
  79.         } finally {
  80.             executor.shutdown();
  81.         }
  82.     } catch (InterruptedException e) {
  83.         Thread.currentThread().interrupt();
  84.         return false;
  85.     } finally {
  86.         // 停止锁续期任务
  87.         if (renewalTask != null) {
  88.             renewalTask.stop();
  89.         }
  90.         
  91.         // 释放锁
  92.         safeReleaseLock(lockKey, requestId);
  93.     }
  94. }
复制代码

6. 避免死锁的策略

6.1 设置合理的过期时间

设置合理的过期时间是避免死锁的基本策略。过期时间应该根据业务逻辑的执行时间来设定,一般应该大于业务逻辑的正常执行时间,但也不宜过长,以免影响系统的可用性。

1. 业务逻辑的执行时间:通过测试和监控,了解业务逻辑的正常执行时间。
2. 系统负载:在高负载情况下,业务逻辑的执行时间可能会延长。
3. 网络延迟:考虑网络延迟对业务逻辑执行时间的影响。
4. 锁的重要性:对于关键业务,可以适当延长过期时间。
  1. // 动态计算过期时间
  2. public long calculateExpireTime(long normalExecutionTime, double safetyFactor) {
  3.     // 安全系数,通常在1.5到3之间
  4.     return (long) (normalExecutionTime * safetyFactor);
  5. }
  6. // 使用动态过期时间获取锁
  7. public boolean lockWithDynamicExpire(String lockKey, String requestId, long normalExecutionTime) {
  8.     double safetyFactor = 2.0; // 安全系数为2
  9.     long expireTime = calculateExpireTime(normalExecutionTime, safetyFactor);
  10.     return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
  11. }
复制代码

6.2 实现锁续期

锁续期是避免死锁的重要策略,特别是在业务逻辑执行时间不确定或可能超过锁的初始过期时间的情况下。

1. 后台线程续期:在获取锁后启动一个后台线程,定期为锁续期。
2. 定时任务续期:使用定时任务定期检查并续期。
3. 业务逻辑中续期:在业务逻辑执行过程中,定期检查并续期。
  1. // 使用Redisson的自动续期功能
  2. public void businessWithAutoRenewal() {
  3.     Config config = new Config();
  4.     config.useSingleServer().setAddress("redis://127.0.0.1:6379");
  5.     RedissonClient redisson = Redisson.create(config);
  6.    
  7.     RLock lock = redisson.getLock("myLock");
  8.    
  9.     try {
  10.         // 获取锁,并设置锁自动释放时间为30秒
  11.         lock.lock(30, TimeUnit.SECONDS);
  12.         
  13.         // 执行业务逻辑,Redisson会自动为锁续期
  14.         doBusiness();
  15.     } finally {
  16.         // 释放锁
  17.         lock.unlock();
  18.         redisson.shutdown();
  19.     }
  20. }
复制代码

6.3 死锁检测与恢复

尽管我们可以通过设置过期时间和锁续期来减少死锁的可能性,但在某些情况下,死锁仍然可能发生。因此,我们需要实现死锁检测与恢复机制。

1. 超时检测:为锁设置一个超时时间,如果在超时时间内锁未被释放,则认为可能发生死锁。
2. 等待图检测:维护一个锁等待图,定期检查是否存在环路,如果存在环路,则认为发生死锁。
3. 资源监控:监控资源的使用情况,如果某个资源长时间被占用,则认为可能发生死锁。

1. 强制释放锁:当检测到死锁时,强制释放某些锁,打破死锁状态。
2. 回滚操作:回滚导致死锁的操作,恢复系统到一致状态。
3. 重启服务:在极端情况下,重启相关服务,清除所有锁状态。
  1. // 死锁检测与恢复
  2. public class DeadlockDetector {
  3.     private final RedisTemplate<String, String> redisTemplate;
  4.     private final long deadlockThreshold; // 死锁阈值,单位毫秒
  5.     private ScheduledExecutorService scheduler;
  6.    
  7.     public DeadlockDetector(RedisTemplate<String, String> redisTemplate, long deadlockThreshold) {
  8.         this.redisTemplate = redisTemplate;
  9.         this.deadlockThreshold = deadlockThreshold;
  10.         this.scheduler = Executors.newSingleThreadScheduledExecutor();
  11.     }
  12.    
  13.     public void start() {
  14.         // 定期检测死锁
  15.         scheduler.scheduleAtFixedRate(this::detectAndRecoverDeadlocks,
  16.                                     deadlockThreshold,
  17.                                     deadlockThreshold,
  18.                                     TimeUnit.MILLISECONDS);
  19.     }
  20.    
  21.     private void detectAndRecoverDeadlocks() {
  22.         // 获取所有锁的键
  23.         Set<String> lockKeys = redisTemplate.keys("*lock*");
  24.         
  25.         for (String lockKey : lockKeys) {
  26.             // 获取锁的剩余过期时间
  27.             Long ttl = redisTemplate.getExpire(lockKey, TimeUnit.MILLISECONDS);
  28.             
  29.             // 如果锁没有设置过期时间或过期时间过长,可能发生死锁
  30.             if (ttl == null || ttl > deadlockThreshold * 2) {
  31.                 // 记录死锁日志
  32.                 log.warn("Potential deadlock detected for lock: {}", lockKey);
  33.                
  34.                 // 获取锁的值(持有者标识)
  35.                 String holder = redisTemplate.opsForValue().get(lockKey);
  36.                
  37.                 // 尝试恢复死锁
  38.                 recoverFromDeadlock(lockKey, holder);
  39.             }
  40.         }
  41.     }
  42.    
  43.     private void recoverFromDeadlock(String lockKey, String holder) {
  44.         // 根据持有者标识,尝试通知持有者释放锁
  45.         notifyLockHolder(holder, lockKey);
  46.         
  47.         // 如果通知失败,强制释放锁
  48.         forceReleaseLock(lockKey, holder);
  49.     }
  50.    
  51.     private void notifyLockHolder(String holder, String lockKey) {
  52.         // 实现通知持有者的逻辑,例如通过消息队列或RPC调用
  53.         // 这里只是一个示例,实际实现可能更复杂
  54.         log.info("Notifying lock holder {} to release lock {}", holder, lockKey);
  55.     }
  56.    
  57.     private void forceReleaseLock(String lockKey, String holder) {
  58.         // 强制释放锁
  59.         redisTemplate.delete(lockKey);
  60.         log.warn("Force released lock {} held by {}", lockKey, holder);
  61.     }
  62.    
  63.     public void stop() {
  64.         scheduler.shutdown();
  65.     }
  66. }
复制代码

7. Redis锁的最佳实践

7.1 锁的粒度控制

锁的粒度是指锁定的资源范围。锁的粒度越细,并发性越高,但实现起来也越复杂;锁的粒度越粗,并发性越低,但实现起来简单。

1. 根据业务需求选择:根据业务逻辑的特点,选择合适的锁粒度。
2. 考虑并发性能:在高并发场景下,尽量选择细粒度的锁。
3. 权衡实现复杂度:在满足性能需求的前提下,尽量选择简单的实现方式。
  1. // 粗粒度锁:锁定整个资源
  2. public boolean coarseGrainedLock(String resourceId, String requestId, long expireTime) {
  3.     String lockKey = "resource:lock:" + resourceId;
  4.     return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
  5. }
  6. // 细粒度锁:锁定资源的特定部分
  7. public boolean fineGrainedLock(String resourceId, String partId, String requestId, long expireTime) {
  8.     String lockKey = "resource:lock:" + resourceId + ":" + partId;
  9.     return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
  10. }
  11. // 使用细粒度锁的业务逻辑
  12. public void updateResourcePart(String resourceId, String partId, Object newValue) {
  13.     String requestId = generateRequestId();
  14.    
  15.     try {
  16.         // 获取细粒度锁
  17.         if (!fineGrainedLock(resourceId, partId, requestId, 30000)) {
  18.             throw new RuntimeException("Failed to acquire lock");
  19.         }
  20.         
  21.         // 执行业务逻辑
  22.         doUpdateResourcePart(resourceId, partId, newValue);
  23.     } finally {
  24.         // 释放细粒度锁
  25.         safeReleaseLock("resource:lock:" + resourceId + ":" + partId, requestId);
  26.     }
  27. }
复制代码

7.2 性能优化

在高并发场景下,Redis锁的性能可能成为系统的瓶颈。以下是一些优化Redis锁性能的策略:

1. 减少锁的持有时间:尽量在获取锁后快速执行关键操作,然后立即释放锁。
2. 使用本地缓存:对于频繁访问但不经常修改的数据,可以使用本地缓存减少对Redis的访问。
3. 批量操作:对于需要获取多个锁的操作,可以使用批量操作减少网络开销。
4. 使用连接池:使用Redis连接池减少连接创建和销毁的开销。
  1. // 减少锁的持有时间
  2. public void updateWithMinimalLock(String resourceId, Object newValue) {
  3.     String requestId = generateRequestId();
  4.     String lockKey = "resource:lock:" + resourceId;
  5.    
  6.     try {
  7.         // 获取锁
  8.         if (!acquireLock(lockKey, requestId, 30000)) {
  9.             throw new RuntimeException("Failed to acquire lock");
  10.         }
  11.         
  12.         // 尽快执行关键操作
  13.         Object oldValue = getResource(resourceId);
  14.         Object mergedValue = mergeValues(oldValue, newValue);
  15.         
  16.         // 尽快释放锁
  17.         safeReleaseLock(lockKey, requestId);
  18.         
  19.         // 在锁外执行耗时操作
  20.         doTimeConsumingUpdate(mergedValue);
  21.     } catch (Exception e) {
  22.         // 确保锁被释放
  23.         safeReleaseLock(lockKey, requestId);
  24.         throw e;
  25.     }
  26. }
  27. // 使用本地缓存减少Redis访问
  28. public class ResourceCache {
  29.     private final RedisTemplate<String, Object> redisTemplate;
  30.     private final Cache<String, Object> localCache;
  31.    
  32.     public ResourceCache(RedisTemplate<String, Object> redisTemplate) {
  33.         this.redisTemplate = redisTemplate;
  34.         this.localCache = Caffeine.newBuilder()
  35.             .expireAfterWrite(10, TimeUnit.SECONDS)
  36.             .maximumSize(1000)
  37.             .build();
  38.     }
  39.    
  40.     public Object getResource(String resourceId) {
  41.         // 首先尝试从本地缓存获取
  42.         Object value = localCache.getIfPresent(resourceId);
  43.         if (value != null) {
  44.             return value;
  45.         }
  46.         
  47.         // 本地缓存中没有,从Redis获取
  48.         value = redisTemplate.opsForValue().get(resourceId);
  49.         if (value != null) {
  50.             localCache.put(resourceId, value);
  51.         }
  52.         
  53.         return value;
  54.     }
  55.    
  56.     public void updateResource(String resourceId, Object newValue, String requestId) {
  57.         String lockKey = "resource:lock:" + resourceId;
  58.         
  59.         try {
  60.             // 获取锁
  61.             if (!acquireLock(lockKey, requestId, 30000)) {
  62.                 throw new RuntimeException("Failed to acquire lock");
  63.             }
  64.             
  65.             // 更新Redis中的值
  66.             redisTemplate.opsForValue().set(resourceId, newValue);
  67.             
  68.             // 更新本地缓存
  69.             localCache.put(resourceId, newValue);
  70.         } finally {
  71.             // 释放锁
  72.             safeReleaseLock(lockKey, requestId);
  73.         }
  74.     }
  75. }
复制代码

7.3 异常处理

在使用Redis锁时,需要注意各种异常情况,并进行适当的处理,以确保系统的稳定性和可靠性。

1. Redis连接异常:当Redis连接失败时,应该有备用方案,如降级处理或重试。
2. 获取锁失败:当获取锁失败时,应该有重试机制或降级策略。
3. 锁释放失败:当锁释放失败时,应该有监控和报警机制,及时发现和处理。
4. 业务逻辑异常:当业务逻辑执行失败时,应该确保锁被正确释放,避免死锁。
  1. // 带异常处理的锁操作
  2. public <T> T executeWithLock(String lockKey, long expireTime, long waitTime, Supplier<T> businessLogic) {
  3.     String requestId = generateRequestId();
  4.    
  5.     try {
  6.         // 尝试获取锁,带有重试机制
  7.         boolean locked = false;
  8.         long startTime = System.currentTimeMillis();
  9.         long remainingTime = waitTime;
  10.         
  11.         while (!locked && remainingTime > 0) {
  12.             try {
  13.                 locked = acquireLock(lockKey, requestId, expireTime);
  14.             } catch (RedisConnectionFailureException e) {
  15.                 // Redis连接异常,记录日志并尝试重新连接
  16.                 log.error("Redis connection failed while acquiring lock", e);
  17.                 try {
  18.                     TimeUnit.MILLISECONDS.sleep(100);
  19.                 } catch (InterruptedException ie) {
  20.                     Thread.currentThread().interrupt();
  21.                     throw new RuntimeException("Interrupted while waiting for Redis connection", ie);
  22.                 }
  23.             }
  24.             
  25.             if (!locked) {
  26.                 try {
  27.                     // 短暂休眠后重试
  28.                     TimeUnit.MILLISECONDS.sleep(Math.min(100, remainingTime));
  29.                 } catch (InterruptedException e) {
  30.                     Thread.currentThread().interrupt();
  31.                     throw new RuntimeException("Interrupted while waiting for lock", e);
  32.                 }
  33.                
  34.                 remainingTime = waitTime - (System.currentTimeMillis() - startTime);
  35.             }
  36.         }
  37.         
  38.         if (!locked) {
  39.             // 获取锁失败,执行降级逻辑
  40.             log.warn("Failed to acquire lock {} after {} ms", lockKey, waitTime);
  41.             return executeFallback(businessLogic);
  42.         }
  43.         
  44.         // 成功获取锁,执行业务逻辑
  45.         try {
  46.             return businessLogic.get();
  47.         } catch (Exception e) {
  48.             // 业务逻辑执行失败,记录日志
  49.             log.error("Business logic execution failed", e);
  50.             throw e;
  51.         }
  52.     } finally {
  53.         // 确保锁被释放
  54.         try {
  55.             safeReleaseLock(lockKey, requestId);
  56.         } catch (Exception e) {
  57.             // 锁释放失败,记录日志
  58.             log.error("Failed to release lock {}", lockKey, e);
  59.         }
  60.     }
  61. }
  62. // 降级逻辑
  63. private <T> T executeFallback(Supplier<T> businessLogic) {
  64.     // 实现降级逻辑,例如返回默认值、从缓存读取数据等
  65.     log.info("Executing fallback logic");
  66.     try {
  67.         return businessLogic.get();
  68.     } catch (Exception e) {
  69.         log.error("Fallback logic execution failed", e);
  70.         throw new RuntimeException("Both primary and fallback logic failed", e);
  71.     }
  72. }
复制代码

8. 实际应用场景与案例分析

8.1 库存扣减场景

在电商系统中,库存扣减是一个典型的需要使用分布式锁的场景。多个用户可能同时购买同一商品,如果不使用锁,可能会导致超卖。

1. 并发问题:多个请求同时读取库存,发现库存充足,然后同时扣减库存,导致超卖。
2. 数据一致性:库存数据需要保持一致性,不能出现负数。
3. 性能要求:在高并发场景下,需要保证系统的响应速度。

使用Redis锁来控制库存扣减的并发访问,确保同一时间只有一个请求能够扣减库存。
  1. // 库存服务
  2. public class InventoryService {
  3.     private final RedisTemplate<String, String> redisTemplate;
  4.    
  5.     public InventoryService(RedisTemplate<String, String> redisTemplate) {
  6.         this.redisTemplate = redisTemplate;
  7.     }
  8.    
  9.     // 扣减库存
  10.     public boolean deductInventory(String productId, int quantity) {
  11.         String lockKey = "inventory:lock:" + productId;
  12.         String requestId = UUID.randomUUID().toString();
  13.         
  14.         try {
  15.             // 获取锁,最多等待5秒,锁自动释放时间为10秒
  16.             boolean locked = lockWithWait(lockKey, requestId, 10000, 5000);
  17.             if (!locked) {
  18.                 log.warn("Failed to acquire lock for product {}", productId);
  19.                 return false;
  20.             }
  21.             
  22.             // 获取当前库存
  23.             String inventoryKey = "inventory:" + productId;
  24.             String inventoryStr = redisTemplate.opsForValue().get(inventoryKey);
  25.             if (inventoryStr == null) {
  26.                 log.error("Inventory not found for product {}", productId);
  27.                 return false;
  28.             }
  29.             
  30.             int inventory = Integer.parseInt(inventoryStr);
  31.             if (inventory < quantity) {
  32.                 log.warn("Insufficient inventory for product {}. Required: {}, Available: {}",
  33.                          productId, quantity, inventory);
  34.                 return false;
  35.             }
  36.             
  37.             // 扣减库存
  38.             int newInventory = inventory - quantity;
  39.             redisTemplate.opsForValue().set(inventoryKey, String.valueOf(newInventory));
  40.             
  41.             // 记录库存变更日志
  42.             logInventoryChange(productId, inventory, newInventory);
  43.             
  44.             return true;
  45.         } finally {
  46.             // 释放锁
  47.             safeReleaseLock(lockKey, requestId);
  48.         }
  49.     }
  50.    
  51.     private void logInventoryChange(String productId, int oldInventory, int newInventory) {
  52.         // 实现库存变更日志记录逻辑
  53.         log.info("Inventory changed for product {}. Old: {}, New: {}",
  54.                  productId, oldInventory, newInventory);
  55.     }
  56. }
复制代码

8.2 定时任务场景

在分布式系统中,定时任务可能会在多个节点上同时执行,导致重复处理。使用Redis锁可以确保同一时间只有一个节点执行定时任务。

1. 重复执行:多个节点同时执行同一个定时任务,可能导致数据重复处理或资源浪费。
2. 任务协调:需要一种机制来协调多个节点上的任务执行。
3. 故障恢复:当执行任务的节点故障时,需要有机制让其他节点接管任务。

使用Redis锁来控制定时任务的执行,确保同一时间只有一个节点能够执行任务。
  1. // 定时任务执行器
  2. public class ScheduledTaskExecutor {
  3.     private final RedisTemplate<String, String> redisTemplate;
  4.     private final String taskId;
  5.     private final Runnable task;
  6.     private final long lockExpireTime;
  7.     private final long taskInterval;
  8.    
  9.     public ScheduledTaskExecutor(RedisTemplate<String, String> redisTemplate,
  10.                                String taskId,
  11.                                Runnable task,
  12.                                long lockExpireTime,
  13.                                long taskInterval) {
  14.         this.redisTemplate = redisTemplate;
  15.         this.taskId = taskId;
  16.         this.task = task;
  17.         this.lockExpireTime = lockExpireTime;
  18.         this.taskInterval = taskInterval;
  19.     }
  20.    
  21.     public void start() {
  22.         ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
  23.         scheduler.scheduleAtFixedRate(this::executeWithLock,
  24.                                     0,
  25.                                     taskInterval,
  26.                                     TimeUnit.MILLISECONDS);
  27.     }
  28.    
  29.     private void executeWithLock() {
  30.         String lockKey = "task:lock:" + taskId;
  31.         String requestId = UUID.randomUUID().toString();
  32.         
  33.         try {
  34.             // 尝试获取锁
  35.             boolean locked = acquireLock(lockKey, requestId, lockExpireTime);
  36.             if (!locked) {
  37.                 log.debug("Failed to acquire lock for task {}", taskId);
  38.                 return;
  39.             }
  40.             
  41.             log.info("Acquired lock for task {}, starting execution", taskId);
  42.             
  43.             // 执行任务
  44.             long startTime = System.currentTimeMillis();
  45.             try {
  46.                 task.run();
  47.                 long executionTime = System.currentTimeMillis() - startTime;
  48.                 log.info("Task {} completed in {} ms", taskId, executionTime);
  49.             } catch (Exception e) {
  50.                 log.error("Task {} execution failed", taskId, e);
  51.             }
  52.         } finally {
  53.             // 释放锁
  54.             safeReleaseLock(lockKey, requestId);
  55.         }
  56.     }
  57. }
  58. // 使用定时任务执行器
  59. public class Application {
  60.     public static void main(String[] args) {
  61.         RedisTemplate<String, String> redisTemplate = createRedisTemplate();
  62.         
  63.         // 创建定时任务
  64.         Runnable task = () -> {
  65.             // 执行定时任务逻辑
  66.             System.out.println("Executing scheduled task at " + new Date());
  67.             doScheduledWork();
  68.         };
  69.         
  70.         // 创建定时任务执行器
  71.         ScheduledTaskExecutor executor = new ScheduledTaskExecutor(
  72.             redisTemplate,
  73.             "sampleTask",
  74.             task,
  75.             30000,   // 锁过期时间30秒
  76.             60000    // 任务执行间隔60秒
  77.         );
  78.         
  79.         // 启动定时任务
  80.         executor.start();
  81.     }
  82.    
  83.     private static void doScheduledWork() {
  84.         // 实现定时任务的具体逻辑
  85.     }
  86.    
  87.     private static RedisTemplate<String, String> createRedisTemplate() {
  88.         // 创建并配置RedisTemplate
  89.         // ...
  90.         return new RedisTemplate<>();
  91.     }
  92. }
复制代码

8.3 分布式事务场景

在分布式系统中,跨多个服务或数据源的操作需要保证事务的一致性。Redis锁可以用于实现分布式事务,确保多个操作的原子性。

1. 跨服务操作:一个业务操作可能涉及多个服务,需要保证所有服务要么都成功,要么都失败。
2. 数据一致性:需要保证跨多个数据源的数据一致性。
3. 并发控制:需要控制对共享资源的并发访问,避免数据冲突。

使用Redis锁来实现分布式事务,确保多个操作的原子性。
  1. // 分布式事务管理器
  2. public class DistributedTransactionManager {
  3.     private final RedisTemplate<String, String> redisTemplate;
  4.     private final List<TransactionParticipant> participants;
  5.    
  6.     public DistributedTransactionManager(RedisTemplate<String, String> redisTemplate,
  7.                                        List<TransactionParticipant> participants) {
  8.         this.redisTemplate = redisTemplate;
  9.         this.participants = participants;
  10.     }
  11.    
  12.     // 执行分布式事务
  13.     public boolean executeTransaction(String transactionId) {
  14.         String lockKey = "transaction:lock:" + transactionId;
  15.         String requestId = UUID.randomUUID().toString();
  16.         
  17.         try {
  18.             // 获取全局事务锁
  19.             boolean locked = lockWithWait(lockKey, requestId, 30000, 5000);
  20.             if (!locked) {
  21.                 log.error("Failed to acquire transaction lock for {}", transactionId);
  22.                 return false;
  23.             }
  24.             
  25.             log.info("Acquired transaction lock for {}", transactionId);
  26.             
  27.             // 准备阶段:预执行所有参与者的操作
  28.             List<TransactionParticipant> preparedParticipants = new ArrayList<>();
  29.             boolean prepareSuccess = true;
  30.             
  31.             for (TransactionParticipant participant : participants) {
  32.                 try {
  33.                     if (participant.prepare(transactionId)) {
  34.                         preparedParticipants.add(participant);
  35.                     } else {
  36.                         log.error("Participant {} prepare failed for transaction {}",
  37.                                  participant.getId(), transactionId);
  38.                         prepareSuccess = false;
  39.                         break;
  40.                     }
  41.                 } catch (Exception e) {
  42.                     log.error("Participant {} prepare exception for transaction {}",
  43.                              participant.getId(), transactionId, e);
  44.                     prepareSuccess = false;
  45.                     break;
  46.                 }
  47.             }
  48.             
  49.             // 如果准备阶段失败,回滚所有已准备的参与者
  50.             if (!prepareSuccess) {
  51.                 log.warn("Transaction {} prepare failed, rolling back", transactionId);
  52.                 for (TransactionParticipant participant : preparedParticipants) {
  53.                     try {
  54.                         participant.rollback(transactionId);
  55.                     } catch (Exception e) {
  56.                         log.error("Participant {} rollback exception for transaction {}",
  57.                                  participant.getId(), transactionId, e);
  58.                     }
  59.                 }
  60.                 return false;
  61.             }
  62.             
  63.             // 提交阶段:提交所有参与者的操作
  64.             boolean commitSuccess = true;
  65.             for (TransactionParticipant participant : preparedParticipants) {
  66.                 try {
  67.                     if (!participant.commit(transactionId)) {
  68.                         log.error("Participant {} commit failed for transaction {}",
  69.                                  participant.getId(), transactionId);
  70.                         commitSuccess = false;
  71.                     }
  72.                 } catch (Exception e) {
  73.                     log.error("Participant {} commit exception for transaction {}",
  74.                              participant.getId(), transactionId, e);
  75.                     commitSuccess = false;
  76.                 }
  77.             }
  78.             
  79.             // 如果提交阶段失败,记录日志,可能需要人工干预
  80.             if (!commitSuccess) {
  81.                 log.error("Transaction {} commit failed, manual intervention may be required",
  82.                          transactionId);
  83.                 return false;
  84.             }
  85.             
  86.             log.info("Transaction {} completed successfully", transactionId);
  87.             return true;
  88.         } finally {
  89.             // 释放全局事务锁
  90.             safeReleaseLock(lockKey, requestId);
  91.         }
  92.     }
  93. }
  94. // 事务参与者接口
  95. public interface TransactionParticipant {
  96.     String getId();
  97.    
  98.     // 预执行阶段
  99.     boolean prepare(String transactionId);
  100.    
  101.     // 提交阶段
  102.     boolean commit(String transactionId);
  103.    
  104.     // 回滚阶段
  105.     boolean rollback(String transactionId);
  106. }
  107. // 示例:订单服务作为事务参与者
  108. public class OrderService implements TransactionParticipant {
  109.     private final OrderRepository orderRepository;
  110.    
  111.     public OrderService(OrderRepository orderRepository) {
  112.         this.orderRepository = orderRepository;
  113.     }
  114.    
  115.     @Override
  116.     public String getId() {
  117.         return "orderService";
  118.     }
  119.    
  120.     @Override
  121.     public boolean prepare(String transactionId) {
  122.         // 预创建订单,状态为PREPARED
  123.         Order order = createOrder(transactionId);
  124.         order.setStatus(OrderStatus.PREPARED);
  125.         return orderRepository.save(order);
  126.     }
  127.    
  128.     @Override
  129.     public boolean commit(String transactionId) {
  130.         // 更新订单状态为COMMITTED
  131.         Order order = orderRepository.findByTransactionId(transactionId);
  132.         if (order != null) {
  133.             order.setStatus(OrderStatus.COMMITTED);
  134.             return orderRepository.save(order);
  135.         }
  136.         return false;
  137.     }
  138.    
  139.     @Override
  140.     public boolean rollback(String transactionId) {
  141.         // 更新订单状态为ROLLED_BACK
  142.         Order order = orderRepository.findByTransactionId(transactionId);
  143.         if (order != null) {
  144.             order.setStatus(OrderStatus.ROLLED_BACK);
  145.             return orderRepository.save(order);
  146.         }
  147.         return false;
  148.     }
  149.    
  150.     private Order createOrder(String transactionId) {
  151.         // 创建订单的逻辑
  152.         return new Order();
  153.     }
  154. }
复制代码

9. 总结与展望

9.1 总结

本文详细解析了Redis锁从获取到释放的全流程,介绍了分布式并发控制的关键技术,包括:

1. Redis锁的实现方式:包括基于SETNX的实现、基于RedLock算法的实现和基于Redisson的实现。
2. Redis锁获取全流程:包括获取锁的基本流程、锁的超时设置、锁的重入机制和等待锁的策略。
3. Redis锁释放全流程:包括释放锁的基本流程、确保锁的安全性和锁续期机制。
4. 避免死锁的策略:包括设置合理的过期时间、实现锁续期和死锁检测与恢复。
5. Redis锁的最佳实践:包括锁的粒度控制、性能优化和异常处理。
6. 实际应用场景与案例分析:包括库存扣减场景、定时任务场景和分布式事务场景。

通过掌握这些技术和策略,我们可以有效地使用Redis锁来控制分布式系统中的并发访问,避免死锁问题,提高系统的稳定性和可靠性。

9.2 展望

随着分布式系统的不断发展,Redis锁技术也在不断演进。未来,我们可以期待以下发展方向:

1. 更高效的锁算法:开发更高效的锁算法,减少锁的获取和释放开销。
2. 自适应锁策略:根据系统负载和业务特点,自动调整锁的策略和参数。
3. 更完善的死锁检测与恢复机制:开发更智能的死锁检测与恢复机制,提高系统的自愈能力。
4. 更好的可观测性:提供更全面的锁监控和诊断工具,帮助开发者快速定位和解决问题。
5. 与其他分布式技术的集成:与分布式事务、分布式缓存等技术更好地集成,提供更完整的分布式解决方案。

总之,Redis锁作为分布式系统中的重要组件,将在未来的分布式系统中继续发挥重要作用,为系统的稳定性和可靠性提供有力保障。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

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

本版积分规则