分布式锁/进程锁 设计方案

分布式锁/进程锁设计方案

基于 Redisson / JDK ReentrantLock 的分层锁机制,支持单体到分布式的平滑演进。


1. 设计目标

目标 说明
平滑演进 单体阶段不用 Redis,分布式阶段切配置即可,业务代码零改动
接口统一 进程锁和分布式锁对外暴露相同的 API
声明式 + 编程式 支持注解 @DistributedLock,也支持编程式调用
可配置 等待时间、租约、锁类型、失败策略均可自定义
防遗漏 强制回调式执行,确保锁在 finally 中释放

2. 核心抽象

2.1 锁模板接口

所有锁实现的顶层接口:

java 复制代码
public interface LockTemplate {
    <T> T execute(LockInfo lockInfo, Supplier<T> callback);
    void execute(LockInfo lockInfo, Runnable callback);
}

2.2 锁类型枚举

java 复制代码
public enum LockType {
    /** 可重入锁(默认) */
    REENTRANT,
    /** 公平锁 */
    FAIR,
    /** 联锁(同时获取多个锁) */
    MULTI_LOCK,
    /** 读写锁 - 读 */
    READ_LOCK,
    /** 读写锁 - 写 */
    WRITE_LOCK
}

2.3 锁失败策略

java 复制代码
public enum LockFailStrategy {
    /** 抛异常(默认) */
    THROW_EXCEPTION,
    /** 返回 null */
    RETURN_NULL,
    /** 静默跳过 */
    SKIP
}

2.4 锁信息对象

java 复制代码
@Data
@Builder
public class LockInfo {

    /** 锁名称(必填) */
    private String name;

    /** 锁类型,默认可重入锁 */
    @Builder.Default
    private LockType type = LockType.REENTRANT;

    /** 等待时间(秒),默认 30 秒 */
    @Builder.Default
    private long waitTime = 30L;

    /**
     * 租约时间(秒)
     * -1  = 启用看门狗自动续期(推荐)
     * >0  = 固定时长,不续期(适合已知耗时的操作)
     */
    @Builder.Default
    private long leaseTime = -1L;

    /** 获取失败策略,默认抛异常 */
    @Builder.Default
    private LockFailStrategy failStrategy = LockFailStrategy.THROW_EXCEPTION;
}

注意@Builder.Default 必须标注,否则 Lombok @Builder 会忽略字段初始化表达式。


3. 进程锁实现(JVM 内)

适用于单体部署,零外部依赖。

java 复制代码
@Slf4j
@Component
public class JdkLockTemplate implements LockTemplate {

    private final ConcurrentHashMap<String, LockWrapper> locks = new ConcurrentHashMap<>();

    @Override
    public <T> T execute(LockInfo lockInfo, Supplier<T> callback) {
        LockWrapper wrapper = getWrapper(lockInfo);
        Lock lock = getLock(lockInfo, wrapper);

        boolean acquired = false;
        long startTime = System.currentTimeMillis();

        try {
            acquired = lock.tryLock(lockInfo.getWaitTime(), TimeUnit.SECONDS);
            if (!acquired) {
                return handleFail(lockInfo);
            }
            return callback.get();

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new LockException("获取锁被中断: " + lockInfo.getName());
        } finally {
            if (acquired) {
                release(lockInfo, wrapper, lock, startTime);
            }
        }
    }

    @Override
    public void execute(LockInfo lockInfo, Runnable callback) {
        execute(lockInfo, () -> {
            callback.run();
            return null;
        });
    }

    private LockWrapper getWrapper(LockInfo lockInfo) {
        return locks.computeIfAbsent(lockInfo.getName(), k -> {
            boolean fair = lockInfo.getType() == LockType.FAIR;
            return new LockWrapper(
                new ReentrantLock(fair),
                new ReentrantReadWriteLock(fair)
            );
        });
    }

    private Lock getLock(LockInfo lockInfo, LockWrapper wrapper) {
        return switch (lockInfo.getType()) {
            case REENTRANT, FAIR, MULTI_LOCK -> wrapper.reentrantLock();
            case READ_LOCK -> wrapper.readWriteLock().readLock();
            case WRITE_LOCK -> wrapper.readWriteLock().writeLock();
        };
    }

