"为什么工作 10 年都没遇过分布式锁?"
如果在一个技术社区抛出这个问题,底下大概率会吵起来。有人说自己一直在做传统企业项目,连 Redis 都没用过几次,分布式锁离得太远;有人说自己明明在写微服务,但拆来拆去都是 CRUD,并发量小到不需要锁;还有人直接吐槽:分布式锁就是面试造火箭,工作拧螺丝。我觉得这个问题问得很好,它表面上是技术疑问,实际上是职业体检——为什么别人会遇到,而你十年都没遇到?
先说结论:没遇过分布式锁,不代表分布式锁不重要,也不代表你的技术路线正确。它至少说明,你深度参与的业务场景里,还没有出现"多个独立进程同时修改同一份共享资源"的强竞争情况。这句话听起来很简单,但很多人工作多年都说不清楚。下面我会从原因、判断标准、实现原理、方案选型和工程踩坑这几个角度,把这堂课补上。
其实写这篇文章的初衷是:我面试过不少工作五到十年的候选人,简历上都写了"熟悉分布式",但问到分布式锁时,两眼一抹黑。其中一两个是真的很努力,只是业务场景确实没碰到。所以我想把"为什么没遇到"和"真的遇到时该怎么办"一次性讲透,既给新人做认识铺垫,也给自己做技术复盘。
1. 十年没遇到分布式锁,通常卡在哪一类项目里
1.1 单体应用加单库单表:分布式锁没有舞台
工作十年的老工程师,很大一部分时间是在传统单体应用中度过的。这种项目的特点是:一个应用包部署在一台服务器上,业务数据集中在一个数据库里,应用的并发能力由进程内线程池和数据库连接池决定。在这种架构下,多个线程访问共享资源时,用的是 synchronized、ReentrantLock、数据库事务和唯一索引,这些都是"进程内锁"或"数据库锁"。
进程内锁只能锁住当前 JVM 里的线程。只要系统还是单进程部署,这个锁就能生效。问题在于,现在很多单体应用实际上也会做多实例部署:同一个 WAR 包放到两台机器上,前面挂一个 Nginx 做负载均衡。这时候进程内锁就彻底失效了。假设秒杀扣库存接口加了 synchronized,两个请求分别打到两个实例上,每个实例的锁都觉得自己拿到了锁,于是两个实例同时去数据库扣减库存,超卖就发生了。很多人工作多年没遇到分布式锁,有很大概率是项目做了多实例部署,但业务接口没有出现这种跨实例并发写,或者并发量低到数据库行锁就把冲突消化了。
所以,单体应用不等于不能用分布式锁,而是"在单进程内竞争资源"这个前提不成立,分布式锁没有舞台而已。
1.2 读多写少或天然串行的业务:并发竞争被业务稀释
另一种常见情况是业务本身就是读多写少。比如一个企业官网、一个内容管理后台、一个报表展示平台,绝大多数请求是读数据,写操作寥寥无几。读操作天然不互斥,不需要锁;写操作频率很低,两个用户同时写同一个数据的概率极小,即使真的撞上了,最后一次覆盖也无所谓。这种业务里引入分布式锁,属于典型的"用大炮打蚊子"。
还有一些业务天然串行。比如线下业务的人工审核流程,一个工单同一时间只可能被一个审核员领取;比如银行柜面业务,一个客户在同一时刻只能有一个柜员操作;再比如批量结算任务,系统本来就有状态字段控制流程。当业务流程本身就把并发排除了,分布式锁自然也就没有出现的必要。
我说这些不是给"没遇到过"找借口,而是想强调:分布式锁是一种解决并发冲突的工具,不是所有系统都需要。业务形态决定架构需求,没有需求时你不会想到它,这与技术能力无关,但与技术视野有关。
1.3 分布式系统里绕开了分布式锁:用数据约束"设计掉"锁
还有一类人,系统确实是分布式的,多个服务多个实例,但他们依然没写过分布式锁。原因往往是架构师在设计时已经把并发冲突"设计掉了"。
最典型的做法是按业务键做路由。比如用户端的请求都通过网关根据 userId 取模,把同一个用户的所有请求固定到同一台后端实例。这样同一个用户的数据只可能被一台实例处理,服务内部用进程内锁就能搞定,服务间不再需要锁。它本质上是用路由把分布式问题还原成了单机问题。
另一种做法是乐观锁替代。在数据库表里加一个 version 字段,更新时执行 update table set value = ?, version = version + 1 where id = ? and version = ?,受影响行数为 0 说明版本冲突,业务重试或报错。这样既不阻塞,也不需要跨进程锁。
这
