在微服务架构与高并发场景下,如何保证数据一致性与系统高可用,是每位后端开发者必须面对的挑战。乐观锁、悲观锁、分布式锁这三把“锁”,看似简单,却常常在实际项目中让人困惑。本文将通过图解与实例,帮你理清它们的本质区别,并给出清晰的选型指南。
乐观锁:适合冲突少的更新场景
很多人第一次听到“乐观锁”时,会下意识觉得它是一种“真的加了锁”的机制。其实不是,乐观锁不强调“先锁住”,而强调“更新时确认数据还是不是我看到的那份数据”。
乐观锁到底在解决什么问题?
在并发场景下,多个请求可能同时修改同一份数据,如果没有任何控制,就容易出现覆盖更新的问题。比如,商品库存原本是10:
- 请求A读到库存是10
- 请求B也读到库存是10
- A扣减后写回9
- B也扣减后写回9
看起来两个请求都成功了,但实际上库存只少了1次,这就是典型的并发更新问题,导致数据的不一致性。

⚙️ 乐观锁通常怎么实现?
最常见的做法,是给数据加一个version版本号字段。比如一条商品记录长这样:
id = 1 stock = 10 version = 3
当某个请求读到这条数据时,它不仅会读到库存,也会读到当前版本号。后续更新时,不是直接根据id去更新,而是带着这个version一起更新:
update product
set stock = stock - 1,
version = version + 1
where id = 1
and version = 3;
这条 SQL 的关键点不在stock - 1,而在:
and version = 3
只有当这条数据的版本号还是我最初读到的 3 时,我这次更新才允许成功。
如果更新成功,说明这期间没人动过这条数据。
如果更新失败,说明别的请求已经先一步改过了,你当前这次操作所基于的数据已经过期。
乐观锁实际例子
假设现在有一个“普通商品扣库存”的场景,库存初始值为:
stock = 10 version = 5
这时请求A和请求B同时进来。
- 第一步:请求A和请求B都查询到:
stock = 10 version = 5
然后两个请求都准备执行更新。
- 第二步:请求A先执行:
update product
set stock = 9,
version = 6
where id = 1
and version = 5;
执行成功,数据库中数据变成:
stock = 9 version = 6
- 第三步:接着请求B再执行:
update product
set stock = 9,
version = 6
where id = 1
and version = 5;
这时候就会失败,因为数据库中的version已经不是5,而是6了。这就是乐观锁的本质:不是抢着先锁住资源,而是通过版本一致性校验,拒绝基于旧数据的更新。

悲观锁:适合高冲突、强一致场景
如果说乐观锁的思路是“先假设冲突不常发生”,那悲观锁正好相反:它先假设冲突一定会发生,所以干脆在操作前先把资源锁住。悲观锁的重点不是“更新失败后怎么补救”,而是在真正修改数据之前,就先阻止别人同时改。
悲观锁到底在解决什么问题?
悲观锁解决的依然是并发场景下对同一份数据的竞争更新问题。但它和乐观锁的处理方式完全不同:
- 乐观锁:允许大家先一起读,一起尝试更新,最后根据版本号判断谁成功、谁失败。
- 悲观锁:谁先拿到锁,谁先操作;没拿到锁的人先等着,等前一个操作完成后,后面的人再继续。
所以从结果上看,悲观锁更像是在并发场景下,把对关键数据的修改强行串行化。
可以用一个账户余额扣减的例子来解释悲观锁。假设一个账户当前余额是100元,现在同时来了两个扣款请求:
- 请求A:扣80元
- 请求B:扣30元
如果没有做好并发控制,就可能出现问题:A读到余额100,B也读到余额100,A判断余额足够,扣减成功;B也判断余额足够,扣减成功。结果两个请求都通过了校验,总共扣了110元,账户上没有那么多钱,这样账户就出问题了。

⚙️ 悲观锁通常怎么实现?
在后端开发里,最常见的悲观锁实现,就是数据库的行锁。
select * from account
where id = 1
for update;
这条SQL的核心是最后的:
for update
它表示:在当前事务提交之前,把这条记录锁住。后续如果其他事务也想对这条记录加锁或修改这条记录,就必须等待当前事务释放锁。
悲观锁实际例子
还是刚刚提到的余额扣减例子,如果应用悲观锁的话,会变成下面这样:
- 第一步:请求A先开启事务并加锁:
select balance from account
where id = 1
for update;
数据库把这条账户记录锁住。
- 第二步:请求B也来了,它如果也想对同一条账户记录执行
for update,就会被阻塞,必须等待请求A提交事务。 - 第三步:请求A在事务里完成校验和更新,查到
balance = 100,判断100 >= 80,允许扣款,执行更新,余额变成20,提交事务,释放锁。 - 第四步:请求A释放锁后,请求B才能获得锁,才能进行查询操作,这时它再查余额,读到的已经是最新值20,然后会发现20 < 30,余额不足,从而扣款失败。
这样一来,系统就不会出现“两个请求都基于旧余额通过校验”的问题。

