乐观锁和悲观锁

目录

悲观锁(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;

表结构:

  1. 如果返回 0 行,说明版本冲突;你可以重试或提示失败。

📌 特点:

  • 无需加锁,适合并发量大、冲突少的场景
  • 实现复杂度略高,需业务控制
  • 很适合读多写少的业务(如订单系统、配置系统)

流程

前端/调用方应当在读取数据后,连同 version 一并提交回来,否则服务器无法判断是否有并发修改行为。

查询数据

{
  "id": 1,
  "name": "Tom",
  "version": 3
}
  1. 前端修改 name,连同 version 提交
{
  "id": 1,
  "name": "Jerry",
  "version": 3
}

后端执行 SQL 判断 version 是否一致

UPDATE user
SET name = 'Jerry', version = version + 1
WHERE id = 1 AND version = 3;
  1. 建议后端接口文档明确要求前端带上 version 字段
  2. 若前端实在无法改,可以:
    • 后端先查当前版本(增加一次 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 等,在保存文档前先比对版本。
后台运营系统操作控制 多人后台同时维护资源时避免状态冲突,如运营审核、上下架。
非关键业务的数据采集 / 日志写入控制 冲突可以接受失败或重试,适合乐观锁策略。

点赞一下

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