方案概述

本方案基于 Redisson 实现分布式锁,结合重试机制双重检查模式,确保在高并发场景下的数据一致性和系统稳定性。

核心特性

  • 分布式锁:防止多实例/多线程并发执行
  • 重试机制:提高系统容错能力
  • 双重检查:减少不必要的锁竞争
  • 缓存降级:锁获取失败时尝试从缓存读取
  • 异常处理:完善的异常捕获和日志记录

核心设计模式

1. 双重检查锁定(Double-Check Locking)

┌─────────────────────────────────────────┐
│  1. 检查缓存(无锁)                      │
│     ↓ 缓存未命中                          │
│  2. 获取分布式锁                          │
│     ↓ 获取成功                            │
│  3. 再次检查缓存(双重检查)               │
│     ↓ 缓存仍未命中                        │
│  4. 执行业务逻辑                          │
│  5. 更新缓存                              │
│  6. 释放锁                                │
└─────────────────────────────────────────┘

优势

  • 减少锁竞争:大部分请求在第一次检查时就能从缓存获取数据
  • 避免重复执行:获取锁后再次检查,确保不重复执行

2. 重试机制(Retry Pattern)

尝试次数: 1 ──失败──> 等待 200ms ──> 尝试次数: 2 ──失败──> 等待 400ms ──> 尝试次数: 3 ──失败──> 抛出异常

特点

  • 指数退避:每次重试间隔递增(200ms, 400ms, 600ms…)
  • 最大重试次数:防止无限重试
  • 异常记录:记录最后一次异常信息

Redisson 分布式锁详解

lock.tryLock(waitTime, leaseTime, timeUnit) 方法详解

方法签名
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException
参数说明
参数 类型 说明 示例值
waitTime long 等待时间:尝试获取锁的最大等待时间。如果在这段时间内无法获取锁,方法返回 false 15
leaseTime long 持有时间:锁的自动释放时间。超过这个时间,锁会自动释放,即使业务逻辑未完成 30
timeUnit TimeUnit 时间单位 TimeUnit.SECONDS
示例调用
RLock lock = redissonClient.getLock("lock:key");
boolean locked = lock.tryLock(15, 30, TimeUnit.SECONDS);

含义

  • 最多等待 15秒 尝试获取锁
  • 如果获取成功,锁将在 30秒 后自动释放
  • 时间单位:

关键问题解答

1. 是否会自动续命(看门狗机制)?

答案:❌ 不会自动续命

当使用 tryLock(waitTime, leaseTime, timeUnit) 方法并指定了 leaseTime 参数时,Redisson 的看门狗机制会被禁用

原因

  • 看门狗机制只在未指定 leaseTime 时生效
  • 指定 leaseTime 后,Redisson 认为你希望锁在固定时间后自动释放
  • 这是为了避免死锁,但可能导致业务逻辑未完成时锁被释放

验证方法

// 方式1:指定 leaseTime(无看门狗)
lock.tryLock(15, 30, TimeUnit.SECONDS);  // ❌ 无看门狗

// 方式2:不指定 leaseTime(有看门狗)
lock.tryLock(15, -1, TimeUnit.SECONDS);  // ✅ 有看门狗(-1 表示不设置过期时间)
// 或者
lock.lock();  // ✅ 有看门狗,默认30秒续命
2. 如果 30 秒内业务没做完,会出现什么情况?

场景分析

时间线:
T=0s:  线程A获取锁,开始执行业务逻辑(预计需要45秒)
T=30s: 锁自动释放(leaseTime到期)
T=31s: 线程B获取锁,开始执行业务逻辑
T=45s: 线程A的业务逻辑完成,尝试释放锁(可能失败或释放了线程B的锁)

可能的问题

  1. 重复执行:多个线程可能同时执行相同的业务逻辑
  2. 数据不一致:并发修改可能导致数据冲突
  3. 资源浪费:重复调用外部接口,增加系统负载
  4. 锁释放异常:线程A可能释放了线程B的锁

解决方案

  • ✅ 使用看门狗机制(见下文)
  • ✅ 合理设置 leaseTime,确保大于业务执行时间
  • ✅ 业务逻辑中增加幂等性检查
  • ✅ 使用 lock.isHeldByCurrentThread() 检查锁的持有者

看门狗机制详解

什么是看门狗(Watchdog)?

看门狗是 Redisson 提供的一种自动续命机制,用于防止业务逻辑执行时间超过锁的持有时间。

工作原理