    private void release(LockInfo lockInfo, LockWrapper wrapper, Lock lock, long startTime) {
        lock.unlock();

        // 无等待线程时清理,防止内存无限增长
        if (lockInfo.getType() == LockType.REENTRANT || lockInfo.getType() == LockType.FAIR) {
            ReentrantLock rl = wrapper.reentrantLock();
            if (!rl.hasQueuedThreads() && !rl.isLocked()) {
                locks.remove(lockInfo.getName(), wrapper);
            }
        }

        log.debug("[进程锁] 释放: {}, 持有{}ms", lockInfo.getName(),
            System.currentTimeMillis() - startTime);
    }

    private <T> T handleFail(LockInfo lockInfo) {
        return switch (lockInfo.getFailStrategy()) {
            case THROW_EXCEPTION -> throw new LockException("获取锁失败: " + lockInfo.getName());
            case RETURN_NULL -> null;
            case SKIP -> null;
        };
    }

    private record LockWrapper(
        ReentrantLock reentrantLock,
        ReentrantReadWriteLock readWriteLock
    ) {}
}

4. 分布式锁实现(Redisson)

java 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class RedissonLockTemplate implements LockTemplate {

    private final RedissonClient redissonClient;

    @Override
    public <T> T execute(LockInfo lockInfo, Supplier<T> callback) {
        RLock lock = getLock(lockInfo);
        boolean acquired = false;
        long startTime = System.currentTimeMillis();

        try {
            if (lockInfo.getLeaseTime() > 0) {
                acquired = lock.tryLock(
                    lockInfo.getWaitTime(),
                    lockInfo.getLeaseTime(),
                    TimeUnit.SECONDS
                );
            } else {
                acquired = lock.tryLock(
                    lockInfo.getWaitTime(),
                    -1,
                    TimeUnit.SECONDS
                );
            }

            if (!acquired) {
                return handleFail(lockInfo);
            }

            return callback.get();

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new LockException("获取锁被中断: " + lockInfo.getName(), e);
        } finally {
            if (acquired && lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.debug("[分布式锁] 释放: {}, 持有{}ms",
                    lockInfo.getName(), System.currentTimeMillis() - startTime);
            }
        }
    }

    @Override
    public void execute(LockInfo lockInfo, Runnable callback) {
        execute(lockInfo, () -> {
            callback.run();
            return null;
        });
    }

    private RLock getLock(LockInfo info) {
        return switch (info.getType()) {
            case REENTRANT -> redissonClient.getLock(info.getName());
            case FAIR -> redissonClient.getFairLock(info.getName());
            case READ_LOCK -> redissonClient.getReadWriteLock(info.getName()).readLock();
            case WRITE_LOCK -> redissonClient.getReadWriteLock(info.getName()).writeLock();
            case MULTI_LOCK -> throw new UnsupportedOperationException("请用 multiLock() 方法");
        };
    }

    private <T> T handleFail(LockInfo lockInfo) {
        return switch (lockInfo.getFailStrategy()) {
            case THROW_EXCEPTION -> throw new LockException("获取锁失败: " + lockInfo.getName());
            case RETURN_NULL -> null;
            case SKIP -> null;
        };
    }
}

5. 统一入口(配置驱动选择实现)

java 复制代码
@Slf4j
@Service
@RequiredArgsConstructor
public class LockService {

    private final LockProperties lockProperties;
    private final JdkLockTemplate jdkTemplate;
    private final Optional<RedissonLockTemplate> redissonTemplate;

    @PostConstruct
    public void init() {
        log.info("[Lock] 当前模式: {}", lockProperties.getMode());
    }

    public <T> T execute(LockInfo lockInfo, Supplier<T> callback) {
        return getTemplate().execute(lockInfo, callback);
    }

    public void execute(LockInfo lockInfo, Runnable callback) {
        getTemplate().execute(lockInfo, callback);
    }

