当前位置: 代码网 > it编程>数据库>Mysql > MySQL 乐观锁原理、实现、踩坑与实战完整指南

MySQL 乐观锁原理、实现、踩坑与实战完整指南

2026年08月20日 Mysql 我要评论
前言在开发扣库存、账户余额变更、订单状态流转这类并发业务时,如果不加控制,高并发下很容易出现超卖、数据覆盖、余额错乱问题。很多同学第一反应想到用 mysql 悲观锁 select ... for up

前言

在开发扣库存、账户余额变更、订单状态流转这类并发业务时,如果不加控制,高并发下很容易出现超卖、数据覆盖、余额错乱问题。

很多同学第一反应想到用 mysql 悲观锁 select ... for update,但是悲观锁会加行锁,并发高时容易出现锁等待、死锁、性能下降。
这时就有了另一种方案:乐观锁

很多人听过乐观锁,但是搞不清楚:

  • 乐观锁到底是数据库自带功能吗?
  • 版本号怎么写sql?
  • 时间戳做乐观锁有什么坑?
  • 和悲观锁、事务隔离级别是什么关系?

本文结合业务实战讲透 mysql 乐观锁。

说明:乐观锁并不是 mysql 内置的锁机制,它是一种业务层实现的并发控制思想,数据库本身没有 lock optimistic 这类语法。

一、乐观锁与悲观锁核心思想对比

悲观锁

悲观的认为:并发一定会发生冲突,查询数据的时候就直接上锁,其他人必须等待锁释放才能操作。

  • 实现:select ... for update,行排他锁
  • 特点:先加锁,后修改;冲突时阻塞等待;
  • 缺点:高并发场景锁竞争激烈,容易超时、死锁,吞吐量低。

乐观锁

乐观的认为:大部分时候不会发生并发冲突,不上锁,直接执行业务修改;提交更新的时候再检查,看看数据有没有被别人修改过

  • 实现:业务版本号 / 时间戳,基于 update where 条件做版本校验
  • 特点:不加锁,读无阻塞;冲突发生时直接返回更新行数0,业务捕获后做重试或者报错;
  • 缺点:需要业务代码处理冲突重试;不能解决脏读问题。
类型锁机制并发冲突处理性能典型实现
悲观锁数据库行锁阻塞等待并发高较差select … for update
乐观锁业务逻辑,无数据库锁更新判断,失败抛异常/重试并发高较好version版本号

二、乐观锁两种主流实现方案

方案1:版本号 version(推荐,生产首选)

核心原理:给业务表增加一个 version 版本字段,数字类型。

  1. 查询时,把当前 version 一并读出来;
  2. 更新的时候,where 条件带上 version = 查询出来的旧版本号
  3. 更新成功同时 version = version + 1
  4. 如果这条sql影响行数为0,代表期间已经被其他线程修改过,发生冲突。

建表示例(库存表)

create table `goods_stock` (
  `id` bigint not null auto_increment comment '主键',
  `stock` int not null default '0' comment '库存',
  `version` int not null default '0' comment '乐观锁版本号',
  primary key (`id`)
) engine=innodb default charset=utf8mb4;

业务执行流程

  1. 查询库存与版本号
select id, stock, version from goods_stock where id = 1;
-- 返回 stock=10,version=1
  1. 业务判断库存充足,执行扣减,更新条件带上旧version
update goods_stock 
set stock = stock -1, version = version +1 
where id = 1 and version = 1;

关键点:

  • 如果更新返回 affected_rows = 1:更新成功;
  • 如果更新返回 affected_rows = 0:说明在查询和更新间隙,有其他请求已经修改过这条数据,version已经改变,本次操作冲突失败。

python伪代码示例

# 1 查询
row = db.query("select id,stock,version from goods_stock where id=1")
stock = row["stock"]
old_version = row["version"]
if stock <= 0:
    print("库存不足")
# 2 扣库存更新
affect_rows = db.execute(
    "update goods_stock set stock=stock-1,version=version+1 where id=%s and version=%s",
    (1, old_version)
)
if affect_rows == 0:
    # 乐观锁冲突,需要重试或者返回提示用户
    raise exception("并发冲突,请重试")

方案2:使用更新时间戳update_time实现(不推荐优先使用)

利用数据的修改时间作为校验依据,不需要额外增加version字段。
表字段:update_time datetime not null default current_timestamp on update current_timestamp

执行逻辑:

-- 查询拿到旧的更新时间
select id,stock,update_time from goods_stock where id=1;
-- 更新带上旧时间戳
update goods_stock 
set stock = stock -1 
where id =1 and update_time = '2026-08-19 10:00:00';

