1. 分布式锁与Redisson基础认知
第一次在生产环境遇到库存超卖问题时,我盯着那个负数的库存值愣了半天。当时用的是简单的Redis setnx命令,直到看见Redisson的Java原生锁API才明白,原来分布式锁可以像synchronized一样自然。作为Java开发者最熟悉的分布式锁解决方案,Redisson确实把复杂问题简单化了。
Redisson本质上是Redis的Java客户端,但它提供的远不止基础的数据结构操作。其核心价值在于将Redis打造成了一个分布式服务框架,分布式锁只是其众多功能之一。与其他Redis客户端相比,Redisson的独特之处在于:
- 原生Java接口:锁对象直接实现了java.util.concurrent.locks.Lock接口
- 看门狗机制:自动续期避免业务未完成锁已过期
- 丰富的锁类型:包括可重入锁、公平锁、联锁、红锁等
- 异步支持:所有操作都提供异步(Async)、反射式(Reactive)和RxJava接口
重要提示:Redisson 3.x需要JDK 1.8+和Redis 2.8+,而Redisson 2.x支持JDK 1.6+和Redis 2.6+
2. 为什么需要分布式锁
去年双十一大促时,我们的优惠券发放系统在集群环境下出现了重复发放。事后分析发现,当多个应用实例同时检查"是否已发放"时,本地锁根本无法跨JVM互斥。这正是分布式锁的典型场景——当多个进程需要互斥访问共享资源时。
分布式锁要解决的三大核心问题:
- 互斥性:任何时候只能有一个客户端持有锁
- 死锁避免:即使客户端崩溃,锁也能自动释放
- 容错性:只要大部分Redis节点存活,客户端就能获取和释放锁
常见错误方案对比:
| 方案 | 问题 | Redisson解决方案 |
|---|---|---|
| 数据库乐观锁 | 高并发下大量重试 | 内存操作无阻塞 |
| Redis setnx | 无自动续期可能死锁 | 看门狗自动续期 |
| Zookeeper | 性能较低 | 基于Redis高性能 |
3. Redisson分布式锁实战
3.1 基础配置
首先引入Maven依赖(以Redisson 3.17.4为例):
xml复制<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.4</version>
</dependency>
SpringBoot配置示例:
yaml复制spring:
redis:
host: 127.0.0.1
port: 6379
# 生产环境建议配置连接池
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
3.2 可重入锁实现
Redisson最常用的就是可重入锁(ReentrantLock),使用方式与JDK锁几乎一致:
java复制@RestController
public class LockController {
@Autowired
private RedissonClient redisson;
@GetMapping("/lock")
public String lockResource() {
RLock lock = redisson.getLock("resourceLock");
try {
// 尝试加锁,最多等待100秒,上锁后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if(res) {
// 业务逻辑
return "success";
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
return "failed";
}
}
关键参数说明:
- tryLock(waitTime, leaseTime, unit):等待时间建议设置,避免长时间阻塞
- leaseTime设置为0时启用看门狗机制(默认30秒续期一次)
- 必须要在finally中解锁,否则可能导致死锁
3.3 读写锁应用
在高并发读少写多的场景,读写锁能显著提升性能:
java复制RReadWriteLock rwLock = redisson.getReadWriteLock("anyRWLock");
// 读锁
rwLock.readLock().lock();
try {
// 多个线程可同时进入
// 读操作...
} finally {
rwLock.readLock().unlock();
}
// 写锁
rwLock.writeLock().lock();
try {
// 只有一个线程能进入
// 写操作...
} finally {
rwLock.writeLock().unlock();
}
读写锁的特性:
- 读读不互斥:多个线程可同时持有读锁
- 读写互斥:有读锁时写锁等待,反之亦然
- 写写互斥:同一时间只能有一个写锁
4. 锁原理深度解析
4.1 可重入锁实现机制
Redisson的可重入锁通过Redis的Hash结构实现,Key是锁名称,field是客户端ID,value是重入次数。加锁的Lua脚本核心逻辑:
lua复制if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
解锁时的重入次数处理:
lua复制if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
4.2 看门狗机制
当不指定leaseTime时,Redisson会启动看门狗线程定期(默认每10秒)检查客户端是否还持有锁,如果是则延长锁过期时间(默认续到30秒)。这个机制解决了两个问题:
- 业务执行时间超过锁过期时间的问题
- 客户端崩溃导致锁无法释放的问题
经验之谈:对于执行时间可控的业务,建议明确设置leaseTime;不可控的业务则依赖看门狗
5. 生产环境注意事项
5.1 性能优化建议
- 锁粒度控制:锁的key应该精确到具体资源,如"order_lock:123"比"order_lock"更好
- 超时设置:根据业务特点设置合理的waitTime和leaseTime
- 避免锁嵌套:虽然可重入,但多层锁会增加死锁风险
- 监控:通过Redisson的监控接口统计锁等待时间、持有时间等指标
5.2 常见问题排查
- 锁未释放:检查是否所有代码路径都调用了unlock()
- 锁等待时间过长:评估锁粒度是否过粗或业务处理是否过慢
- Redis集群故障:考虑使用红锁(RedLock)机制
- 网络分区:配置合理的超时时间,避免长时间阻塞
5.3 Redisson与Spring事务的配合
在需要事务的场景,要注意锁的生命周期应该大于事务:
java复制@Transactional
public void processWithLock() {
RLock lock = redisson.getLock("txLock");
lock.lock();
try {
// 业务操作
// ...
} finally {
// 建议在事务提交后再解锁
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronization() {
@Override
public void afterCommit() {
lock.unlock();
}
});
}
}
6. 扩展应用场景
6.1 秒杀系统实现
通过分布式锁+Redis原子操作实现秒杀:
java复制public boolean seckill(Long productId, Long userId) {
RLock lock = redisson.getLock("seckill:" + productId);
try {
lock.lock();
// 检查库存
Integer stock = redisTemplate.opsForValue().get("stock:" + productId);
if(stock <= 0) return false;
// 扣减库存
redisTemplate.opsForValue().decrement("stock:" + productId);
// 记录购买关系
redisTemplate.opsForSet().add("bought:" + productId, userId.toString());
return true;
} finally {
lock.unlock();
}
}
6.2 分布式定时任务
确保集群中只有一个节点执行定时任务:
java复制@Scheduled(cron = "0 0/5 * * * ?")
public void scheduledTask() {
RLock lock = redisson.getLock("scheduledTaskLock");
if(lock.tryLock()) {
try {
// 执行任务逻辑
} finally {
lock.unlock();
}
}
}
在实际项目中,Redisson的分布式锁已经成为我们处理并发问题的标准工具。从最初的简单应用到后来的各种高级特性,它的稳定性和易用性经受住了我们日均百万级并发请求的考验。特别是在处理金融交易和库存管理这类对一致性要求极高的场景时,合理的锁策略设计往往能事半功倍。