    private LockTemplate getTemplate() {
        return switch (lockProperties.getMode()) {
            case PROCESS -> jdkTemplate;
            case DISTRIBUTED -> redissonTemplate.orElseThrow(() ->
                new IllegalStateException("未配置 Redisson,无法使用分布式锁")
            );
        };
    }
}

6. 配置项

6.1 application.yml

yaml 复制代码
lock:
  # 工作模式:process(进程锁) / distributed(分布式锁)
  mode: process

  # 各模式独有配置
  process:
    # 进程锁独享配置

  distributed:
    # Redisson 配置(略)
    server-address: redis://localhost:6379

6.2 配置类

java 复制代码
@Data
@ConfigurationProperties(prefix = "lock")
public class LockProperties {

    /** 工作模式 */
    private LockMode mode = LockMode.PROCESS;

    public enum LockMode {
        /** 进程锁 */
        PROCESS,
        /** 分布式锁 */
        DISTRIBUTED
    }
}

6.3 自动装配

java 复制代码
@Configuration
@EnableConfigurationProperties(LockProperties.class)
public class LockAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public JdkLockTemplate jdkLockTemplate() {
        return new JdkLockTemplate();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean(RedissonClient.class)
    public RedissonLockTemplate redissonLockTemplate(RedissonClient client) {
        return new RedissonLockTemplate(client);
    }
}

7. 使用示例

7.1 编程式(推荐复杂场景)

极简用法(只传锁名称)
java 复制代码
@Service
@RequiredArgsConstructor
public class OrderService {

    private final LockService lockService;

    public void deductStock(Long productId) {
        lockService.execute(
            LockInfo.builder().name("stock:" + productId).build(),
            () -> {
                // 扣减库存逻辑...
                stockMapper.deduct(productId);
            }
        );
    }
}
局部覆盖(只改关心的字段)
java 复制代码
// 防重复提交:不等待、5 秒租约
tonService.execute(
    LockInfo.builder()
        .name("form:" + formId)
        .waitTime(0)       // 覆盖默认:不等待
        .leaseTime(5)      // 覆盖默认:固定 5 秒
        .build(),
    () -> { /* 提交逻辑 */ }
);
模板常量(团队复用)
java 复制代码
public class LockTemplates {

    /** 防重复提交 */
    public static LockInfo idempotent(String bizId) {
        return LockInfo.builder()
            .name("idem:" + bizId)
            .waitTime(0)
            .leaseTime(5)
            .failStrategy(LockFailStrategy.SKIP)
            .build();
    }

    /** 用户级互斥 */
    public static LockInfo userMutex(Long userId) {
        return LockInfo.builder()
            .name("user:" + userId)
            .waitTime(10)
            .leaseTime(-1)  // 看门狗
            .build();
    }

    /** 库存扣减(公平锁) */
    public static LockInfo stockDeduction(Long skuId) {
        return LockInfo.builder()
            .name("stock:" + skuId)
            .type(LockType.FAIR)
            .waitTime(3)
            .leaseTime(10)
            .build();
    }
}

使用:

java 复制代码
lockService.execute(LockTemplates.idempotent(formId), () -> {
    // 提交表单...
});

lockService.execute(LockTemplates.stockDeduction(skuId), () -> {
    // 扣减库存...
});

7.2 声明式(AOP 注解)

java 复制代码
// 注解定义
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
    /** 锁名称(支持 SpEL) */
    String key();
    LockType type() default LockType.REENTRANT;
    long waitTime() default 30;
    long leaseTime() default -1;
    LockFailStrategy failStrategy() default LockFailStrategy.THROW_EXCEPTION;
    String prefix() default "lock:";
    String message() default "系统繁忙,请稍后再试";
}
java 复制代码
@Aspect
@Component
@RequiredArgsConstructor
public class DistributedLockAspect {

    private final LockService lockService;