┌─────────────────────────────────────────────────────┐
│  1. 获取锁(不指定 leaseTime)                        │
│  2. Redisson 启动看门狗线程                          │
│  3. 每 10 秒检查一次锁是否仍被当前线程持有            │
│  4. 如果持有,自动续命 30 秒(默认值)                │
│  5. 业务逻辑完成后,释放锁,看门狗停止                │
│  6. 如果线程异常退出,锁在 30 秒后自动释放            │
└─────────────────────────────────────────────────────┘

如何启用看门狗?

方式1:使用 lock() 方法(推荐)
RLock lock = redissonClient.getLock("lock:key");
try {
    // 先尝试获取锁,最多等待15秒
    if (lock.tryLock(15, -1, TimeUnit.SECONDS)) {
        try {
            // 业务逻辑
            // 看门狗会自动续命
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new RuntimeException("获取锁被中断", e);
}

注意leaseTime 设置为 -1 表示不设置过期时间,启用看门狗。

方式2:使用 lock() 无参方法
RLock lock = redissonClient.getLock("lock:key");
try {
    // 阻塞等待获取锁,启用看门狗
    lock.lock();
    try {
        // 业务逻辑
        // 看门狗会自动续命
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
} catch (Exception e) {
    // 异常处理
}

注意lock() 方法会阻塞等待,直到获取到锁。

方式3:使用 lock(long leaseTime, TimeUnit unit) 并手动续命
RLock lock = redissonClient.getLock("lock:key");
try {
    if (lock.tryLock(15, 30, TimeUnit.SECONDS)) {
        try {
            // 业务逻辑
            // 如果预计执行时间超过30秒,需要手动续命
            if (需要续命) {
                lock.expire(30, TimeUnit.SECONDS);  // 手动续命30秒
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new RuntimeException("获取锁被中断", e);
}

看门狗配置

Redisson 默认配置:

  • 续命间隔lockWatchdogTimeout = 30秒(默认值)
  • 续命时长:每次续命 30秒

自定义配置

Config config = new Config();
// 设置看门狗超时时间为60秒
config.setLockWatchdogTimeout(60000);  // 单位:毫秒
RedissonClient redissonClient = Redisson.create(config);

看门狗 vs 固定过期时间

特性 看门狗机制 固定过期时间
适用场景 业务执行时间不确定 业务执行时间可预估
自动续命 ✅ 是 ❌ 否
死锁风险 低(异常退出时自动释放) 低(固定时间后释放)
重复执行风险 高(超时后可能重复执行)
性能开销 略高(需要后台线程)
推荐使用 ✅ 业务时间不确定时 ✅ 业务时间确定且较短时

重试机制设计

设计原则

  1. 指数退避:重试间隔逐渐增加,避免系统过载
  2. 最大重试次数:防止无限重试
  3. 异常记录:记录最后一次异常,便于排查
  4. 中断处理:正确处理线程中断

实现示例

/**
 * 带重试机制的获取资源方法
 *
 * @param maxRetries 最大重试次数
 * @return 资源对象
 */
private String getResourceWithRetry(int maxRetries) {
    int retryCount = 0;
    Exception lastException = null;

    while (retryCount < maxRetries) {
        try {
            return tryGetResourceWithLock();
        } catch (Exception e) {
            lastException = e;
            retryCount++;

            if (retryCount < maxRetries) {
                // 指数退避:200ms, 400ms, 600ms...
                long sleepMs = 200L * retryCount;
                log.warn("第{}次尝试失败,{}ms后重试,错误: {}",
                        retryCount, sleepMs, e.getMessage());
                try {
                    Thread.sleep(sleepMs);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("获取资源被中断", ie);
                }
            }
        }
    }

    log.error("重试{}次后仍然失败", maxRetries);
    throw new RuntimeException("获取资源失败: " + (lastException != null ? lastException.getMessage() : "未知错误"));
}

重试策略对比

策略 公式 示例(3次重试) 适用场景
固定间隔 sleepMs = fixed 200ms, 200ms, 200ms 系统负载稳定
线性递增 sleepMs = base * retryCount 200ms, 400ms, 600ms 通用场景(推荐)
指数退避 sleepMs = base * 2^(retryCount-1) 200ms, 400ms, 800ms 高并发场景
随机退避 sleepMs = random(base, max) 150-250ms, 300-500ms 避免惊群效应

完整实现示例

标准模板代码

@Component
@Slf4j
public class ResourceManager {
    
    private static final String RESOURCE_CACHE_KEY = "resource:cache:key";
    private static final String RESOURCE_LOCK_KEY = "lock:resource:key";
    private static final int MAX_RETRIES = 3;
    
    @Resource
    private RedissonClient redissonClient;
    
    /**
     * 获取资源(带缓存和重试机制)
     *
     * @param forceRefresh 是否强制刷新
     * @return 资源对象
     */
    public String getResource(boolean forceRefresh) {
        // 1. 如果不是强制刷新,先从缓存读取
        if (!forceRefresh) {
            RBucket<String> bucket = redissonClient.getBucket(RESOURCE_CACHE_KEY);
            String cachedResource = bucket.get();
            if (StringUtils.isNotBlank(cachedResource)) {
                log.debug("资源从缓存获取成功");
                return cachedResource;
            }
        }
        
        // 2. 使用分布式锁获取资源,增加重试机制
        return getResourceWithRetry(forceRefresh, MAX_RETRIES);
    }
    
    /**
     * 带重试机制的获取资源
     *
     * @param forceRefresh 是否强制刷新
     * @param maxRetries 最大重试次数
     * @return 资源对象
     */
    private String getResourceWithRetry(boolean forceRefresh, int maxRetries) {
        int retryCount = 0;
        Exception lastException = null;
        
        while (retryCount < maxRetries) {
            try {
                return tryGetResourceWithLock(forceRefresh);
            } catch (Exception e) {
                lastException = e;
                retryCount++;
                
                if (retryCount < maxRetries) {
                    long sleepMs = 200L * retryCount; // 递增延迟
                    log.warn("第{}次尝试失败,{}ms后重试,错误: {}",
                            retryCount, sleepMs, e.getMessage());
                    try {
                        Thread.sleep(sleepMs);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException("获取资源被中断", ie);
                    }
                }
            }
        }
        
        log.error("重试{}次后仍然失败", maxRetries);
        throw new RuntimeException("获取资源失败: " +
                (lastException != null ? lastException.getMessage() : "未知错误"));
    }
    
    /**
     * 使用分布式锁尝试获取资源
     *
     * @param forceRefresh 是否强制刷新
     * @return 资源对象
     */
    private String tryGetResourceWithLock(boolean forceRefresh) {
        RLock lock = redissonClient.getLock(RESOURCE_LOCK_KEY);
        try {
            // 方案1:使用固定过期时间(适合业务时间可预估的场景)
            boolean locked = lock.tryLock(15, 30, TimeUnit.SECONDS);
            
            // 方案2:使用看门狗机制(适合业务时间不确定的场景)
            // boolean locked = lock.tryLock(15, -1, TimeUnit.SECONDS);
            
            if (!locked) {
                log.warn("获取分布式锁超时");
                
                // 锁获取失败时,再次尝试从缓存读取
                if (!forceRefresh) {
                    RBucket<String> bucket = redissonClient.getBucket(RESOURCE_CACHE_KEY);
                    String cachedResource = bucket.get();
                    if (StringUtils.isNotBlank(cachedResource)) {
                        log.info("锁超时后从缓存获取到资源");
                        return cachedResource;
                    }
                }
                
                throw new RuntimeException("获取分布式锁超时,可能系统繁忙");
            }
            
            try {
                // 3. 双重检查,避免重复获取
                if (!forceRefresh) {
                    RBucket<String> bucket = redissonClient.getBucket(RESOURCE_CACHE_KEY);
                    String cachedResource = bucket.get();
                    if (StringUtils.isNotBlank(cachedResource)) {
                        log.info("获取锁后从缓存获取到资源(双重检查)");
                        return cachedResource;
                    }
                }
                
                // 4. 执行业务逻辑(获取资源)
                String resource = fetchResourceFromSource();
                
                // 5. 写入缓存
                if (StringUtils.isNotBlank(resource)) {
                    RBucket<String> bucket = redissonClient.getBucket(RESOURCE_CACHE_KEY);
                    bucket.set(resource, 3600, TimeUnit.SECONDS); // 缓存1小时
                    log.info("资源已缓存");
                }
                
                return resource;
                
            } finally {
                // 6. 释放锁(确保只释放当前线程持有的锁)
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                    log.debug("锁已释放");
                }
            }
            
        } catch (InterruptedException e) {
            log.error("获取锁时被中断", e);
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取资源被中断", e);
        }
    }
    
    /**
     * 从数据源获取资源(业务逻辑)
     */
    private String fetchResourceFromSource() {
        // 实现具体的业务逻辑
        // 例如:调用外部API、查询数据库等
        return "resource_data";
    }
}

最佳实践与参数推荐

lock.tryLock() 参数推荐

场景1:业务执行时间可预估(< 30秒)
// 推荐配置
boolean locked = lock.tryLock(15, 30, TimeUnit.SECONDS);

参数说明

  • waitTime = 15秒:等待时间适中,避免长时间阻塞
  • leaseTime = 30秒:根据业务最大执行时间设置,建议设置为 业务最大执行时间 * 1.5
  • 适用场景:Token刷新、缓存预热、数据同步等
场景2:业务执行时间不确定
// 推荐配置:使用看门狗机制
boolean locked = lock.tryLock(15, -1, TimeUnit.SECONDS);
// 或者
lock.lock();  // 阻塞等待,启用看门狗

参数说明

  • waitTime = 15秒:等待时间
  • leaseTime = -1:不设置过期时间,启用看门狗
  • 适用场景:复杂计算、批量处理、长时间任务等
场景3:高并发场景
// 推荐配置:缩短等待时间,避免线程堆积
boolean locked = lock.tryLock(5, 20, TimeUnit.SECONDS);

参数说明

  • waitTime = 5秒:快速失败,避免线程堆积
  • leaseTime = 20秒:根据实际业务时间设置
  • 适用场景:秒杀、限流、高频接口等

参数选择决策树

业务执行时间是否可预估?
├─ 是
│  ├─ 执行时间 < 10秒  → waitTime=10s, leaseTime=15s
│  ├─ 执行时间 10-30秒 → waitTime=15s, leaseTime=30s
│  └─ 执行时间 > 30秒  → 考虑使用看门狗机制
│
└─ 否
   └─ 使用看门狗机制 → waitTime=15s, leaseTime=-1

通用推荐值

业务类型 waitTime leaseTime 是否看门狗 说明
Token刷新 15s 30s 通常很快完成
缓存预热 10s 20s 数据加载较快
数据同步 30s 60s 中等耗时操作
批量处理 15s -1 时间不确定
复杂计算 15s -1 时间不确定
外部API调用 15s 30s 有超时控制

重试机制参数推荐

场景 最大重试次数 初始延迟 退避策略 说明
高可用要求 5 200ms 线性递增 提高成功率
快速失败 2 100ms 固定间隔 快速响应
网络不稳定 3 300ms 指数退避 适应网络波动
通用场景 3 200ms 线性递增 推荐配置

常见问题与解决方案

Q1: 锁获取失败后应该如何处理?

问题tryLock() 返回 false 时,业务逻辑无法执行。

解决方案

  1. 降级策略:尝试从缓存读取(如示例代码)
  2. 重试机制:外层增加重试逻辑
  3. 快速失败:直接返回错误,由调用方处理

Q2: 如何避免死锁?

问题:业务异常导致锁未释放。

解决方案

  1. ✅ 使用 try-finally 确保锁释放
  2. ✅ 使用 lock.isHeldByCurrentThread() 检查
  3. ✅ 设置合理的 leaseTime 或使用看门狗
  4. ✅ 避免在锁内调用可能阻塞的方法

Q3: 锁被其他线程释放怎么办?

问题:线程A的锁被线程B释放。

解决方案

// ✅ 正确:检查锁的持有者
if (lock.isHeldByCurrentThread()) {
    lock.unlock();
}

// ❌ 错误:直接释放
lock.unlock();  // 可能释放其他线程的锁

Q4: 如何监控锁的使用情况?

解决方案

  1. 日志记录:记录锁获取/释放时间
  2. 指标监控:统计锁等待时间、持有时间
  3. 告警机制:锁等待时间过长时告警
StopWatch stopWatch = StopWatch.createStarted();
boolean locked = lock.tryLock(15, 30, TimeUnit.SECONDS);
long waitTime = stopWatch.getTime();
if (waitTime > 5000) {
    log.warn("锁等待时间过长: {}ms", waitTime);
}

Q5: 分布式锁的性能影响?

优化建议

  1. 减少锁粒度:只锁必要的资源
  2. 缩短持有时间:尽快释放锁
  3. 使用本地锁:单机场景优先使用 synchronized
  4. 缓存降级:锁获取失败时使用缓存

总结

核心要点

  1. 双重检查:减少锁竞争,提高性能
  2. 重试机制:提高系统容错能力
  3. 合理参数:根据业务场景选择 waitTimeleaseTime
  4. 看门狗机制:业务时间不确定时使用
  5. 异常处理:确保锁正确释放

选择建议

  • 业务时间可预估 → 使用固定 leaseTime
  • 业务时间不确定 → 使用看门狗机制
  • 高并发场景 → 缩短 waitTime,快速失败
  • 高可用要求 → 增加重试次数,使用降级策略

参考资源

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