找回密码
 立即注册
首页 业界区 业界 学习高可靠Redis分布式锁实现思路

学习高可靠Redis分布式锁实现思路

孟茹云 2025-6-4 20:59:13
一、分布式锁的必要性

在单体应用时代,我们使用ReentrantLock或synchronized就能解决线程安全问题。但当系统拆分为分布式架构后(目前大多数公司应该不会只是单体应用了),跨进程的共享资源竞争就成了必须要解决的问题。
分布式锁由此应运而生,但是必须解决三大核心问题:

  • 竞态条件:多人操作共享资源,顺序不可控
  • 锁失效:锁自动过期但业务未执行完,其他客户端抢占资源 / 加锁成功但未设置过期时间,服务宕机导致死锁
  • 锁误删:客户端A释放了客户端B持有的锁。
二、核心实现解析(附源码)

2.1 原子性加锁
  1. local lockKey = KEYS[1]              -- 锁的键名,如"order_lock_123"
  2. local lockSecret = ARGV[1]           -- 锁的唯一标识(建议UUID)
  3. local expireTime = tonumber(ARGV[2]) -- 过期时间(单位:秒)
  4. -- 参数有效性校验
  5. if not expireTime or expireTime <= 0 then
  6.     return "0" -- 参数非法直接返回失败
  7. end
  8. -- 原子操作:SET lockKey lockSecret NX EX expireTime
  9. local result = redis.call("set", lockKey, lockSecret, "NX", "EX", expireTime)
  10. return result and "1" or "0" -- 成功返回"1",失败返回"0"
复制代码
设计思路:

  • 续期间隔=过期时间/3(如30s过期则10s续期)
  • 异步线程池需单独配置
  • 双重校验锁状态(内存标记+Redis实际值)
2.3 安全释放锁
  1. local lockKey = KEYS[1]              -- 锁的键名
  2. local lockSecret = ARGV[1]           -- 锁标识
  3. local expireTime = tonumber(ARGV[2]) -- 新的过期时间
  4. -- 参数校验
  5. if not expireTime or expireTime <= 0 then
  6.     return "0"
  7. end
  8. -- 获取当前锁的值
  9. local storedSecret = redis.call("get", lockKey)
  10. -- 续期逻辑
  11. if storedSecret == lockSecret then
  12.     -- 值匹配则延长过期时间
  13.     local result = redis.call("expire", lockKey, expireTime)
  14.     return result == 1 and "1" or "0" -- 续期成功返回"1"
  15. else
  16.     -- 锁不存在或值不匹配
  17.     return "0"
  18. end
复制代码
设计思路:

  • 校验value避免误删其他线程的锁
三、源码
  1. // 定时续约线程
  2. watchdogExecutor.scheduleAtFixedRate(() -> {
  3.     locks.entrySet().removeIf(entry -> entry.getValue().isCancelled());
  4.     for (Entry<String, Lock> entry : locks.entrySet()) {
  5.         if (!entry.getValue().isCancelled()) {
  6.             String result = redisTemplate.execute(RENEWAL_SCRIPT,
  7.                 Collections.singletonList(key),
  8.                 lock.value, "30");
  9.             if ("0".equals(result)) lock.cancel();
  10.         }
  11.     }
  12. }, 0, 10, TimeUnit.SECONDS);
复制代码
四、如何使用

4.1 配置类
  1. local lockKey = KEYS[1]       -- 锁的键名
  2. local lockSecret = ARGV[1]    -- 要释放的锁标识
  3. -- 获取当前锁的值
  4. local storedSecret = redis.call("get", lockKey)
  5. -- 校验锁归属
  6. if storedSecret == lockSecret then
  7.     -- 值匹配则删除Key
  8.     return redis.call("del", lockKey) == 1 and "1" or "0"
  9. else
  10.     -- 值不匹配
  11.     return "0"
  12. end
