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