工作 5 年没碰过分布式锁?别慌!这篇从场景到代码教你落地

37 阅读8分钟

前言

你是不是也有过这样的经历?简历上写着 “熟悉分布式系统”,可实际工作好几年,连分布式锁的影子都没见过。每次面试被面试官问到 “怎么实现分布式锁”,只能支支吾吾说个大概,心里还忍不住犯嘀咕:是我技术太菜,还是公司业务太稳?

其实不用焦虑,最近在开发群里聊起这个话题,一堆同行都在附和:“+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 字段),这些其实都是解决资源竞争的方案,只是你没意识到而已。

但咱们做技术的,不能只满足于 “当前够用”。说不定下次跳槽、公司业务扩张,突然就需要用分布式锁了。现在把这篇文章收藏起来,真遇到问题时翻出来,代码能直接用,原理也清楚,比临时查资料高效多了。

而且分布式锁也是面试的高频考点,下次面试官再问,你不仅能说清 “什么场景用”,还能拿出实际代码案例,甚至指出自己实现时的坑,这绝对能加分。

最后想问问你:你工作中遇到过需要分布式锁的场景吗?如果有,当时是怎么解决的?要是没遇到过,看完这篇有没有清楚一点?欢迎在评论区聊聊你的经历,咱们一起交流技术,少踩坑!