浅聊一下Redisson分布式锁
大家好!今天咱们聚焦分布式系统里的 “当红选手”——Redisson 分布式锁。不少小伙伴做微服务或分布式项目时,都遇到过并发抢资源的坑,比如库存超卖、重复下单,而 Redisson 锁就是解决这类问题的常用方案。咱们不用堆砌复杂术语,从 “为啥需要它” 到 “怎么用”,一步步说清楚,看完你也能上手。
一、先明白:为啥单机锁不行,非要分布式锁?
先从咱们熟悉的 “单机锁” 唠起。写单体项目时,用 Java 的 synchronized 或者 ReentrantLock 就能搞定并发,比如多个线程抢着改库存,加个锁就能保证同一时间只有一个线程操作。但项目一拆成微服务,多台服务器跑同一个服务,单机锁就 “失灵” 了 —— 因为 synchronized 只能锁当前服务器的 JVM 线程,服务器 A 的锁管不了服务器 B 的线程。
举个实际例子:比如电商秒杀,两台服务器同时接收到 “扣减同一商品库存” 的请求,要是用单机锁,服务器 A 的线程加了锁,服务器 B 的线程没被限制,照样能操作数据库,最后库存就可能变成负数,这就是 “超卖”。而分布式锁的作用,就是让多台服务器的线程 “共用一把锁”,不管哪个服务器的线程,要操作资源都得先拿到这把锁,这样就不会出并发问题了。
二、Redisson 锁:为啥比自己写的 Redis 锁好用?
很多人会想:“我用 Redis 的 setNx 命令也能实现分布式锁,为啥还要用 Redisson?” 确实,setNx 能实现 “加锁”,但实际项目里,分布式锁需要考虑的问题远比 “加锁” 多,比如锁过期、重入、释放锁的原子性,这些 Redisson 都帮咱们做好了,不用自己踩坑。
先说说 Redisson 的核心优势:
- 支持重入锁:比如线程 A 拿到锁后,在没释放锁的情况下,再次请求加锁还能拿到(比如递归调用场景),而自己用 setNx 实现的话,很容易造成 “自己锁自己”。Redisson 通过记录 “线程 ID + 重入次数” 来实现这点,每次重入次数加 1,释放时次数减 1,直到次数为 0 才真正释放锁。
- 自动续期(看门狗机制):假设咱们给锁设了 10 秒过期,但线程 A 执行任务需要 20 秒,锁过期后其他线程就会拿到锁,导致并发问题。Redisson 的 “看门狗” 会在锁快过期时(默认过期时间的 1/3,比如 10 秒过期,3 秒左右续期),自动把锁的过期时间重置为默认值(默认 30 秒),直到线程 A 执行完任务主动释放锁。
- 释放锁原子性:自己用 Redis 实现时,释放锁需要先判断 “是不是自己的锁”,再删除锁,但这两步不是原子操作(比如判断完后锁刚好过期,其他线程加了锁,这时再删除就会删错别人的锁)。Redisson 用 Lua 脚本把 “判断 + 删除” 做成原子操作,避免了这个问题。
三、实战:Spring Boot 项目集成 Redisson 锁
光说原理不够,咱们直接上代码,看看在 Spring Boot 里怎么用 Redisson 锁。
第一步:加依赖
先在 pom.xml 里加 Redisson 的依赖(注意和 Spring Boot 版本匹配,这里用的是常用版本):
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.3</version>
</dependency>
第二步:配置 Redisson
不用写复杂的配置类,直接在 application.yml 里配 Redis 地址就行(Redisson 会自动创建客户端):
spring:redis:host: 127.0.0.1 # 你的Redis地址port: 6379 # Redis端口password: 123456 # 你的Redis密码(没设的话可以删了这行)
第三步:实际用锁(以 “扣减库存” 为例)
假设咱们有个秒杀接口,需要扣减商品库存,用 Redisson 锁保证同一时间只有一个线程操作:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.TimeUnit;@RestController
public class StockController {// 注入Redisson客户端(自动配置好的,直接用)@Autowiredprivate RedissonClient redissonClient;// 模拟库存(实际项目里存在数据库,这里简化)private int stock = 100;/*** 扣减库存接口* @param goodsId 商品ID(用商品ID作为锁的key,保证同一商品共用一把锁)*/@GetMapping("/reduceStock/{goodsId}")public String reduceStock(@PathVariable String goodsId) {// 1. 创建锁:锁的key要唯一,比如用“商品ID+业务标识”,避免和其他锁冲突RLock lock = redissonClient.getLock("stock:lock:" + goodsId);try {// 2. 加锁:参数分别是“等待时间”“过期时间”“时间单位”// 意思是:最多等5秒拿锁,拿到锁后30秒自动过期(看门狗会续期)boolean isLocked = lock.tryLock(5, 30, TimeUnit.SECONDS);if (isLocked) {// 3. 拿到锁,执行业务(扣减库存)if (stock > 0) {stock--;return "扣减成功!当前库存:" + stock;} else {return "库存不足!";}} else {// 4. 没拿到锁,返回提示return "系统繁忙,请稍后再试!";}} catch (InterruptedException e) {e.printStackTrace();return "操作失败!";} finally {// 5. 释放锁(注意:只有当前线程持有锁时,才释放)if (lock.isHeldByCurrentThread()) {lock.unlock();}}}
}
这段代码里有几个关键点要注意:
- 锁的 key 要唯一:比如用 “stock:lock: 商品 ID”,这样不同商品的锁不会互相影响,同一商品的所有请求都抢同一把锁。
- tryLock 的参数:等待时间(5 秒)表示线程最多等 5 秒拿锁,超过就返回 false;过期时间(30 秒)是初始过期时间,看门狗会自动续期。
- 释放锁的判断:用
lock.isHeldByCurrentThread()
判断当前线程是否持有锁,避免释放别人的锁(比如线程没拿到锁,却执行了 unlock)。
四、踩坑提醒:这 3 个问题要注意
- Redis 集群环境下的 “脑裂” 问题:如果 Redis 是主从集群,主节点挂了,从节点还没同步到锁的信息,新的主节点起来后,可能会让其他线程拿到锁。解决办法是用 Redisson 的 “红锁”(RedLock),多个 Redis 节点同时加锁,只有多数节点加锁成功,才算整体加锁成功。
- 不要滥用分布式锁:如果业务能通过 “数据库乐观锁”(比如用 version 字段)解决,就不用分布式锁,因为分布式锁依赖 Redis,多了一次网络请求,性能会有损耗。
- 避免死锁:虽然 Redisson 有自动过期和看门狗,但还是要确保代码里会执行 unlock(比如放在 finally 里),避免线程拿到锁后,因为异常没释放锁,导致锁过期前一直占用(虽然过期会自动释放,但还是会影响并发)。
五、总结
Redisson 分布式锁的核心价值,就是帮咱们解决了分布式环境下的并发问题,而且封装好了重入、续期、原子释放等细节,不用自己造轮子。实际项目里,只要做好 “锁 key 唯一”“合理设置 tryLock 参数”“确保释放锁” 这几点,就能轻松应对大部分并发场景。
如果你的项目里还在自己手写 Redis 锁,或者被并发问题困扰,不妨试试 Redisson,上手简单,稳定性也够,亲测在高并发场景(比如秒杀)下表现很好。后续如果需要深入了解红锁、读写锁(ReadWriteLock)的用法,咱们再单独聊~