时间戳方案重大缺陷:

  1. datetime 只到秒级别,如果同一秒内多次修改,时间戳不变,乐观锁失效,出现数据覆盖
  2. 如果业务代码手动修改了update_time字段,会直接破坏乐观锁逻辑。

✅建议:优先使用数字version版本号。

三、乐观锁常见坑点

坑1:只在代码做版本判断,没有放到update where条件中

❌错误写法

# 先查询version,代码里面if判断
row = query(...)
if row.version == old_version:
    execute("update goods_stock set stock=stock-1 where id=1")

查询和update是两条sql,中间存在时间窗口,并发下会出现竞态条件,乐观锁完全失效。
版本校验必须写在update的where条件里面,交给数据库原子执行。

坑2:乐观锁不能解决aba问题

aba场景:
线程a读取version=1;
线程b修改数据version变为2,又改回version=1;
线程a执行更新,where version=1条件命中,更新成功。
业务数据实际发生过变更,但是版本号回到原值,更新直接放行。

  • version自增方案:天然规避aba,每次修改version必然+1,不会回到旧值
  • 时间戳方案:会存在aba风险。

大部分互联网业务场景aba并不多见,version自增可以解决绝大多数场景。

坑3:乐观锁只针对行级别,无法处理批量更新

如果需要批量更新多条记录,乐观锁每一行都要携带version条件;
批量并发很高的场景,大量更新返回0,大量冲突重试,cpu压力上升。

坑4:乐观锁不能替代事务

乐观锁依然需要事务保证业务一致性。比如扣库存同时生成订单,需要事务包裹保证多条sql要么全部成功,要么全部回滚。
乐观锁解决并发覆盖问题,不解决原子性问题

坑5:高并发下大量冲突,疯狂重试

秒杀场景大量请求同时竞争同一条库存记录,乐观锁大量返回影响行数0,业务不断重试,会把数据库压垮。

优化:上层做限流、队列削峰,不要单纯依靠乐观锁硬抗大并发。

四、乐观锁 vs 悲观锁 该怎么选

使用乐观锁场景

  1. 读多写少,冲突概率不高;
  2. 追求高吞吐量,不想出现锁等待;
  3. 冲突发生允许业务重试,比如扣库存、用户信息修改。

使用悲观锁场景

  1. 写冲突概率非常高,乐观锁大量冲突重试代价更大;
  2. 业务不能接受失败重试,必须保证一定拿到最新数据;
  3. 锁持有时间很短。

举例子:
商品库存秒杀:读多写少,优先乐观锁;
金融账户转账,强一致性,冲突概率高,可考虑悲观锁。

五、mybatis-plus 中乐观锁简单了解(拓展)

很多java开发使用mp内置乐观锁插件,原理和我们手写sql完全一样:

  1. 实体增加 @version 注解标记version字段;
  2. 开启乐观锁插件;
  3. 更新实体对象,mp自动拼接 where version = ? 并且自动 version+1

底层依然只是封装sql,数据库本身没有特殊锁。

六、完整业务流程模板

1.开启事务
2.查询业务数据,同时读取version版本号
3.业务逻辑校验(库存是否大于0等)
4.执行update set xxx=xxx,version=version+1 where id=? and version=?
5.判断update返回影响行数
    - 等于1:commit提交事务,业务成功
    - 等于0:rollback回滚,发生并发冲突,抛出异常或重试

重试小提示:重试不要无限循环,设置最大重试次数3~5次,防止死循环。

总结

  1. 乐观锁不是mysql数据库自带锁,是业务层并发控制思想,依靠version版本号+update where条件实现;
  2. 数字自增version是最优方案;时间戳datetime存在秒级冲突漏洞,谨慎使用;
  3. 版本校验必须写在update的where条件,不能在代码内存中判断;
  4. version自增可以规避aba问题;
  5. 乐观锁适合读多写少场景,冲突时返回影响行数0,业务需要处理冲突与重试;
  6. 乐观锁不能替代事务;高并发热点数据需要配合限流、消息队列,避免大量重试压垮数据库;
  7. 和悲观锁区别:乐观锁不上锁,冲突检测;悲观锁先上锁,阻塞等待。

记住一句核心:乐观锁本质就是利用数据库 update 的原子性,把「读取-判断-修改」的判断逻辑放到数据库单条sql内部执行。

到此这篇关于mysql 乐观锁原理、实现、踩坑与实战完整指南的文章就介绍到这了,更多相关mysql乐观锁原理内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com