前言
你是不是也有过这样的经历?简历上写着 “熟悉分布式系统”,可实际工作好几年,连分布式锁的影子都没见过。每次面试被面试官问到 “怎么实现分布式锁”,只能支支吾吾说个大概,心里还忍不住犯嘀咕:是我技术太菜,还是公司业务太稳?
其实不用焦虑,最近在开发群里聊起这个话题,一堆同行都在附和:“+1,我也是!”“我们公司连 Redis 集群都没有,单机 Redis 用到现在,哪需要什么分布式锁?” 今天就从咱们开发者的实际需求出发,把分布式锁讲透 —— 什么时候必须用,用的时候要踩哪些坑,还有现成的代码模板直接拿过去用,看完这篇,不管是面试还是实际项目,都能应对自如。
什么场景下,你必须用分布式锁?
很多朋友觉得分布式锁是 “大厂专属”,小公司用不上,其实只要你的系统满足两个条件,就可能遇到需要它的情况。
第一个条件是 “分布式部署”:如果你的服务只部署在一台服务器上,哪怕并发再高,用 Java 的 synchronized 或者 ReentrantLock 就能解决线程安全问题。可一旦服务部署在多台服务器,比如秒杀活动用了 3 台服务器承载流量,这时候 3 台服务器上的线程没有共享内存,本地锁就不管用了。
第二个条件是 “资源竞争”:简单说就是多台服务器要抢着操作同一个资源。举两个咱们开发中最常遇到的场景,你肯定有共鸣。
场景一:秒杀超卖。去年大促我朋友负责的项目就踩过坑,明明设置了 “限量 100 件” 的秒杀活动,结果活动结束后后台显示卖了 130 件。客服被用户追着要货,仓库那边又没库存,最后只能赔偿优惠券收场。后来查日志才发现,3 台服务器同时处理下单请求时,每台服务器都检测到 “还有库存”,各自扣减后就出现了超卖。这时候如果在扣库存前加一把分布式锁,让 3 台服务器排队抢锁,只有抢到锁的才能操作库存,超卖问题就解决了。
场景二:重复退款。用户点退款时手抖点了两次,刚好这两个请求被分配到不同的服务器上。如果没加锁,两台服务器都会执行退款逻辑,结果就是用户收到两笔退款,财务对账时发现问题,还得花时间追回。虽然最后能解决,但既浪费时间又影响用户体验,要是早用分布式锁,让同一个订单的退款请求只能执行一次,就能避免这种麻烦。
反过来想,如果你的系统要么是单机部署,要么业务逻辑里没有 “多服务抢资源” 的情况,那用不上分布式锁太正常了,这不是你技术不行,是公司业务跑得稳,这反而是好事。
别自己造轮子!先避坑,再看现成方案
很多朋友刚开始接触分布式锁,会想着用 Redis 的 setIfAbsent 方法自己实现,虽然思路没错,但生产环境用起来全是坑,咱们先把这些坑说清楚,避免你踩雷。
第一个坑:锁过期时间没设好。比如你把锁的过期时间设为 10 秒,结果业务逻辑复杂,执行了 15 秒,锁在第 10 秒就自动释放了,这时候另一个线程就会抢到锁,两个线程同时操作资源,还是会出问题。要是把过期时间设太长,万一业务执行到一半服务器宕机,锁一直不释放,其他线程就只能傻等,导致服务卡住。
第二个坑:释放了别人的锁。比如线程 A 获取锁后执行时间太长,锁过期被线程 B 抢走了。后来线程 A 执行完,直接把线程 B 的锁释放了,接下来线程 C 又能抢锁,这样锁就完全失去了互斥作用。
第三个坑:没考虑重入问题。如果一个线程已经获取了锁,接下来它自己再次请求锁时,反而被挡住了,这就会导致死锁。比如在订单流程里,创建订单和扣减库存都需要同一个锁,要是不支持重入,创建订单后扣减库存时就会被自己的锁拦住。
其实这些问题早就有成熟的解决方案,根本不用自己瞎折腾,直接用 Redisson 框架就行。Redisson 已经帮咱们解决了锁过期、误释放、重入这些问题,而且集成简单,代码量还少,咱们直接看怎么用。
实战代码:SpringBoot 集成 Redisson
不管你是第一次用分布式锁,还是之前踩过坑,下面这套代码都能直接用在项目里,分两步走:加依赖、写业务逻辑。
第一步:添加 Maven 依赖
Redisson 有专门的 SpringBoot starter,不用自己配太多配置,直接在 pom.xml 里加这段依赖就行,注意版本可以根据你的 SpringBoot 版本调整,这里用的是 3.24.1,兼容性比较好:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.1</version>
</dependency>
第二步:写分布式锁业务代码
以退款场景为例,咱们写一个完整的服务类,核心逻辑就是 “获取锁→检查是否已退款→执行退款→释放锁”,而且不管业务执行成功还是失败,最后都会释放锁,避免锁泄漏。
@Service
public class PaymentRefundService {
// 注入Redisson客户端,starter会自动配置
@Autowired
private RedissonClient redissonClient;
/**
* 处理订单退款
* @param orderNo 订单号(作为锁的唯一标识)
*/
public void processOrderRefund(String orderNo) {
// 1. 定义锁的key,用订单号区分,避免锁冲突
String lockKey = "distributed:lock:refund:" + orderNo;
// 2. 获取锁对象
RLock lock = redissonClient.getLock(lockKey);
try {
// 3. 尝试获取锁:最多等100秒,拿到锁后10秒自动释放
// 等待时间100秒:避免因锁争抢导致请求直接失败
// 自动释放时间10秒:防止服务宕机后锁一直不释放
boolean isLockAcquired = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (!isLockAcquired) {
// 没拿到锁,返回友好提示
throw new RuntimeException("当前退款请求过多,请稍后重试");
}
// 4. 拿到锁后,执行核心业务逻辑
// 先检查订单是否已退款,避免重复操作
if (checkOrderHasRefunded(orderNo)) {
throw new RuntimeException("订单" + orderNo + "已退款,请勿重复提交");
}
// 执行退款逻辑(调用支付接口、更新订单状态等)
executeRefundOperation(orderNo);
// 记录退款日志
recordRefundLog(orderNo);
} catch (InterruptedException e) {
// 处理线程中断异常
Thread.currentThread().interrupt();
throw new RuntimeException("获取退款锁失败,请联系技术人员");
} finally {
// 5. 无论如何都要释放锁,且只释放自己的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 检查订单是否已退款
*/
private boolean checkOrderHasRefunded(String orderNo) {
// 实际项目中会查数据库或缓存,这里模拟逻辑
// 比如select refund_status from order_refund where order_no = #{orderNo}
return false;
}
/**
* 执行实际的退款操作
*/
private void executeRefundOperation(String orderNo) {
// 调用支付网关的退款接口,比如支付宝、微信支付的退款API
// 同时更新订单表的退款状态
System.out.println("订单" + orderNo + "退款操作执行成功");
}
/**
* 记录退款日志
*/
private void recordRefundLog(String orderNo) {
// 记录操作人、退款金额、时间等信息到日志表
}
}
这段代码里有几个关键点要注意:锁的 key 用 “业务类型 + 订单号” 命名,避免不同业务用同一个锁;tryLock 方法设置了等待时间和自动释放时间,平衡了用户体验和系统稳定性;finally 块里判断 “是否当前线程持有锁”,避免释放别人的锁。你把这段代码复制到项目里,改改业务逻辑,就能直接用在秒杀、退款、定时任务这些场景里。
最后说两句:没用过不丢人,会用才重要
看到这里,你应该明白为什么自己没碰过分布式锁了 —— 要么公司业务没到分布式部署的量级,要么框架或数据库已经帮你处理了并发问题,比如数据库的悲观锁(SELECT ... FOR UPDATE)、乐观锁(version 字段),这些其实都是解决资源竞争的方案,只是你没意识到而已。
但咱们做技术的,不能只满足于 “当前够用”。说不定下次跳槽、公司业务扩张,突然就需要用分布式锁了。现在把这篇文章收藏起来,真遇到问题时翻出来,代码能直接用,原理也清楚,比临时查资料高效多了。
而且分布式锁也是面试的高频考点,下次面试官再问,你不仅能说清 “什么场景用”,还能拿出实际代码案例,甚至指出自己实现时的坑,这绝对能加分。
最后想问问你:你工作中遇到过需要分布式锁的场景吗?如果有,当时是怎么解决的?要是没遇到过,看完这篇有没有清楚一点?欢迎在评论区聊聊你的经历,咱们一起交流技术,少踩坑!