redis分布式锁

redis分布式锁

文档

  1. redis单机安装
  2. redis常用的五种数据类型
  3. springboot整合redis-RedisTemplate单机模式
  4. 布隆过滤器 -Bloom Filter

官方文档

  1. 官网操作命令指南页面:https://redis.io/docs/latest/commands/?name=get&group=string
  2. Redis cluster specification
  3. Distributed Locks with Redis

说明

  1. redis版本:7.0.0
  2. springboot版本:3.2.0
  3. redisson的版本号:3.37.0

redis执行lua脚本

安装单机版redis
  1. 安装单机版redis参考文档:redis单机安装
普通的分布式锁
  1. 上锁示例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;
    }
    • 可用。能够原子性地"抢锁并设置过期时间",满足基础的分布式锁加锁需求
    • 优化点:根据业务需要,在加锁失败时支持有限次重试、带退避间隔,而不是一次失败就放弃。
  2. 解锁示例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
  • 这时锁刚好过期,或者被别的线程 B 抢到并写入了新 value(B 已经持有锁)

    • A 随后执行 DEL lockKey,会把 B 的锁删除(误删)

    结果是:锁的互斥性被破坏,可能出现两个业务同时认为自己持有锁并进入临界区。

  1. 解锁示例2

    java 复制代码
    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
    );
    
    /**
     * @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. 上锁示例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 的基础上增加"有限次重试 + 间隔等待",在高竞争场景下提高获取锁成功率,同时保持锁语义不变
带续期的分布式锁
  1. 代码示例:RedisLockWithRenewService.java

    java 复制代码
    @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();
        }
    }
  2. 基本功能

    • 加锁:在 Redis 中以原子方式创建锁(SET key value NX PX ttl),成功返回唯一 value(锁标识),失败表示已有锁。
    • 解锁:只允许持有者释放锁(对比 value 一致才删除),避免误删别人刚抢到的锁。
    • 续期(看门狗):业务执行时间可能超过初始 TTL,通过后台任务定期延长锁的过期时间,防止锁在业务未完成时过期。
  3. 续期原理

    • 定时触发:加锁成功后启动一个定时任务,按固定间隔(常用 TTL 的 1/3)去续期。
    • 校验持有者:续期时先检查 GET key 是否仍等于自己的 value,只有一致才 PEXPIRE key ttl 把 TTL 续回去。
    • 原子性保证:用 Lua 将"校验 value + 续期"合成一次原子执行,避免把别人的锁续长;一旦校验失败(锁不存在/不属于自己),停止续期并不再认为自己持有锁。
    • 应注意锁不释放,导致不断续期的问题
可重入的分布式锁
  1. 代码示例:ReentrantRedisLockService.java

    java 复制代码
    @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();
        }
    }
  2. 调用示例

    java 复制代码
    ReentrantRedisLockService.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 一次
    }
  3. 基本功能

    • 加锁:用 Lua 原子执行
      • 锁不存在:HSET ownerId -> 1 + PEXPIRE ttl
      • 锁存在且 hash 里已有相同 ownerIdHINCRBY count +1 并刷新 PEXPIRE
      • 锁存在但不是自己的 ownerId:返回失败(0)
    • 解锁:用 Lua 原子执行
      • 只有当 hash 里存在自己的 ownerId 才会 count - 1
      • count > 0:不删 key,只刷新 PEXPIRE
      • count == 0DEL 整个 lockKey(把该锁相关的所有数据删掉)
    • 续期(可选 enableRenew=true):定时RENEW_SCRIPT
      • 仍然只在"hash 里仍存在自己的 ownerId field"时才续 PEXPIRE
      • 应注意锁不释放,导致不断续期的问题
  4. 可重入原理(为什么同一线程能重复加锁)

    • ownerId 作为 HSET 的 field:同一线程第一次成功后拿到固定的 ownerId
    • Lua 在加锁时判断:
      • 如果 HEXISTS(lockKey, ownerId) 为真,就把 count +1
    • 因此同一线程(同一 ownerId)重复加锁不会失败,而是递增重入次数;解锁时 count 递减到 0 才真正释放。
  5. 某些异常场景下,可能会出现多个 ownerId(同一个 lockKey 的 Hash 里同时有多个 field),导致分布式锁语义被破坏

可重入的分布式锁优化(单 owner + count)
  1. 代码示例:ReentrantRedisLockService1.java

    java 复制代码
    @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();
        }
    }
  2. 优化说明

    1. 不让 hash 存"多个 owner 字段计数",而是只存一个当前 owner + 一个重入计数。例如用 hash 固定字段:

      • HSET lockKey owner = ownerId
      • HSET lockKey count = 1
      • 加锁:只有当 owner 相等才 count+1
      • 解锁:count-1,为 0 才 DEL

      这样即使 hash 里出现脏字段,也不会影响"锁的唯一持有者"判断。

可重入的分布式锁实现Lock接口
  1. 代码示例ReentrantRedisLock2.java

    java 复制代码
    public 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;
        }
    }
  2. ReentrantRedisLock2 是一个基于 Redis + Lua 的可重入分布式锁实现(implements Lock),主要功能是:

    • 加锁:tryLock/lock时用 Lua 原子执行
      • 锁不存在:写入 ownercount=1,并设置过期时间
      • 锁存在且 owner 是自己:count+1(可重入)并刷新过期时间
      • 锁存在但 owner 不是自己:加锁失败
    • 解锁:unlock 时仅允许当前 owner 解锁,count-1,降到 0 才删除锁 key。
    • 续期(可选):开启后会启动定时续期任务,仅当 owner 仍是自己时才续期,防止业务执行时间超过 TTL 导致锁提前过期。
    • 支持"每次 new 一个锁对象"仍可重入:通过线程级固定 ownerIdThreadLocal)保证同一线程同一 lockKey 的多次加锁被识别为重入。
Redisson实现分布式锁
  1. 使用示例

    java 复制代码
    public 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();
                }
            }
        }
    }
  2. 方法快速对照

    • lock.lock()
      • 抢不到锁:一直等
      • 抢到后:看门狗自动续期
    • tryLock(wait, lease>0, unit)
      • 抢不到锁:最多等 wait
      • 抢到后:固定租约,不自动续期
    • tryLock(wait, -1, unit)
      • 抢不到锁:最多等 wait
      • 抢到后:看门狗自动续期

参考资料

  1. https://www.bilibili.com/video/BV13R4y1v7sP

注意事项

  1. 部分内容由AI生成
  2. 如有不对,欢迎指正!!!
相关推荐
今天背单词了吗98016 小时前
MySQL InnoDB引擎八大核心特性详解(高频面试题)
java·数据库·mysql
麦聪聊数据16 小时前
中小企无需重型数据中台:轻量化数据体系搭建完整方案
数据库
song50116 小时前
昇腾 910 的硬件架构:为什么它适合跑大模型
图像处理·人工智能·分布式·flutter·硬件架构·交互
会编程的土豆16 小时前
Kafka 操作流程(零基础完整流程)
分布式·kafka
我也不曾来过116 小时前
MYSQL 使用C语言链接
数据库·mysql
今天背单词了吗98016 小时前
缓存与数据库双写不一致问题及终极解决方案(高频面试题)
java·数据库·学习·缓存
倔强的石头_16 小时前
内核代差揭秘:从 DISTINCT 优化实测看国产数据库的逻辑推理深度
数据库
未若君雅裁16 小时前
分布式接口幂等性设计:唯一索引、Token 与分布式锁
分布式·微服务
云边有个稻草人16 小时前
金仓数据库 KES:DISTINCT 语句性能优化实践与内核实现
数据库·金仓·kes·数据库内核优化·kes 数据库性能优化·distinct 语句优化·sql 调优