乐观锁 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 UPDATESQL 的锁定语法 ,通常跟在 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
  ↓
事务结束、释放锁

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

BEGINCOMMITROLLBACK

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

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

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

一句话记忆

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

数据库中的 DECIMAL 字段

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

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

相关推荐
Leo.yuan2 小时前
2026年多数据库实时同步的四种架构方案及工具推荐
数据库·架构
hasty3 小时前
Origin 校验不是身份认证:MySQL MCP Server 漏洞给 AI 平台的警告
数据库·mysql·安全
梦想平凡4 小时前
百游棋牌源代码开发搭建教程(五):房间创建、座位分配与请求幂等实现
前端·javascript·数据库·源代码管理
凤山老林4 小时前
Spring Boot 整合 Flowable 的企业级落地指南
数据库·spring boot·后端·flowable·工作流
企业数字化笔记4 小时前
AI写的系统出现504怎么办?接口超时和数据库慢查询排查
数据库·后端
2601_962218615 小时前
万象生鲜系统订单全生命周期状态同步技术实现业务可视
大数据·数据库·人工智能·python·算法
Wang's Blog5 小时前
Java框架快速入门:Spring Security+OAuth2之用户注册与唯一性校验实现
java·数据库·spring
跨境生态圈5 小时前
2026谷歌SEO快速排名深度解析:合规起量、避坑指南与实战落地策略
数据库·人工智能·爬虫·搜索引擎·chatgpt
JavaPub-rodert6 小时前
Docker 安装 MySQL 完整教程:从零部署数据库,到生产环境持久化配置
数据库·mysql·docker