Skip to content

分布式锁 ​

这篇解决什么 ​

多实例同时跑同一段写逻辑时,只让一个线程进临界区。读完能改 Redisson RLock、会在方法上加 @Lock4j,并分清租约时间和解锁时机。

默认连本机 Redis 127.0.0.1:6379,库号 0。管理端前缀仍是 /admin-api。锁和缓存共用这套连接,不必再开一套 Redis。

示意图:Service 调 DAO 或 @Lock4j;框架用 Redisson 在 Redis 上加锁。

组件位置 ​

名称说明仓库路径
Protection Starter分布式锁、幂等、限流ruoyi-office/yudao-framework/yudao-spring-boot-starter-protection/
Redis Starter提供 RedissonClient,锁不再单独配连接ruoyi-office/yudao-framework/yudao-spring-boot-starter-redis/
Lock4jlock4j-redisson-spring-boot-starter,BOM 2.2.7yudao-dependencies/pom.xml
失败策略抢锁失败抛 ServiceException(423).../lock4j/core/DefaultLockFailureStrategy.java
本地超时acquire-timeout / expireruoyi-office/yudao-server/src/main/resources/application-local.yaml
单体入口引入 protection Starterruoyi-office/yudao-server/pom.xml

编程式锁直接注入 RedissonClient。声明式锁用 @Lock4j,类路径上要有 com.baomidou.lock.annotation.Lock4j;protection 的 pom 把它标成 optional,编译找不到注解时在该模块补同一坐标。

编程式锁 ​

application-local.yaml 只配 Redis 地址。Redisson 跟 Spring Data Redis 共用这三项,锁这边没有第二套 host。

yaml
spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      database: 0

redissonClient.getLock(key) 得到可重入锁,Value 是 Hash。需要红锁、读写锁时换 getRedLock / getReadWriteLock,支付和结账用的都是普通 RLock。

支付通知 ​

每个通知任务同一时刻只跑一条。Key 和租约写在支付模块,不写死在 Service 里。

名称说明仓库路径
pay_notify:lock:%d按任务 id 一把锁yudao-module-pay-server/.../dal/redis/RedisKeyConstants.java
PayNotifyLockRedisDAOlock(id, timeout, runnable).../dal/redis/notify/PayNotifyLockRedisDAO.java
PayNotifyServiceImpl加锁后再比对 notifyTimes.../service/notify/PayNotifyServiceImpl.java
租约NOTIFY_TIMEOUT_MILLIS,120 秒同上
java
public void lock(Long id, Long timeoutMillis, Runnable runnable) {
    String lockKey = formatKey(id);
    RLock lock = redissonClient.getLock(lockKey);
    try {
        lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
        runnable.run();
    } finally {
        lock.unlock();
    }
}

lock(leaseTime, unit) 会一直等到拿到锁,租约是 120 秒,到期不续期。业务比 120 秒长,别的线程会抢到同一把锁。

java
public void executeNotify(PayNotifyTaskDO task) {
    notifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> {
        PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId());
        if (ObjectUtil.notEqual(task.getNotifyTimes(), dbTask.getNotifyTimes())) {
            return;
        }
        getSelf().executeNotify0(dbTask);
    });
}

两个调度同时满足通知条件时,后拿到锁的那个仍可能进来。所以锁内再读库,notifyTimes 对不上就放弃。锁只保证互斥,不保证「只通知一次」。

示意图:先加锁,再按次数判断;finally 里解锁。

钱包与结账 ​

同一套 RLock,租约和等待策略按业务选。

名称说明仓库路径
pay_wallet:lock:%d创建按 userId,改余额按 walletIdPayWalletLockRedisDAO
钱包租约UPDATE_TIMEOUT_MILLIS,120 秒PayWalletServiceImpl
finance:close:{}按账套 bookIdFinanceCloseServiceImpl
结账tryLock(0, 5, MINUTES),抢不到抛 CLOSE_LOCK_FAIL同上
Stream 清理redis:stream:message-cleanup:lockRedisStreamMessageCleanupJob
IoT 清理…:lock:iot,不能和业务清理抢同一把同上
消息补发tryLock(),没抢到就跳过本轮RedisPendingMessageResendJob

钱包创建是双重检查:外面先查一次,没有再锁内查一次再 insert。结账 tryLock 的等待是 0,避免第二个操作员堵在锁上。

定时 Job 用 tryLock() 无参:拿不到就记 debug 并返回。解锁前看 isHeldByCurrentThread(),避免租约过期后误解别人的锁。

编程式还是 @Lock4j

要自己选 lock / tryLock、红锁、读写锁:用 RedissonClient。 按方法拦截、失败直接回 423:用 @Lock4j。 拿到锁还要再读库判断状态:支付通知这种写法更合适。

热点缓存回源用的短 TTL SETNX(例如天气)不是可重入业务锁,见 Redis 与本地缓存。

声明式锁 ​

YudaoLock4jConfiguration 写在 LockAutoConfiguration 前面,注册 DefaultLockFailureStrategy。只有类路径存在 @Lock4j 时才生效。

yaml
lock4j:
  acquire-timeout: 3000
  expire: 30000

单位是毫秒:最多等 3 秒抢锁;拿到后租约 30 秒。方法上的 expire / acquireTimeout 覆盖这两项。

名称说明仓库路径
@Lock4j方法进出自动加解锁,keys 支持 Spring ELLock4j
lock4j:%s声明式锁的 Key 形态,方便在 Redis 里按前缀找Lock4jRedisKeyConstants.LOCK4J
失败GlobalErrorCodeConstants.LOCKED,code 423yudao-common/.../GlobalErrorCodeConstants.java
文案「请求失败,请稍后重试」同上
java
@Lock4j
public void simple() {
    // 用 yaml 里的 acquire-timeout / expire
}

@Lock4j(keys = {"#userId"}, expire = 60000, acquireTimeout = 1000)
public void byUser(Long userId) {
    // Key 带 userId;租约 60 秒,最多等 1 秒
}

抢不到锁时 DefaultLockFailureStrategy.onLockFailure 打 debug,再抛 ServiceException。前端按 code !== 0 提示即可,不要再包一层业务错误码。

防重复提交用 @Idempotent:成功后 Key 还在,一段时间内同参不能再进。成功就删 Key 变成互斥,那种场景用 @Lock4j,不要把幂等当锁使。

持有时间与解锁 ​

租约到期不会替你续命

RLock.lock(leaseTime, unit) 和 Lock4j 的 expire 都是租约。临界区比租约长,锁会先掉,别的线程会进来。 支付通知、钱包按 120 秒估;结账按 5 分钟。估不准就加长,或改用不传 lease 的 lock() 走看门狗续期。

finally 里先确认是自己的锁

unlock 必须进 finally。租约已过或 tryLock 失败时,Redisson 再 unlock 会报错。 FinanceCloseServiceImpl、RedisStreamMessageCleanupJob 先判断 isHeldByCurrentThread()。 PayNotifyLockRedisDAO 的 lock() 会阻塞到拿到锁,再在 finally 里直接 unlock。

本机连不上 127.0.0.1:6379 时,先看 Redis 进程和 application-local.yaml,不要改成别的环境的地址。

配置与操作 ​

互斥方法加 @Lock4j,Key 用 SpEL。本机必须能连 127.0.0.1:6379。解锁放 finally。

开启见 框架层。

相关篇 ​

联系我们

获取报价、演示和二开方案

微信咨询二维码

微信咨询

17156169080

添加时备注「RuoYi Office」

在线体验商业版