乐观锁 vs 悲观锁

数据库知识点

乐观锁(Optimistic Locking)

一句话定义

乐观锁是一种并发控制 策略,它假设数据在读取和更新之间很少发生冲突 ,因此不加锁,而是在提交更新时才检查数据是否被其他事务修改过。

核心思想

"先干活,提交时再验证"。大多数时候没人抢,所以不加锁;真有冲突,让最后提交的人失败重试。

工作机制(常见实现)

通常依赖版本号(version) 或时间戳:

  1. 读取数据,同时拿到 version = 1
  2. 业务处理(可能耗时较久)
  3. 更新时 WHERE id = X AND version = 1
  4. 如果受影响行数 = 0 → 说明被别人改过了 → 失败/重试
  5. 如果受影响行数 = 1 → 成功,同时 version 自增为 2

举例(电商下单)

sql 复制代码
-- 1. 读取库存
SELECT stock, version FROM product WHERE id = 1001;
-- 假设得到 stock=10, version=1

-- 2. 用户A和用户B同时下单,各自计算 stock-1

-- 3. 用户A先提交:
UPDATE product
   SET stock = 9, version = version + 1
 WHERE id = 1001 AND version = 1;
-- 成功,version 变为 2

-- 4. 用户B再提交:
UPDATE product
   SET stock = 9, version = version + 1
 WHERE id = 1001 AND version = 1;
-- 影响行数 = 0,version 已经变成 2,条件不匹配 → 失败,需重试

乐观锁 vs 悲观锁

维度 乐观锁 悲观锁
假设 冲突很少 冲突经常发生
加锁时机 不加锁,提交时检查 读取时就加锁
适用场景 读多写少 写多读少
性能 高(无锁阻塞) 低(锁等待)
实现 版本号、CAS SELECT ... FOR UPDATE
失败处理 重试或回退 阻塞等待

常见应用场景

  1. 电商秒杀:库存扣减,版本号防超卖
  2. 分布式系统:无中心节点的并发协调
  3. 配置更新:多人可能同时改同一配置
  4. ORM 框架 :MyBatis-Plus 的 @Version 注解、Hibernate 的乐观锁
  5. Redis/MongoDB :用 WATCH/version 字段实现

实现要点

typescript 复制代码
// 伪代码:典型乐观锁更新
async function updateWithOptimisticLock(id: string, updater: (data: T) => T) {
  const current = await db.findById(id);
  const newData = updater(current);

  const result = await db.update(
    { id, version: current.version },  // 关键:带上旧版本号
    { ...newData, version: current.version + 1 }
  );

  if (result.affectedRows === 0) {
    throw new ConflictError('数据已被修改,请重试');
  }
  return newData;
}

注意事项

  • 重试机制:失败后必须有重试策略(限制重试次数)
  • ABA 问题:单纯版本号可解决;若用时间戳需注意时钟漂移
  • 不适合高冲突场景:频繁重试反而比悲观锁更慢

版本号起始值(常见做法)

版本号只是一个递增的标记,具体从几开始,完全看业务设计:

核心认知 :数字大小不重要,关键是版本号必须单调递增 ,且更新时 WHERE 必须带上读到的旧版本号。

sql 复制代码
-- 推荐写法:NOT NULL DEFAULT 0,避免 NULL 带来的三值逻辑问题
ALTER TABLE product ADD COLUMN version INT NOT NULL DEFAULT 0;

悲观锁

FOR UPDATE 是 SQL 的锁定语法 ,通常跟在 SELECT 语句后面,用于实现悲观锁。

例如:

sql 复制代码
SELECT *
FROM account
WHERE id = 1
FOR UPDATE;

它的含义是:

查询 id = 1 这条数据,同时对它加排他锁(X Lock)。

必须结合事务理解

通常完整写法是:

sql 复制代码
BEGIN;

SELECT *
FROM account
WHERE id = 1
FOR UPDATE;

UPDATE account
SET balance = balance - 100
WHERE id = 1;

COMMIT;

执行过程:

text 复制代码
BEGIN
  ↓
SELECT ... FOR UPDATE
  ↓
查询数据 + 对数据加锁
  ↓
