跳转到内容

分布式锁(Redisson)


单机环境下,synchronizedReentrantLock 可以保证线程安全。但在分布式部署时,多个服务实例各自的 JVM 锁互不感知,无法保证跨实例的互斥访问。

单机(JVM 锁有效):
实例 A ──▶ synchronized(this) ──▶ 安全
分布式(JVM 锁失效):
实例 A ──▶ synchronized(this) ──┐
├──▶ 同时操作同一资源 ❌
实例 B ──▶ synchronized(this) ──┘
(两个 JVM 的锁互不影响)

分布式锁需要一个所有实例共享的外部存储来协调,Redis 是最常用的选择。

直接用 SET NX EX 实现分布式锁,存在诸多边界问题:

// 看似简单,实则问题很多
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent("lock:order", "1", 30, TimeUnit.SECONDS);
问题描述
锁超时续期业务执行超过 30 秒,锁自动过期,其他实例获得锁,产生并发
误删他人锁A 的锁过期后 B 获得锁,A 执行完后把 B 的锁删了
删锁非原子判断锁是自己的 + 删除锁,两步非原子,中间可能被抢占
可重入性同一线程再次获取同一把锁会死锁
主从切换丢锁Master 写锁后宕机,Slave 提升为 Master,锁丢失

Redisson 将以上问题全部解决,生产环境应直接使用 Redisson。


Redisson 是基于 Redis 的 Java 分布式组件库,提供了分布式锁、信号量、限流器等丰富的分布式工具,底层使用 Lua 脚本保证原子性,并通过 WatchDog(看门狗) 机制自动续期。

Redisson 分布式锁核心特性:
├── 自动续期(WatchDog):锁未释放前每隔 leaseTime/3 自动续期
├── 可重入:同一线程可多次获取同一把锁
├── 锁所有权校验:只有加锁线程才能释放锁
└── Lua 脚本保证原子性:加锁/释放锁均为原子操作
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.0</version>
</dependency>
spring:
data:
redis:
host: localhost
port: 6379
password: 123456

Redisson Starter 会自动读取 Spring Redis 配置创建 RedissonClient Bean,无需额外配置。

如需自定义配置:

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("123456")
.setConnectionPoolSize(64)
.setConnectionMinimumIdleSize(10);
return Redisson.create(config);
}

@Autowired
private RedissonClient redissonClient;
public void createOrder(Long orderId) {
RLock lock = redissonClient.getLock("lock:order:" + orderId);
lock.lock(); // 加锁(默认 leaseTime = -1,WatchDog 自动续期)
try {
// 业务逻辑
doCreateOrder(orderId);
} finally {
lock.unlock(); // 释放锁(必须在 finally 中)
}
}

5.2. 指定锁超时时间(关闭 WatchDog)

Section titled “5.2. 指定锁超时时间(关闭 WatchDog)”
// leaseTime > 0 时 WatchDog 不工作,到时间自动释放
lock.lock(10, TimeUnit.SECONDS);
// waitTime:等待获取锁的最长时间
// leaseTime:锁的持有时间(-1 = WatchDog 自动续期)
boolean acquired = lock.tryLock(3, -1, TimeUnit.SECONDS);
if (acquired) {
try {
doCreateOrder(orderId);
} finally {
lock.unlock();
}
} else {
// 未获得锁,直接返回或抛异常
throw new RuntimeException("系统繁忙,请稍后重试");
}
RLock lock = redissonClient.getLock("lock:order:1001");
lock.lock(); // 加锁 1 次,计数 = 1
try {
lock.lock(); // 同一线程再次加锁,计数 = 2,不会死锁
try {
doWork();
} finally {
lock.unlock(); // 释放 1 次,计数 = 1
}
} finally {
lock.unlock(); // 再释放 1 次,计数 = 0,锁完全释放
}

WatchDog 工作流程:
lock.lock() 加锁成功(默认 leaseTime = 30 秒)
WatchDog 启动定时任务(每 10 秒执行一次,即 30/3)
├── 检查锁是否仍被当前线程持有
│ │
│ ├── 是 ──▶ 重置过期时间为 30 秒(续期)
│ └── 否 ──▶ 停止续期任务
lock.unlock() 释放锁 ──▶ WatchDog 停止

Redis 中锁的数据结构(Hash):

key: lock:order:1001
type: Hash
field: <UUID>:<线程ID> ← 标识锁的持有者(UUID 区分实例,线程ID 支持可重入)
value: 2 ← 重入次数
ttl: 30s ← 每次续期重置

按请求顺序依次获取锁,防止线程饥饿:

RLock fairLock = redissonClient.getFairLock("lock:order");
fairLock.lock();

读读不互斥,读写/写写互斥,适合读多写少场景:

RReadWriteLock rwLock = redissonClient.getReadWriteLock("lock:config");
// 读操作(共享锁)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
readConfig();
} finally {
readLock.unlock();
}
// 写操作(独占锁)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
updateConfig();
} finally {
writeLock.unlock();
}

同时锁住多个资源,全部加锁成功才算获得锁:

RLock lock1 = redissonClient.getLock("lock:account:A");
RLock lock2 = redissonClient.getLock("lock:account:B");
RLock multiLock = redissonClient.getMultiLock(lock1, lock2);
multiLock.lock();
try {
transfer(accountA, accountB, amount);
} finally {
multiLock.unlock();
}

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String key(); // SpEL 表达式,如 "#orderId"
String prefix() default "lock:"; // key 前缀
long waitTime() default 3; // 等待时间(秒)
long leaseTime() default -1; // 持有时间(-1 = WatchDog)
TimeUnit unit() default TimeUnit.SECONDS;
}
// AOP 切面
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class DistributedLockAspect {
private final RedissonClient redissonClient;
private final ExpressionParser parser = new SpelExpressionParser();
@Around("@annotation(distributedLock)")
public Object around(ProceedingJoinPoint joinPoint,
DistributedLock distributedLock) throws Throwable {
// 解析 SpEL key
String keyExpr = distributedLock.key();
String keyVal = parseSpel(keyExpr, joinPoint);
String lockKey = distributedLock.prefix() + keyVal;
RLock lock = redissonClient.getLock(lockKey);
boolean acquired = lock.tryLock(
distributedLock.waitTime(),
distributedLock.leaseTime(),
distributedLock.unit()
);
if (!acquired) {
throw new RuntimeException("获取锁失败,请稍后重试:" + lockKey);
}
try {
return joinPoint.proceed();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
private String parseSpel(String expr, ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
return parser.parseExpression(expr).getValue(context, String.class);
}
}
// 使用注解
@Service
public class OrderService {
@DistributedLock(key = "#orderId", prefix = "lock:order:")
public void createOrder(Long orderId) {
// 自动加锁、自动释放
doCreateOrder(orderId);
}
@DistributedLock(key = "#userId + ':' + #productId", prefix = "lock:stock:",
waitTime = 5, leaseTime = 10)
public void deductStock(Long userId, Long productId, int qty) {
doDeductStock(productId, qty);
}
}

场景推荐锁类型关键配置
一般互斥场景RLock(可重入锁)默认 WatchDog 续期
读多写少(配置、字典)RReadWriteLock读锁共享,写锁独占
转账/多资源同时锁MultiLock全部加锁才算成功
顺序性要求严格FairLock按请求顺序分配