分布式锁:适合多节点之间抢执行权
讲到这里,很多人会自然地把分布式锁和乐观锁、悲观锁并列理解,觉得它们只是“不同类型的锁”。但严格来说,它们并不完全在同一个维度上。因为前两者主要解决的是:并发更新同一份数据时,如何保证结果正确。而分布式锁更关注的是:在多个服务实例同时运行的情况下,某个动作到底该由谁来执行。
分布式锁很多时候锁的不是“某条数据库记录”,而是:
- 某个任务的执行资格
- 某段业务逻辑的处理权
- 某个全局动作的唯一执行者
为什么单机锁到了分布式场景就不够用了?
在单机应用里,如果你想让一段代码同一时刻只能被一个线程执行,往往可以直接用 synchronized 和 ReentrantLock。这类本地锁的前提是,所有竞争者都在同一个进程、同一台机器里。但后端服务一旦真正上线,往往不会只部署一个实例。这时候问题就来了,比如你写了一个定时任务,代码里加了synchronized,你以为这样就能保证“同一时刻只执行一次”。但实际上:机器A上的线程能进来,机器B上的线程也能进来,它们彼此根本看不到对方的锁。

⚙️ 分布式锁通常怎么实现?
在工程里,分布式锁的实现方式不止一种,但最常见的是Redis分布式锁。最基础的做法通常类似这样:
SET lock_key unique_value NX PX 30000
其中:
- NX:只有当key不存在时才设置成功,这保证了只有一个实例能抢到锁
- PX 30000:设置过期时间30秒,避免服务宕机后锁永远不释放
- unique_value:锁的唯一标识,释放锁时要校验,避免把别人的锁删掉
分布式锁最关键的不是“加锁成功”本身,而是“锁的持有、过期、释放”都要设计清楚,因为它不像本地锁那样天然可靠,分布式锁涉及网络、超时、节点故障,所以边界问题会多很多。
分布式锁实际例子
用“定时任务只允许一个实例执行”这个经典例子来讲解:假设系统部署了服务A和服务B两个实例,现在有一个定时任务,每天凌晨扫描超时订单并自动取消。如果没有分布式锁,那么在同一时刻,服务A会触发一次任务,服务B也会触发一次任务。这样就可能出现:
- 同一批订单被重复扫描
- 同一笔退款逻辑被重复执行
- 同一条通知被重复发送
很多人会说:“那我不是已经写了幂等了吗?”幂等当然很重要,但它解决的是“重复执行后结果尽量不出错”;而分布式锁解决的是:从源头上减少重复执行,让同一时刻只有一个实例先进去做。他们解决问题的侧重点不一样。
一个典型流程通常是这样:
- 第一步:服务A和服务B同时触发任务,它们都准备执行“取消超时订单”,然后都去竞争同一把锁,锁名为:
cancel_timeout_order_task_lock
- 第二步:只有一个实例抢锁成功,假设服务A成功抢到了锁,服务A开始执行业务。
- 第三步:服务B因为没抢到锁,所以直接退出,或者稍后重试,直到服务A执行完成后释放锁,下一轮任务再重新竞争。

不同业务中到底怎么选锁?
写到这里,其实最重要的已经不是再去背“乐观锁是什么、悲观锁是什么、分布式锁是什么”了。真正决定你能不能把这类问题讲明白的,是你有没有意识到:这三种锁根本不是同一个维度上的技术点。很多人会把它们并排记忆,然后试图总结成“乐观锁轻一点,悲观锁重一点,分布式锁更高级一点”。这种记法最容易在面试和项目里出问题,因为它会让你忽略一个关键事实:乐观锁、悲观锁主要解决的是数据竞争,分布式锁主要解决的是执行权竞争。
以下是一个简单的选型对照表,帮助你快速决策:
| 类型 | 核心解决的问题 | 锁/控制的对象 | 常见实现 | 适合场景 | 主要优势 | 主要代价 |
|---|---|---|---|---|---|---|
| 乐观锁 | 并发更新同一份数据时,避免基于旧数据覆盖更新 | 数据版本一致性 | 版本号、CAS、时间戳 | 读多写少、冲突少的更新场景 | 并发性能好,不容易阻塞 | 冲突多时失败率高,需要重试 |
| 悲观锁 | 并发更新同一份关键数据时,保证强互斥和强一致 | 数据访问权 | 数据库行锁、 | 写冲突高、强一致要求高的场景 | 逻辑直观,一致性更稳 | 阻塞明显,吞吐下降,可能死锁 |
| 分布式锁 | 多个服务节点同时竞争某个业务动作的执行权 | 某段业务逻辑的执行资格 | Redis、ZooKeeper、数据库锁表/唯一约束辅助 | 定时任务单点执行、全局互斥、避免重复执行 | 能跨节点控制互斥 | 实现边界复杂,要处理超时、误删、故障等问题 |
总结一下选型原则:
- ✅ 如果冲突概率低,且业务允许偶尔的重试(如商品库存扣减),优先考虑乐观锁。
- ✅ 如果冲突概率高,且对数据一致性有强要求(如金融交易),推荐使用悲观锁。
- ✅ 如果是在微服务架构下,多个节点需要协调执行权(如定时任务、全局ID生成),必须使用分布式锁。
在实际的系统架构设计中,这三把锁往往不是互斥的,而是可以组合使用的。例如,一个高并发的秒杀系统,可能会用乐观锁处理库存扣减,用分布式锁防止重复下单,用悲观锁处理最后的支付环节。理解它们的本质和适用场景,才能做出真正高可用、高并发的设计。
[AFFILIATE_SLOT_2]总结
乐观锁通过版本号控制数据一致性,适合低冲突场景;悲观锁通过数据库行锁强制串行化,适合高冲突场景;分布式锁通过外部协调(如Redis)解决多节点执行权竞争。三者维度不同,但共同构成了后端并发控制的基石。理解它们的本质差异,才能在项目中做出正确的技术选型。
versionselect ... for update
浙公网安备 33010602011771号