悲观锁(Pessimistic Lock)
✅ 核心思想:
始终假设最坏的情况:别人一定会修改数据,所以每次读写都会加锁,确保操作安全。
在读数据时就加锁,防止其他事务修改这条数据,确保当前事务后续的操作(尤其是写)是安全的。
悲观锁就是 “读时加锁,确保写时没有并发冲突”,必须配合事务使用,先查锁定,后更新提交,这是它的核心机制。
🛠 实现方式:
- 数据库层面的锁(如行锁、表锁)
SELECT * FROM user WHERE id = 1 FOR UPDATE;
该语句会对 id=1 的行加行锁(InnoDB),其他事务只能等你释放。
✅ 解释 SELECT … FOR UPDATE是什么?
这是一个 悲观锁 的实现方式。它会:
- 给返回的记录加上行级排他锁(exclusive lock)
- 直到当前事务提交/回滚之前,其他事务不能对这行数据做 UPDATE 或 DELETE
📌 使用场景:
通常用于先读后改的业务逻辑,例如银行扣款:
本质是用于“查询 + 锁定”
BEGIN; -- 开始事务(BEGIN);
-- 查出余额并加锁
SELECT balance FROM account WHERE id = 123 FOR UPDATE;
-- 应用层判断余额是否足够,然后扣款
UPDATE account SET balance = balance - 100 WHERE id = 123;
COMMIT;
应用场景 | 原因 / 说明 |
---|---|
库存扣减(电商系统) | 多用户同时抢购同一个商品,必须保证库存一致性,防止“超卖”现象。 |
积分/余额变更 | 如用户领取奖励、消费积分、修改余额等,必须防止并发重复扣减或增加。 |
用户账户状态变更 | 某些状态是强一致的,例如实名认证状态不能同时被两个操作修改。 |
分布式事务关键数据保护 | 如主业务成功后,写入其他关键表数据(如日志、状态表),必须确保某些步骤不被并发干扰。 |
任务领取 / 队列消费 | 多个服务实例抢占任务,使用悲观锁防止任务被重复领取(抢单系统中也常见)。 |
订单状态流转 | 比如订单状态从“待支付”到“已支付”,不能被重复更新或被非法修改。 |
排队/名额抢占操作 | 比如课程报名、预约挂号、秒杀等,需要严格控制名额。 |
📌 特点:
- 并发低、冲突高的系统下比较安全
- 容易引发锁等待、死锁等问题
- 对数据库压力大
悲观锁特别适合:
- 数据写冲突概率高的场景;
- 强一致性要求高,如金融、电商、任务调度等;
- 可以接受一定程度的性能牺牲来保证数据正确性。
乐观锁
✅ 核心思想:
默认别人不会修改,只有在提交时检查是否有冲突(比如版本号是否变了),如果有冲突,就放弃本次操作或重试。
🛠 实现方式:
使用版本号或时间戳字段来检测冲突。
示例:
id | name | version
1 | Tom | 3
UPDATE user SET name = 'Jerry', version = version + 1
WHERE id = 1 AND version = 3;
表结构:
- 如果返回 0 行,说明版本冲突;你可以重试或提示失败。
📌 特点:
- 无需加锁,适合并发量大、冲突少的场景
- 实现复杂度略高,需业务控制
- 很适合读多写少的业务(如订单系统、配置系统)
流程
前端/调用方应当在读取数据后,连同 version 一并提交回来,否则服务器无法判断是否有并发修改行为。
查询数据
{
"id": 1,
"name": "Tom",
"version": 3
}
- 前端修改 name,连同 version 提交:
{
"id": 1,
"name": "Jerry",
"version": 3
}
后端执行 SQL 判断 version 是否一致:
UPDATE user
SET name = 'Jerry', version = version + 1
WHERE id = 1 AND version = 3;
- 建议后端接口文档明确要求前端带上 version 字段。
- 若前端实在无法改,可以:
- 后端先查当前版本(增加一次 DB IO,失去乐观锁“无锁”的优势);
- 或只能用悲观锁代替(使用 SELECT … FOR UPDATE 加锁)。
版本号字段
目前最主流的乐观锁实现,版本号字段通常就是 int 类型
每次更新时 version + 1,判断是否冲突即可。
❓ 为什么不用 UUID 或 时间戳呢?
类型 | 优点 | 缺点 | 是否推荐 |
---|---|---|---|
int(版本号) | 简单高效,支持自增,SQL 快 | 易被猜测、需要前端传回 | ✅ 常用方案 |
timestamp(更新时间) | 可读性强,也可用于判断冲突 | 精度可能不足,容易误判 | ✅ 也常见(如 updated_at) |
uuid(版本标识) | 安全、不可猜 | 更新逻辑复杂,不能自增 | ❌ 一般不建议用来做版本号 |
可以将 version 包装为不可修改字段,仅供提交用,或传输时改名,比如:
{
"id": 123,
"new_value": "XXX",
"_v": 3 // version
}
应用场景
非常适合以下高并发、读多写少、允许一定冲突重试的场景
应用场景 | 说明 |
---|---|
配置管理系统 | 大量人读取配置,偶尔修改,修改时需避免冲突(比如灰度配置中心)。 |
商品库存预读写 | 多人查看商品详情,实际提交订单时使用乐观锁避免库存扣减冲突(低频冲突)。 |
用户信息修改 | 比如修改昵称/头像等用户信息,避免并发提交导致数据覆盖。 |
内容编辑(博客、文章、文案) | 避免两个编辑者同时保存时,互相覆盖编辑内容。 |
订单更新操作(非支付流程) | 如用户备注、订单地址修改,避免覆盖修改内容。 |
版本控制系统 / 文档协作平台 | 如 Google Docs、Notion 等,在保存文档前先比对版本。 |
后台运营系统操作控制 | 多人后台同时维护资源时避免状态冲突,如运营审核、上下架。 |
非关键业务的数据采集 / 日志写入控制 | 冲突可以接受失败或重试,适合乐观锁策略。 |