    @Around("@annotation(lock)")
    public Object around(ProceedingJoinPoint point, DistributedLock lock) throws Throwable {
        String lockName = resolveKey(point, lock);

        LockInfo lockInfo = LockInfo.builder()
            .name(lockName)
            .type(lock.type())
            .waitTime(lock.waitTime())
            .leaseTime(lock.leaseTime())
            .failStrategy(lock.failStrategy())
            .build();

        return lockService.execute(lockInfo, () -> {
            try {
                return point.proceed();
            } catch (Throwable e) {
                throw new LockException("业务执行异常", e);
            }
        });
    }

    private String resolveKey(ProceedingJoinPoint point, DistributedLock lock) {
        // SpEL 解析...
        return lock.prefix() + "parsedKey";
    }
}
使用示例
java 复制代码
@RestController
@RequiredArgsConstructor
public class CouponController {

    /** SpEL 动态 key */
    @DistributedLock(
        key = "'coupon:' + #couponId + ':user:' + #userId",
        waitTime = 5,
        message = "请勿重复领取"
    )
    @PostMapping("/coupons/{couponId}/claim")
    public Result claim(@PathVariable Long couponId, @RequestParam Long userId) {
        // 领券逻辑...
        return Result.ok();
    }

    /** MQ 幂等消费 */
    @DistributedLock(
        key = "'mq:' + #messageId",
        waitTime = 0,
        leaseTime = 300,
        failStrategy = LockFailStrategy.SKIP
    )
    @KafkaListener(topics = "order-topic")
    public void consume(Message message, @Header("id") String messageId) {
        // 消费逻辑...
    }
}

8. 两套实现对照

特性 进程锁 (JdkLockTemplate) 分布式锁 (RedissonLockTemplate)
外部依赖 Redis
跨进程安全
waitTime tryLock(timeout) 支持 tryLock(timeout) 支持
leaseTime ❌ 无效(无自动续期需求) ✅ 支持,-1 启用看门狗
公平锁 new ReentrantLock(true) getFairLock()
读写锁 ReentrantReadWriteLock getReadWriteLock()
锁释放 线程死亡 JVM 自动释放 看门狗 / leaseTime 到期释放
性能 极高(内存内操作) 毫秒级(网络 RTT)

9. 演进路径

阶段一:单体部署

yaml 复制代码
# application.yml
lock:
  mode: process
  • 不安装 Redis
  • JdkLockTemplate 完全满足单机多线程并发控制
  • 零外部依赖

阶段二:水平扩展

yaml 复制代码
# application.yml
lock:
  mode: distributed
  • 安装 Redis
  • RedissonClient 自动注入
  • 所有锁自动切换为分布式锁
  • 业务代码一行不改
java 复制代码
// 同样的代码------
lockService.execute(
    LockInfo.builder().name("stock:" + productId).build(),
    () -> { /* ... */ }
);
// 单体时走进程锁 → 分布式时走 Redisson

10. 设计要点总结

设计决策 原因
抽象 LockTemplate 接口 让进程锁和分布式锁对外暴露相同的 API
LockInfo 默认值 减少重复代码,每个锁只需要关心 name
回调式执行 强制在 finally 中释放锁,防止遗漏
@Builder.Default Lombok 按需,否则默认值不被 @Builder 保留
配置驱动选择实现 Spring 注入 Optional<RedissonLockTemplate>,无 Redis 时不报错
命名锁池 进程锁用 ConcurrentHashMap 按名称管理 ReentrantLock
联锁排序 多锁场景按名称排序获取,防止死锁
看门狗 vs 固定租约 -1 走看门狗(适合大多数场景),>0 固定不续期(适合已知耗时的操作)

11. 注意事项

  1. 锁粒度要细 :如 user:123 而非 user,避免串行执行
  2. 避免锁内做 RPC:网络波动会导致看门狗误判为死亡
  3. leaseTime = -1 时 :Redisson 看门狗每隔 lockWatchdogTimeout / 3 续期(默认 10s 检查一次)
  4. 线程池场景 :如果锁内用的是线程池,当前线程解锁后子线程可能未完成(看门狗续期会停),应该显式设置 leaseTime
  5. 锁名称唯一:不同业务使用不同的前缀,避免命名冲突