复制代码
4.2 使用
  1. package org.example.tao.util;
  2. import com.alibaba.fastjson2.JSON;
  3. import org.springframework.data.redis.core.RedisTemplate;
  4. import org.springframework.data.redis.core.script.RedisScript;
  5. import javax.annotation.PreDestroy;
  6. import java.util.Collections;
  7. import java.util.Map;
  8. import java.util.Objects;
  9. import java.util.concurrent.ConcurrentHashMap;
  10. import java.util.concurrent.Executors;
  11. import java.util.concurrent.ScheduledExecutorService;
  12. import java.util.concurrent.TimeUnit;
  13. public class RedisUtils {
  14.     static class Lock {
  15.         private final String value;
  16.         private volatile boolean isCancelled = false;
  17.         public Lock(String value) {
  18.             this.value = value;
  19.         }
  20.         public boolean isCancelled() {
  21.             return isCancelled;
  22.         }
  23.         public void cancel() {
  24.             isCancelled = true;
  25.         }
  26.     }
  27.     private static final String LOCK_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local expireTime = tonumber(ARGV[2])  -- 动态过期时间\n" + "if not expireTime or expireTime <= 0 then\n" + "    return "0"\n" + "end\n" + "local result = redis.call("set", lockKey, lockSecret, "NX", "EX", expireTime)\n" + "return result and "1" or "0"";
  28.     private static final String RELEASE_LOCK_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local storedSecret = redis.call("get", lockKey)\n" + "if storedSecret == lockSecret then\n" + "    return redis.call("del", lockKey) == 1 and "1" or "0"\n" + "else\n" + "    return "0"\n" + "end";
  29.     private static final String RENEWAL_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local expireTime = tonumber(ARGV[2])\n" + "if not expireTime or expireTime <= 0 then\n" + "    return "0"\n" + "end\n" + "local storedSecret = redis.call("get", lockKey)\n" + "if storedSecret == lockSecret then\n" + "    local result = redis.call("expire", lockKey, expireTime)\n" + "    return result == 1 and "1" or "0"\n" + "else\n" + "    return "0"\n" + "end";
  30.     private final String defaultExpireTime = "30";
  31.     private final RedisTemplate<String, String> redisTemplate;
  32.     private final Map<String, Lock> locks = new ConcurrentHashMap<>();
  33.     private final ScheduledExecutorService watchdogExecutor = Executors.newScheduledThreadPool(1);
  34.     public RedisUtils(RedisTemplate<String, String> redisTemplate) {
  35.         this.redisTemplate = redisTemplate;
  36.         watchdogExecutor.scheduleAtFixedRate(() -> {
  37.             try {
  38.                 System.out.println("watchdogExecutor 执行中... locks => " + JSON.toJSONString(locks));
  39.                 locks.entrySet().removeIf(entry -> entry.getValue().isCancelled());
  40.                 for (Map.Entry<String, Lock> entry : locks.entrySet()) {
  41.                     String key = entry.getKey();
  42.                     Lock lock = entry.getValue();
  43.                     if (!lock.isCancelled()) {
  44.                         RedisScript<String> redisScript = RedisScript.of(RENEWAL_LUA, String.class);
  45.                         String result = redisTemplate.execute(redisScript, Collections.singletonList(key), lock.value, defaultExpireTime);
  46.                         if (Objects.equals(result, "0")) {
  47.                             lock.cancel(); // 移除已经释放的锁
  48.                         }
  49.                     }
  50.                 }
  51.             } catch (Exception e) {
  52.                 System.err.println("看门狗任务执行失败: " + e.getMessage());
  53.             }
  54.         }, 0, 10, TimeUnit.SECONDS);
  55.     }
  56.     public boolean acquireLock(String key, String value) {
  57.         RedisScript<String> redisScript = RedisScript.of(LOCK_LUA, String.class);
  58.         String result = redisTemplate.execute(redisScript, Collections.singletonList(key), value, defaultExpireTime);
  59.         if (Objects.equals(result, "1")) {
  60.             locks.put(key, new Lock(value));
  61.             return true;
  62.         }
  63.         return false;
  64.     }
  65.     public boolean acquireLockWithRetry(String key, String value, int maxRetries, long retryIntervalMillis) {
  66.         int retryCount = 0;
  67.         while (retryCount < maxRetries) {
  68.             boolean result = this.acquireLock(key, value);
  69.             if (result) {
  70.                 locks.put(key, new Lock(value));
  71.                 return true;
  72.             }
  73.             retryCount++;
  74.             try {
  75.                 Thread.sleep(retryIntervalMillis);
  76.             } catch (InterruptedException e) {
  77.                 Thread.currentThread().interrupt();
  78.                 return false;
  79.             }
  80.         }
  81.         return false;
  82.     }
  83.     public boolean releaseLock(String key, String value) {
  84.         RedisScript<String> redisScript = RedisScript.of(RELEASE_LOCK_LUA, String.class);
  85.         String result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
  86.         if (Objects.equals(result, "1")) {
  87.             Lock lock = locks.get(key);
  88.             if (lock != null) {
  89.                 lock.cancel();
  90.             }
  91.             return true;
  92.         }
  93.         return false;
  94.     }
  95.     @PreDestroy
  96.     public void shutdown() {
  97.         watchdogExecutor.shutdown();
  98.         try {
  99.             if (!watchdogExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
  100.                 watchdogExecutor.shutdownNow();
  101.             }
  102.         } catch (InterruptedException e) {
  103.             watchdogExecutor.shutdownNow();
  104.         }
  105.     }
  106. }
复制代码
后记

还是免责声明,仅供学习参考

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册