UPDATE
  ↓
COMMIT
  ↓
释放锁

假设事务 A 已经执行:

sql 复制代码
SELECT * FROM account WHERE id = 1 FOR UPDATE;

这时事务 B 又执行:

sql 复制代码
SELECT * FROM account WHERE id = 1 FOR UPDATE;

由于 A 还没有 COMMIT,B 通常会:

text 复制代码
事务 A:获得锁 ───── UPDATE ───── COMMIT
              ↑
事务 B:      等待等待等待... ───→ 获得锁

所以可以简单记住:

SELECT ... FOR UPDATE = 查询数据,并把查询到的数据锁住,直到当前事务提交或回滚。

另外,FOR UPDATE 不是 UPDATE 语句。它仍然是一个 SELECT 查询的附加子句,只是告诉数据库:"我接下来准备修改这些数据,所以先锁住它们。"

悲观锁为什么必须结合事务?

SELECT ... FOR UPDATE 获取的锁需要有明确的生命周期,而这个生命周期通常由事务控制:

sql 复制代码
BEGIN;

SELECT * FROM account
WHERE id = 1
FOR UPDATE;   -- 获取锁

UPDATE account
SET balance = balance - 100
WHERE id = 1;

COMMIT;       -- 提交事务,同时释放锁

可以理解为:

text 复制代码
BEGIN
  ↓
事务开始
  ↓
SELECT ... FOR UPDATE
  ↓
获取并持有锁
  ↓
业务处理 / UPDATE
  ↓
COMMIT 或 ROLLBACK
  ↓
事务结束、释放锁

如果查询后锁马上释放,就无法保护后续的"读取 → 判断 → 修改"整个过程,因此悲观锁通常需要放在事务中。

BEGIN、COMMIT、ROLLBACK

语句 含义
BEGIN / START TRANSACTION 开启事务
COMMIT 提交事务,让修改正式生效
ROLLBACK 回滚事务,撤销本次事务中的修改

一个事务通常有两种结局:

text 复制代码
BEGIN
  ↓
执行 SQL
  ↓
 ┌──────────────┐
 ↓              ↓
COMMIT        ROLLBACK
成功提交        失败回滚
 ↓              ↓
 └──────┬───────┘
        ↓
   事务结束
   释放相关锁

一句话记忆

事务用 BEGIN 开始,用 COMMIT 提交或 ROLLBACK 回滚;FOR UPDATE 获取的锁通常会一直持有到事务结束,从而保证整个过程不被其他冲突事务干扰。

数据库中的 DECIMAL 字段

DECIMAL 是数据库中用来存储精确小数的字段类型,专门为金额、价格、汇率等不能丢精度的数值设计。

数据库字段 必须用 DECIMAL,不要用 FLOAT/DOUBLE,金融场景禁浮点(精度丢失)

相关推荐
PaperData3 小时前
1985-2025年全国区县专利申请与授权面板数据
数据库
snpgroupcn3 小时前
大数据量SAP迁移方案:系统越大,越不能硬搬
数据库·sap
Flynt4 小时前
Redis Cluster主节点挂了,为什么"高可用"还全员掉线?我把三次kill的记录翻出来了
数据库·redis·分布式
笃行3504 小时前
KingbaseES 数据加密全解:从 SSL 到全密态
数据库
Ivanqhz5 小时前
激活函数在 Transformer 中的作用及各种变体简述
java·linux·数据库·人工智能·深度学习
Seraphina365 小时前
SQLI-labs(一)黑盒全流程
sql·安全·web安全
java1234_小锋6 小时前
【技术专题】Mysql8 数据库 - Mysql8 数据类型简介
数据库·mysql
知行EDI6 小时前
知行之桥 MaBang 端口使用指南——Create Order 订单创建篇
java·服务器·数据库
成旭先生6 小时前
【2026】企业信息模糊查询 API 实战:名称、注册号、统一社会信用代码、企业类型与法人一次查全
服务器·数据库·数据服务
JosieBook6 小时前
【数据库】MySQL 实战精通系列 · 第10篇:分库分表与分布式事务实战
数据库·分布式·mysql