分布式锁

返回 Spring Boot 基础

分布式锁用于在多实例部署环境中保证同一时刻只有一个节点执行某段逻辑,解决本地 synchronized / ReentrantLock 无法跨 JVM 的问题。


方案对比

方案可靠性性能实现复杂度适用场景
Redis SETNX(手写)简单场景,允许偶发问题
Redisson生产首选
ZooKeeper很高强一致性要求
数据库乐观锁低并发,已有数据库
数据库悲观锁低并发,强一致

方案一:Redisson(生产推荐)

Redisson 基于 Redis 实现了完整的分布式锁语义,包含看门狗(Watchdog)自动续期、可重入、公平锁等特性。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.27.2</version>
</dependency>
spring:
  data:
    redis:
      host: localhost
      port: 6379

基本用法

@Service
public class OrderService {
 
    private final RedissonClient redissonClient;
 
    public void createOrder(String userId) {
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        try {
            // 尝试加锁,最多等待 3 秒,持有 30 秒后自动释放
            boolean acquired = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!acquired) {
                throw new BizException("操作频繁,请稍后重试");
            }
            doCreateOrder(userId);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

看门狗机制

不指定 leaseTime(或传 -1)时,Redisson 启用看门狗:锁默认持有 30 秒,每隔 10 秒自动续期,直到业务方法返回:

lock.lock();                        // 无超时,看门狗自动续期
try {
    doLongRunningTask();
} finally {
    lock.unlock();
}

指定了 leaseTime 的锁不启用看门狗,到期自动释放,适合已知执行时长上界的场景。

其他锁类型

// 可重入锁(同一线程可多次加锁)
RLock lock = redissonClient.getLock("reentrant-lock");
 
// 公平锁(FIFO 顺序)
RLock fairLock = redissonClient.getFairLock("fair-lock");
 
// 联锁(同时锁定多个资源,防死锁)
RLock lock1 = redissonClient.getLock("lock:account:1");
RLock lock2 = redissonClient.getLock("lock:account:2");
RLock multiLock = redissonClient.getMultiLock(lock1, lock2);
 
// 读写锁
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rw-lock");
rwLock.readLock().lock();   // 读操作加读锁(允许并发读)
rwLock.writeLock().lock();  // 写操作加写锁(独占)

方案二:Redis SETNX 手写

适合理解原理或依赖轻量化场景,需手动处理续期、误删等问题:

@Component
public class RedisDistributedLock {
 
    private final StringRedisTemplate redisTemplate;
    private static final String LOCK_PREFIX = "lock:";
 
    public boolean tryLock(String key, String value, long expireSeconds) {
        Boolean result = redisTemplate.opsForValue()
            .setIfAbsent(LOCK_PREFIX + key, value,
                         expireSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }
 
    // 释放时校验 value,防止误删其他线程的锁(Lua 脚本保证原子性)
    public boolean releaseLock(String key, String value) {
        String script = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
            """;
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            List.of(LOCK_PREFIX + key),
            value
        );
        return Long.valueOf(1).equals(result);
    }
}
// 使用
String lockValue = UUID.randomUUID().toString();
boolean locked = lock.tryLock("order:" + userId, lockValue, 30);
if (!locked) throw new BizException("请稍后重试");
try {
    doCreateOrder(userId);
} finally {
    lock.releaseLock("order:" + userId, lockValue);
}

手写方案的主要缺陷:无自动续期,业务超时则锁提前释放;高并发下存在 TOCTOU 竞态(已用 Lua 原子释放解决误删问题)。


方案三:注解驱动(AOP 封装)

通过自定义注解 + AOP 切面,将锁逻辑与业务代码解耦:

// 1. 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    String key();                       // 支持 SpEL,如 "#userId"
    long waitTime() default 3;
    long leaseTime() default 30;
    TimeUnit unit() default TimeUnit.SECONDS;
    String message() default "操作频繁,请稍后重试";
}
 
// 2. AOP 切面
@Aspect
@Component
public class DistributedLockAspect {
 
    private final RedissonClient redissonClient;
    private final ExpressionParser parser = new SpelExpressionParser();
 
    @Around("@annotation(dl)")
    public Object around(ProceedingJoinPoint pjp,
                         DistributedLock dl) throws Throwable {
        String key = resolveKey(pjp, dl.key());
        RLock lock = redissonClient.getLock("lock:" + key);
        boolean acquired = lock.tryLock(dl.waitTime(), dl.leaseTime(), dl.unit());
        if (!acquired) throw new BizException(dl.message());
        try {
            return pjp.proceed();
        } finally {
            if (lock.isHeldByCurrentThread()) lock.unlock();
        }
    }
 
    private String resolveKey(ProceedingJoinPoint pjp, String keyExpr) {
        if (!keyExpr.contains("#")) return keyExpr;
        MethodSignature sig = (MethodSignature) pjp.getSignature();
        EvaluationContext ctx = new MethodBasedEvaluationContext(
            null, sig.getMethod(), pjp.getArgs(),
            new DefaultParameterNameDiscoverer());
        return parser.parseExpression(keyExpr).getValue(ctx, String.class);
    }
}
 
// 3. 使用
@DistributedLock(key = "'order:' + #userId", waitTime = 3, leaseTime = 30)
public void createOrder(String userId) {
    doCreateOrder(userId);
}

定时任务场景

防止多实例重复执行定时任务,详见 定时任务

@Scheduled(cron = "0 0 3 * * ?")
public void nightlyCleanup() {
    RLock lock = redissonClient.getLock("task:nightly-cleanup");
    if (!lock.tryLock()) return;             // 其他实例已在执行
    try {
        doCleanup();
    } finally {
        lock.unlock();
    }
}

接口幂等性

分布式锁也常用于实现接口幂等,详见 接口幂等性

@DistributedLock(key = "'idempotent:' + #req.requestId", leaseTime = 10)
public OrderResponse createOrder(CreateOrderRequest req) {
    // 锁内执行,相同 requestId 的并发请求只有一个能进入
    if (orderRepo.existsByRequestId(req.getRequestId())) {
        return orderRepo.findByRequestId(req.getRequestId());
    }
    return doCreate(req);
}

常见问题

问题原因解决方案
锁过期业务未完成未续期Redisson 看门狗自动续期
释放了别人的锁未校验持有者value 带线程标识,原子 Lua 脚本释放
Redis 主从切换锁丢失主节点崩溃前未同步RedLock 算法(多主节点)或业务补偿
死锁业务异常未释放finally 块释放;设置合理 leaseTime
锁粒度太粗整资源加锁细化 key(如按用户 ID / 订单 ID)

相关链接