redis分布式锁
文档
官方文档
- 官网操作命令指南页面:https://redis.io/docs/latest/commands/?name=get&group=string
- Redis cluster specification
- Distributed Locks with Redis
说明
- redis版本:7.0.0
- springboot版本:3.2.0
- redisson的版本号:
3.37.0
redis执行lua脚本
安装单机版redis
- 安装单机版redis参考文档:redis单机安装
普通的分布式锁
-
上锁示例1
java/** * @return lockValue(用于解锁校验),加锁失败返回 null */ public String tryLock1(String lockKey, Duration ttl) { String lockValue = UUID.randomUUID().toString(); // 唯一标识:谁加的锁 Boolean ok = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, ttl); return Boolean.TRUE.equals(ok) ? lockValue : null; }- 可用。能够原子性地"抢锁并设置过期时间",满足基础的分布式锁加锁需求
- 优化点:根据业务需要,在加锁失败时支持有限次重试、带退避间隔,而不是一次失败就放弃。
-
解锁示例1
java/** * 非原子解锁:仅作为反例演示 */ public boolean unlock1(String lockKey, String lockValue) { String current = redisTemplate.opsForValue().get(lockKey); if (lockValue != null && lockValue.equals(current)) { return Boolean.TRUE.equals(redisTemplate.delete(lockKey)); } return false; }-
不推荐使用 。这个实现把"校验是不是自己的锁"和"删除锁"拆成了两条命令(
GET+DEL),不是原子操作,会出现竞态窗口:- A 执行
GET lockKey,读到 value 等于自己的lockValue
- A 执行
-
-
这时锁刚好过期,或者被别的线程 B 抢到并写入了新 value(B 已经持有锁)
- A 随后执行
DEL lockKey,会把 B 的锁删除(误删)
结果是:锁的互斥性被破坏,可能出现两个业务同时认为自己持有锁并进入临界区。
- A 随后执行
-
解锁示例2
javaprivate static final DefaultRedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('GET', KEYS[1]) == ARGV[1] then " + " return redis.call('DEL', KEYS[1]) " + "else " + " return 0 " + "end", Long.class ); /** * @return true=解锁成功,false=锁不存在或不是自己的锁 */ public boolean unlock2(String lockKey, String lockValue) { Long r = redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(lockKey), lockValue); return r != null && r == 1L; }- 可用。用 Lua 在 Redis 端原子地"校验 value == 自己的 lockValue 时才 DEL",能够保证"只解自己的锁",满足分布式锁解锁的核心要求。
-
上锁示例2
java/** * 尝试获取分布式锁,支持重试 * * @param lockKey 锁的 key * @param ttl 锁过期时间 * @param maxRetry 最大重试次数(不含第一次,0 表示只尝试一次) * @param backoffMs 每次重试之间的休眠时间(毫秒) * @return 成功:返回 lockValue(用于解锁);失败:返回 null */ public String tryLockWithRetry(String lockKey, Duration ttl, int maxRetry, long backoffMs) { String lockValue = UUID.randomUUID().toString(); int attempt = 0; while (true) { Boolean ok = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, ttl); if (Boolean.TRUE.equals(ok)) { // 加锁成功 return lockValue; } if (attempt >= maxRetry) { // 超过最大重试次数 return null; } attempt++; // 简单固定间隔退避,也可以扩展为指数退避 try { Thread.sleep(backoffMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } } }- 可用 。带重试的加锁方法,在原子
SET NX PX的基础上增加"有限次重试 + 间隔等待",在高竞争场景下提高获取锁成功率,同时保持锁语义不变
- 可用 。带重试的加锁方法,在原子
带续期的分布式锁
-
代码示例:
RedisLockWithRenewService.javajava@Component public class RedisLockWithRenewService { private static final Duration MIN_TTL = Duration.ofSeconds(5); private static final DefaultRedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('GET', KEYS[1]) == ARGV[1] then " + " return redis.call('DEL', KEYS[1]) " + "else " + " return 0 " + "end", Long.class ); private static final DefaultRedisScript<Long> RENEW_SCRIPT = new DefaultRedisScript<>( "if redis.call('GET', KEYS[1]) == ARGV[1] then " + " return redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + "else " + " return 0 " + "end", Long.class ); private final StringRedisTemplate redis; private final ScheduledExecutorService scheduler; public RedisLockWithRenewService(StringRedisTemplate redis) { this.redis = redis; this.scheduler = Executors.newScheduledThreadPool(2, r -> { Thread t = new Thread(r, "redis-lock-renew"); t.setDaemon(true); return t; }); } public static final class LockHandle { private final String key; private final String value; private final long ttlMs; private final List<String> keys; private final ScheduledFuture<?> renewFuture; private LockHandle(String key, String value, long ttlMs, ScheduledFuture<?> renewFuture) { this.key = key; this.value = value; this.ttlMs = ttlMs; this.keys = List.of(key); this.renewFuture = renewFuture; } public String key() { return key; } public String value() { return value; } private void stopRenew() { if (renewFuture != null) { renewFuture.cancel(false); } } } /** * 加锁成功返回 handle(自动续期);失败返回 null */ public LockHandle tryLockWithRenew(String lockKey, Duration ttl) { validateTtl(ttl); String lockValue = UUID.randomUUID().toString(); Boolean ok = redis.opsForValue().setIfAbsent(lockKey, lockValue, ttl); if (!Boolean.TRUE.equals(ok)) return null; List<String> keys = List.of(lockKey); long ttlMs = ttl.toMillis(); long periodMs = ttlMs / 3; final LockHandle[] holder = new LockHandle[1]; ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> { LockHandle h = holder[0]; try { Long r = redis.execute(RENEW_SCRIPT, keys, lockValue, String.valueOf(ttlMs)); if (r == null || r == 0L) { // 锁已不存在或不属于自己:停止续期,避免无意义执行 if (h != null) { h.stopRenew(); } } } catch (Exception ignored) { // 续期失败不抛出,避免任务线程终止;需要可加日志/指标 } }, periodMs, periodMs, TimeUnit.MILLISECONDS); LockHandle handle = new LockHandle(lockKey, lockValue, ttlMs, future); holder[0] = handle; return handle; } /** * 仅解自己的锁:先停续期,再原子校验删除 */ public boolean unlock(LockHandle handle) { if (handle == null) return false; handle.stopRenew(); Long r = redis.execute(UNLOCK_SCRIPT, handle.keys, handle.value); return r != null && r == 1L; } private static void validateTtl(Duration ttl) { if (ttl == null || ttl.isZero() || ttl.isNegative()) { throw new IllegalArgumentException("ttl must be positive"); } if (ttl.compareTo(MIN_TTL) < 0) { throw new IllegalArgumentException("ttl too small: " + ttl + ", min is " + MIN_TTL); } } @PreDestroy public void shutdown() { scheduler.shutdownNow(); } } -
基本功能
- 加锁:在 Redis 中以原子方式创建锁(
SET key value NX PX ttl),成功返回唯一value(锁标识),失败表示已有锁。 - 解锁:只允许持有者释放锁(对比
value一致才删除),避免误删别人刚抢到的锁。 - 续期(看门狗):业务执行时间可能超过初始 TTL,通过后台任务定期延长锁的过期时间,防止锁在业务未完成时过期。
- 加锁:在 Redis 中以原子方式创建锁(
-
续期原理
- 定时触发:加锁成功后启动一个定时任务,按固定间隔(常用 TTL 的 1/3)去续期。
- 校验持有者:续期时先检查
GET key是否仍等于自己的value,只有一致才PEXPIRE key ttl把 TTL 续回去。 - 原子性保证:用 Lua 将"校验 value + 续期"合成一次原子执行,避免把别人的锁续长;一旦校验失败(锁不存在/不属于自己),停止续期并不再认为自己持有锁。
- 应注意锁不释放,导致不断续期的问题
可重入的分布式锁
-
代码示例:
ReentrantRedisLockService.javajava@Service public class ReentrantRedisLockService { private static final Duration MIN_TTL = Duration.ofSeconds(5); private final StringRedisTemplate redis; private final ScheduledExecutorService scheduler; private final String instanceId; // 可重入加锁:不存在则创建(owner=1)+expire;存在且owner相同则incr+expire;否则失败 private static final DefaultRedisScript<Long> LOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('EXISTS', KEYS[1]) == 0 then " + " redis.call('HSET', KEYS[1], ARGV[1], 1) " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "end " + "if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then " + " redis.call('HINCRBY', KEYS[1], ARGV[1], 1) " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "end " + "return 0", Long.class ); // 可重入解锁:仅owner可减计数;减到0删除整个key;否则保留并(可选)刷新expire private static final DefaultRedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 then " + " return 0 " + "end " + "local c = redis.call('HINCRBY', KEYS[1], ARGV[1], -1) " + "if c > 0 then " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "else " + " redis.call('DEL', KEYS[1]) " + " return 1 " + "end", Long.class ); // 续期:仍是owner才续 private static final DefaultRedisScript<Long> RENEW_SCRIPT = new DefaultRedisScript<>( "if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then " + " return redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + "else " + " return 0 " + "end", Long.class ); private final ThreadLocal<String> ownerIdHolder = new ThreadLocal<>(); public ReentrantRedisLockService(StringRedisTemplate redis, @Value("${spring.application.name:app}") String appName) { this.redis = redis; this.instanceId = appName + ":" + pidPart() + ":" + UUID.randomUUID(); this.scheduler = Executors.newScheduledThreadPool(2, r -> { Thread t = new Thread(r, "reentrant-redis-lock-renew"); t.setDaemon(true); return t; }); } public static final class LockHandle { private final String key; private final String ownerId; private final long ttlMs; private final List<String> keys; private final ScheduledFuture<?> renewFuture; private LockHandle(String key, String ownerId, long ttlMs, ScheduledFuture<?> renewFuture) { this.key = key; this.ownerId = ownerId; this.ttlMs = ttlMs; this.keys = List.of(key); this.renewFuture = renewFuture; } public String key() { return key; } public String ownerId() { return ownerId; } private void stopRenew() { if (renewFuture != null) renewFuture.cancel(false); } } /** * 尝试获取可重入锁:同一线程重复调用可重入(计数+1) * * @param enableRenew 是否开启看门狗续期 * @return 成功返回 handle,失败返回 null */ public LockHandle tryLock(String lockKey, Duration ttl, boolean enableRenew) { validateTtl(ttl); String ownerId = getOrInitOwnerIdForThread(); List<String> keys = List.of(lockKey); long ttlMs = ttl.toMillis(); Long ok = redis.execute(LOCK_SCRIPT, keys, ownerId, String.valueOf(ttlMs)); if (ok == null || ok != 1L) return null; ScheduledFuture<?> future = null; if (enableRenew) { long periodMs = ttlMs / 3; final LockHandle[] holder = new LockHandle[1]; future = scheduler.scheduleAtFixedRate(() -> { try { Long r = redis.execute(RENEW_SCRIPT, keys, ownerId, String.valueOf(ttlMs)); if (r == null || r == 0L) { // 锁已丢失/不属于自己:停止续期 LockHandle h = holder[0]; if (h != null) { h.stopRenew(); } } } catch (Exception ignored) { // 续期失败不抛出,避免任务线程终止;需要可加日志/指标 } }, periodMs, periodMs, TimeUnit.MILLISECONDS); LockHandle handle = new LockHandle(lockKey, ownerId, ttlMs, future); holder[0] = handle; return handle; } // 不启用续期:future 为 null return new LockHandle(lockKey, ownerId, ttlMs, null); } /** * 解锁:仅解自己的锁;可重入场景下计数-1,减到0才真正删除 key */ public boolean unlock(LockHandle handle) { if (handle == null) return false; handle.stopRenew(); Long r = redis.execute(UNLOCK_SCRIPT, handle.keys, handle.ownerId, String.valueOf(handle.ttlMs)); return r != null && r == 1L; } /** * 可选:在你确定线程上下文结束时调用,避免 ThreadLocal 在复用线程(如线程池)里"粘住" */ public void clearOwnerIdForThread() { ownerIdHolder.remove(); } private String getOrInitOwnerIdForThread() { String v = ownerIdHolder.get(); if (v != null) return v; String ownerId = instanceId + ":" + Thread.currentThread().getId(); ownerIdHolder.set(ownerId); return ownerId; } private static void validateTtl(Duration ttl) { if (ttl == null || ttl.isZero() || ttl.isNegative()) { throw new IllegalArgumentException("ttl must be positive"); } if (ttl.compareTo(MIN_TTL) < 0) { throw new IllegalArgumentException("ttl too small: " + ttl + ", min is " + MIN_TTL); } } private static String pidPart() { // "pid@hostname" String name = ManagementFactory.getRuntimeMXBean().getName(); int idx = name.indexOf('@'); return idx > 0 ? name.substring(0, idx) : name; } @PreDestroy public void shutdown() { scheduler.shutdownNow(); } } -
调用示例
javaReentrantRedisLockService.LockHandle h = lockService.tryLock("lock:order:1", Duration.ofSeconds(30), true); if (h == null) return; // 没拿到锁 try { // 可重入:同一线程再次 tryLock 同一 lockKey lockService.tryLock("lock:order:1", Duration.ofSeconds(30), true); // 业务逻辑... } finally { // 必须"加几次解几次" lockService.unlock(h); // 若你 tryLock 了两次,这里还需要再 unlock 一次 } -
基本功能
- 加锁:用 Lua 原子执行
- 锁不存在:
HSET ownerId -> 1+PEXPIRE ttl - 锁存在且 hash 里已有相同
ownerId:HINCRBY count +1并刷新PEXPIRE - 锁存在但不是自己的
ownerId:返回失败(0)
- 锁不存在:
- 解锁:用 Lua 原子执行
- 只有当 hash 里存在自己的
ownerId才会count - 1 count > 0:不删 key,只刷新PEXPIREcount == 0:DEL整个lockKey(把该锁相关的所有数据删掉)
- 只有当 hash 里存在自己的
- 续期(可选 enableRenew=true):定时
RENEW_SCRIPT- 仍然只在"hash 里仍存在自己的
ownerIdfield"时才续PEXPIRE - 应注意锁不释放,导致不断续期的问题
- 仍然只在"hash 里仍存在自己的
- 加锁:用 Lua 原子执行
-
可重入原理(为什么同一线程能重复加锁)
ownerId作为 HSET 的 field:同一线程第一次成功后拿到固定的ownerId- Lua 在加锁时判断:
- 如果
HEXISTS(lockKey, ownerId)为真,就把count+1
- 如果
- 因此同一线程(同一 ownerId)重复加锁不会失败,而是递增重入次数;解锁时
count递减到 0 才真正释放。
-
某些异常场景下,可能会出现多个
ownerId(同一个lockKey的 Hash 里同时有多个 field),导致分布式锁语义被破坏
可重入的分布式锁优化(单 owner + count)
-
代码示例:
ReentrantRedisLockService1.javajava@Service public class ReentrantRedisLockService1 { private static final Duration MIN_TTL = Duration.ofSeconds(5); // 单 owner + count:Hash 的字段名固定 private static final String OWNER_FIELD = "owner"; private static final String COUNT_FIELD = "count"; private final StringRedisTemplate redis; private final ScheduledExecutorService scheduler; private final String instanceId; // 可重入依据:同一线程同一 ownerId private final ThreadLocal<String> ownerIdHolder = new ThreadLocal<>(); // 只在 tryLock:判断该 lockKey 是否已启动过续期(避免重复 scheduleAtFixedRate) private final ThreadLocal<Map<String, Boolean>> renewStartedMap = ThreadLocal.withInitial(HashMap::new); // 单 owner + count:加锁 private static final DefaultRedisScript<Long> LOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('EXISTS', KEYS[1]) == 0 then " + " redis.call('HSET', KEYS[1], '" + OWNER_FIELD + "', ARGV[1], '" + COUNT_FIELD + "', 1) " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "end " + "if redis.call('HGET', KEYS[1], '" + OWNER_FIELD + "') == ARGV[1] then " + " redis.call('HINCRBY', KEYS[1], '" + COUNT_FIELD + "', 1) " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "end " + "return 0", Long.class ); // 单 owner + count:解锁(仅 owner 可解;count 递减;到 0 删除整个 key) private static final DefaultRedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('HGET', KEYS[1], '" + OWNER_FIELD + "') ~= ARGV[1] then " + " return 0 " + "end " + "local c = redis.call('HINCRBY', KEYS[1], '" + COUNT_FIELD + "', -1) " + "if c > 0 then " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "else " + " redis.call('DEL', KEYS[1]) " + " return 1 " + "end", Long.class ); // 单 owner + count:续期(只有 owner 匹配才续) private static final DefaultRedisScript<Long> RENEW_SCRIPT = new DefaultRedisScript<>( "if redis.call('HGET', KEYS[1], '" + OWNER_FIELD + "') == ARGV[1] then " + " return redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + "else " + " return 0 " + "end", Long.class ); public ReentrantRedisLockService1(StringRedisTemplate redis, @Value("${spring.application.name:app}") String appName) { this.redis = redis; this.instanceId = appName + ":" + pidPart() + ":" + UUID.randomUUID(); this.scheduler = Executors.newScheduledThreadPool(2, r -> { Thread t = new Thread(r, "reentrant-redis-lock-renew-1"); t.setDaemon(true); return t; }); } public static final class LockHandle { private final String key; private final String ownerId; private final long ttlMs; private final List<String> keys; private LockHandle(String key, String ownerId, long ttlMs) { this.key = key; this.ownerId = ownerId; this.ttlMs = ttlMs; this.keys = List.of(key); } public String key() { return key; } public String ownerId() { return ownerId; } public long ttlMs() { return ttlMs; } private List<String> keys() { return keys; } } /** * @return 成功返回 LockHandle;失败返回 null */ public LockHandle tryLock(String lockKey, Duration ttl, boolean enableRenew) { validateTtl(ttl); String ownerId = getOrInitOwnerIdForThread(); long ttlMs = ttl.toMillis(); List<String> keys = List.of(lockKey); Long ok = redis.execute(LOCK_SCRIPT, keys, ownerId, String.valueOf(ttlMs)); if (ok == null || ok != 1L) return null; if (!enableRenew) { return new LockHandle(lockKey, ownerId, ttlMs); } // 只启动一次续期(同一线程同一 lockKey) Map<String, Boolean> renewStarted = renewStartedMap.get(); boolean started = renewStarted.getOrDefault(lockKey, false); if (!started) { renewStarted.put(lockKey, true); long periodMs = Math.max(100L, ttlMs / 3); AtomicReference<ScheduledFuture<?>> futureRef = new AtomicReference<>(); Runnable task = () -> { try { // 续期任务里捕获的 ttlMs 是首次启动时的 ttlMs(按你的约束做简化) Long r = redis.execute(RENEW_SCRIPT, keys, ownerId, String.valueOf(ttlMs)); if (r == null || r == 0L) { ScheduledFuture<?> f = futureRef.get(); if (f != null) { f.cancel(false); } } } catch (Exception ignored) { } }; ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(task, periodMs, periodMs, TimeUnit.MILLISECONDS); futureRef.set(future); } return new LockHandle(lockKey, ownerId, ttlMs); } /** * 解锁:仅解自己的锁(重入次数由 Lua 的 count 字段决定) * * @return true 表示 Lua 成功执行了解锁逻辑;false 表示 owner 不匹配或锁已不存在 */ public boolean unlock(LockHandle handle) { if (handle == null) return false; Long r = redis.execute(UNLOCK_SCRIPT, handle.keys(), handle.ownerId(), String.valueOf(handle.ttlMs())); if (r == null || r != 1L) { return false; } // 如果锁已经被删掉(owner 字段不存在了),允许后续 tryLock 再启动续期 Boolean stillHeld = redis.opsForHash().hasKey(handle.key(), OWNER_FIELD); if (stillHeld != null && !stillHeld) { Map<String, Boolean> renewStarted = renewStartedMap.get(); renewStarted.remove(handle.key()); // 可选:如果这个线程已无任何续期状态,清理 ThreadLocal if (renewStarted.isEmpty()) { ownerIdHolder.remove(); renewStartedMap.remove(); } } return true; } public void clearOwnerIdForThread() { ownerIdHolder.remove(); renewStartedMap.remove(); } private String getOrInitOwnerIdForThread() { String v = ownerIdHolder.get(); if (v != null) return v; String ownerId = instanceId + ":" + Thread.currentThread().getId(); ownerIdHolder.set(ownerId); return ownerId; } private static void validateTtl(Duration ttl) { if (ttl == null || ttl.isZero() || ttl.isNegative()) { throw new IllegalArgumentException("ttl must be positive"); } if (ttl.compareTo(MIN_TTL) < 0) { throw new IllegalArgumentException("ttl too small: " + ttl + ", min is " + MIN_TTL); } } private static String pidPart() { String name = ManagementFactory.getRuntimeMXBean().getName(); int idx = name.indexOf('@'); return idx > 0 ? name.substring(0, idx) : name; } @PreDestroy public void shutdown() { scheduler.shutdownNow(); } } -
优化说明
-
不让 hash 存"多个 owner 字段计数",而是只存一个当前 owner + 一个重入计数。例如用 hash 固定字段:
HSET lockKey owner = ownerIdHSET lockKey count = 1- 加锁:只有当
owner相等才count+1 - 解锁:
count-1,为 0 才DEL
这样即使 hash 里出现脏字段,也不会影响"锁的唯一持有者"判断。
-
可重入的分布式锁实现Lock接口
-
代码示例
ReentrantRedisLock2.javajavapublic class ReentrantRedisLock2 implements Lock { private static final Duration MIN_TTL = Duration.ofSeconds(5); private static final String OWNER_FIELD = "owner"; private static final String COUNT_FIELD = "count"; // 所有 ReentrantRedisLock2 实例共享同一调度器,避免每次 new 一个锁就创建线程池 private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(2, r -> { Thread t = new Thread(r, "reentrant-redis-lock-renew-2"); t.setDaemon(true); return t; }); // 关键:ownerId 必须跨不同 ReentrantRedisLock2 实例保持一致,才能实现"每次上锁 new 一个锁仍可重入" private static final String INSTANCE_ID = pidPart() + ":" + UUID.randomUUID(); // 可重入依据:同一线程同一 ownerId private static final ThreadLocal<String> OWNER_ID_HOLDER = new ThreadLocal<>(); // 只在 tryLock 时启动一次续期(同一线程同一 lockKey) private static final ThreadLocal<Map<String, Boolean>> RENEW_STARTED_MAP = ThreadLocal.withInitial(HashMap::new); private static final DefaultRedisScript<Long> LOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('EXISTS', KEYS[1]) == 0 then " + " redis.call('HSET', KEYS[1], '" + OWNER_FIELD + "', ARGV[1], '" + COUNT_FIELD + "', 1) " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "end " + "if redis.call('HGET', KEYS[1], '" + OWNER_FIELD + "') == ARGV[1] then " + " redis.call('HINCRBY', KEYS[1], '" + COUNT_FIELD + "', 1) " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "end " + "return 0", Long.class ); private static final DefaultRedisScript<Long> UNLOCK_SCRIPT = new DefaultRedisScript<>( "if redis.call('HGET', KEYS[1], '" + OWNER_FIELD + "') ~= ARGV[1] then " + " return 0 " + "end " + "local c = redis.call('HINCRBY', KEYS[1], '" + COUNT_FIELD + "', -1) " + "if c > 0 then " + " redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + " return 1 " + "else " + " redis.call('DEL', KEYS[1]) " + " return 1 " + "end", Long.class ); private static final DefaultRedisScript<Long> RENEW_SCRIPT = new DefaultRedisScript<>( "if redis.call('HGET', KEYS[1], '" + OWNER_FIELD + "') == ARGV[1] then " + " return redis.call('PEXPIRE', KEYS[1], ARGV[2]) " + "else " + " return 0 " + "end", Long.class ); private final StringRedisTemplate redis; private final String lockKey; private final long ttlMs; private final boolean enableRenew; // lock 成功后记录 ownerId(解锁时使用) private String acquiredOwnerId; public ReentrantRedisLock2(StringRedisTemplate redis, String lockKey, Duration ttl, boolean enableRenew) { if (redis == null) throw new IllegalArgumentException("redis must not be null"); if (lockKey == null || lockKey.isEmpty()) throw new IllegalArgumentException("lockKey must not be empty"); validateTtl(ttl); this.redis = redis; this.lockKey = lockKey; this.ttlMs = ttl.toMillis(); this.enableRenew = enableRenew; } @Override public void lock() { while (true) { if (tryLock()) return; sleepQuietly(50); } } @Override public void lockInterruptibly() throws InterruptedException { while (true) { if (tryLock()) return; Thread.sleep(50); } } @Override public boolean tryLock() { String ownerId = getOrInitOwnerIdForThread(); List<String> keys = List.of(lockKey); Long ok = redis.execute(LOCK_SCRIPT, keys, ownerId, String.valueOf(ttlMs)); if (ok == null || ok != 1L) { return false; } this.acquiredOwnerId = ownerId; if (enableRenew) { // 只启动一次续期(同一线程同一 lockKey) Map<String, Boolean> renewStarted = RENEW_STARTED_MAP.get(); boolean started = renewStarted.getOrDefault(lockKey, false); if (!started) { renewStarted.put(lockKey, true); long periodMs = Math.max(100L, ttlMs / 3); AtomicReference<ScheduledFuture<?>> futureRef = new AtomicReference<>(); Runnable task = () -> { try { // 续期任务里捕获的 ttlMs 是首次启动续期时的 ttlMs Long r = redis.execute(RENEW_SCRIPT, keys, ownerId, String.valueOf(ttlMs)); if (r == null || r == 0L) { ScheduledFuture<?> f = futureRef.get(); if (f != null) f.cancel(false); } } catch (Exception ignored) { } }; ScheduledFuture<?> future = SCHEDULER.scheduleAtFixedRate(task, periodMs, periodMs, TimeUnit.MILLISECONDS); futureRef.set(future); } } return true; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { long deadline = System.nanoTime() + unit.toNanos(time); while (System.nanoTime() <= deadline) { if (tryLock()) return true; Thread.sleep(10); } return false; } @Override public void unlock() { if (acquiredOwnerId == null) { throw new IllegalMonitorStateException("Attempt to unlock without lock: " + lockKey); } List<String> keys = List.of(lockKey); Long r = redis.execute(UNLOCK_SCRIPT, keys, acquiredOwnerId, String.valueOf(ttlMs)); if (r == null || r != 1L) { return; } // 如果锁已经被删掉(owner 字段不存在了),允许后续 tryLock 再启动续期 Boolean stillHeld = redis.opsForHash().hasKey(lockKey, OWNER_FIELD); if (stillHeld != null && !stillHeld) { Map<String, Boolean> renewStarted = RENEW_STARTED_MAP.get(); renewStarted.remove(lockKey); // 可选:如果这个线程已无任何续期状态,清理 ThreadLocal if (renewStarted.isEmpty()) { OWNER_ID_HOLDER.remove(); RENEW_STARTED_MAP.remove(); } } } @Override public Condition newCondition() { throw new UnsupportedOperationException("Conditions are not supported by this Redis lock"); } public String key() { return lockKey; } public String ownerId() { return acquiredOwnerId; } public void clearOwnerIdForThread() { OWNER_ID_HOLDER.remove(); RENEW_STARTED_MAP.remove(); } private static void validateTtl(Duration ttl) { if (ttl == null || ttl.isZero() || ttl.isNegative()) { throw new IllegalArgumentException("ttl must be positive"); } if (ttl.compareTo(MIN_TTL) < 0) { throw new IllegalArgumentException("ttl too small: " + ttl + ", min is " + MIN_TTL); } } private static void sleepQuietly(long ms) { try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private static String getOrInitOwnerIdForThread() { String v = OWNER_ID_HOLDER.get(); if (v != null) return v; String ownerId = INSTANCE_ID + ":" + Thread.currentThread().getId(); OWNER_ID_HOLDER.set(ownerId); return ownerId; } private static String pidPart() { String name = ManagementFactory.getRuntimeMXBean().getName(); int idx = name.indexOf('@'); return idx > 0 ? name.substring(0, idx) : name; } } -
ReentrantRedisLock2是一个基于 Redis + Lua 的可重入分布式锁实现(implements Lock),主要功能是:- 加锁:
tryLock/lock时用 Lua 原子执行- 锁不存在:写入
owner和count=1,并设置过期时间 - 锁存在且
owner是自己:count+1(可重入)并刷新过期时间 - 锁存在但
owner不是自己:加锁失败
- 锁不存在:写入
- 解锁:
unlock时仅允许当前owner解锁,count-1,降到0才删除锁 key。 - 续期(可选):开启后会启动定时续期任务,仅当
owner仍是自己时才续期,防止业务执行时间超过 TTL 导致锁提前过期。 - 支持"每次 new 一个锁对象"仍可重入:通过线程级固定
ownerId(ThreadLocal)保证同一线程同一lockKey的多次加锁被识别为重入。
- 加锁:
Redisson实现分布式锁
-
使用示例
javapublic class RedissonLockUsage { private final RedissonClient redissonClient; public RedissonLockUsage(RedissonClient redissonClient) { this.redissonClient = redissonClient; } /** * lock() * - 抢不到锁:会等待(一直阻塞,直到拿到锁) * - 等待时长:无限等待(除非线程中断/业务主动中止) * - 抢到锁后:自动续期(启用看门狗) */ public void useLock() { RLock lock = redissonClient.getLock("lock:demo:lock"); lock.lock(); try { // 业务逻辑 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } /** * tryLock(waitTime, leaseTime, unit) ------ 固定租约 * - 抢不到锁:会等待 * - 等待时长:最多 waitTime(示例 2 秒) * - 抢到锁后:不自动续期(leaseTime=30s,固定过期) */ public void useTryLockFixedLease() throws InterruptedException { RLock lock = redissonClient.getLock("lock:demo:trylock:fixed"); boolean ok = lock.tryLock(2, 30, TimeUnit.SECONDS); if (!ok) { // 2 秒内没抢到 return; } try { // 业务逻辑(应尽量在 30 秒内完成) } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } /** * tryLock(waitTime, -1, unit) ------ 看门狗模式 * - 抢不到锁:会等待 * - 等待时长:最多 waitTime(示例 2 秒) * - 抢到锁后:自动续期(leaseTime=-1,启用看门狗) */ public void useTryLockWatchdog() throws InterruptedException { RLock lock = redissonClient.getLock("lock:demo:trylock:watchdog"); boolean ok = lock.tryLock(2, -1, TimeUnit.SECONDS); if (!ok) { // 2 秒内没抢到 return; } try { // 业务逻辑(耗时不确定) } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } } -
方法快速对照
lock.lock():- 抢不到锁:一直等
- 抢到后:看门狗自动续期
tryLock(wait, lease>0, unit):- 抢不到锁:最多等 wait
- 抢到后:固定租约,不自动续期
tryLock(wait, -1, unit):- 抢不到锁:最多等 wait
- 抢到后:看门狗自动续期
参考资料
注意事项
- 部分内容由AI生成
- 如有不对,欢迎指正!!!