在MySQL高并发场景中,如何选择悲观锁或乐观锁以优化并发控制?

2026-04-30 21:201阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1243个文字,预计阅读时间需要5分钟。

在MySQL高并发场景中,如何选择悲观锁或乐观锁以优化并发控制?

确保在进行读取、业务判断和更新操作时保持原子性,使用`SELECT ... FOR UPDATE`语句才有意义。例如,库存减少:

常见错误现象:Deadlock found when trying to get lock 或响应明显变慢,往往是因为锁范围过大(比如没走索引导致锁全表),或事务里混了非数据库操作(如调外部 API、sleep)让锁持有太久。

  • 必须确保 FOR UPDATE 的查询条件命中索引,否则升级为表级锁;
  • 事务越短越好,查完立刻更新,别在事务里做日志打印、HTTP 请求这类耗时操作;
  • InnoDB 下,FOR UPDATE 只对当前读生效,快照读(普通 SELECT)仍可见旧值,这点常被忽略。

version 字段做乐观锁,为什么不能只靠 WHERE version = ?

只加 WHERE version = ? 是乐观锁的必要条件,但不是充分条件。真正起作用的是:每次更新后必须同步递增 version,且应用层要检查 UPDATE 影响行数是否为 1。如果影响行为 0,说明已被别人抢先改过,当前操作应失败重试或提示冲突。

容易踩的坑:version 字段类型用 INT 没问题,但千万别用 TIMESTAMPdatetime——并发下毫秒级重复、时钟回拨都会导致校验失效。

  • 更新语句必须是 UPDATE t SET ..., version = version + 1 WHERE id = ? AND version = ?
  • 不要在同一个事务里多次读取 version 后再拼 SQL,避免中间被其他事务修改;
  • 如果业务允许“最后写入 wins”,乐观锁够用;但如果要求强顺序(如金融转账),它无法替代悲观锁。

高并发下,INSERT ... ON DUPLICATE KEY UPDATE 算悲观还是乐观

它本质是基于唯一索引的“尝试插入 → 冲突则更新”,MySQL 内部会对冲突的唯一键值加锁,属于隐式悲观锁。它比显式 FOR UPDATE 更轻量,适合“有则更新、无则插入”的场景,比如幂等订单状态机、用户积分累计。

但注意:它只锁住触发冲突的那条记录(或索引间隙),不锁扫描范围。所以如果你依赖它来防并发重复下单,必须确保 UNIQUE KEY 覆盖所有判定维度(比如 (user_id, order_type, date)),否则可能漏锁。

  • 冲突时会触发 DUPLICATE KEY 错误,但用 ON DUPLICATE KEY UPDATE 可捕获并转为更新;
  • 如果 UPDATE 部分引用了 VALUES(col),要注意值来自当前 INSERT 语句,不是原记录;
  • 它不适用于需要读取原值做复杂计算的场景(比如“原积分 + 新积分”还得先查一遍),因为没提供原记录上下文。

Redis 分布式锁 + MySQL 本地事务,能凑合代替数据库锁吗

不能。Redis 锁只能协调服务层,无法保证 MySQL 内部的隔离性。典型翻车场景:A 服务拿到 Redis 锁,开始事务,查库存为 10;B 服务没抢到锁,等待;A 提交事务把库存减到 9;B 拿到锁后,仍会基于自己缓存或旧快照再次查出库存为 10,然后也减到 9——超卖了。

根本问题在于:Redis 锁和 MySQL 事务没有共享的锁上下文,无法感知对方的未提交变更。除非你把整个业务逻辑压进一个数据库事务,并用 FOR UPDATE 控制,否则跨系统加锁只是自我安慰。

  • Redis 锁适合控制「非数据库资源」,比如防止重复发消息、限制接口调用频次;
  • 如果非要混合用,至少得让 MySQL 读操作也走当前读(SELECT ... FOR UPDATE),且 Redis 锁的生命周期严格包裹整个数据库事务;
  • 网络分区时 Redis 锁可能假释放,而 MySQL 的行锁由连接生命周期保障,更可靠。

真正难的不是选哪种锁,而是想清楚:你到底要保护哪一行数据的哪一段业务逻辑不被并发破坏。锁只是工具,边界模糊时,再好的锁也救不了设计缺陷。

标签:Mysql

本文共计1243个文字,预计阅读时间需要5分钟。

在MySQL高并发场景中,如何选择悲观锁或乐观锁以优化并发控制?

确保在进行读取、业务判断和更新操作时保持原子性,使用`SELECT ... FOR UPDATE`语句才有意义。例如,库存减少:

常见错误现象:Deadlock found when trying to get lock 或响应明显变慢,往往是因为锁范围过大(比如没走索引导致锁全表),或事务里混了非数据库操作(如调外部 API、sleep)让锁持有太久。

  • 必须确保 FOR UPDATE 的查询条件命中索引,否则升级为表级锁;
  • 事务越短越好,查完立刻更新,别在事务里做日志打印、HTTP 请求这类耗时操作;
  • InnoDB 下,FOR UPDATE 只对当前读生效,快照读(普通 SELECT)仍可见旧值,这点常被忽略。

version 字段做乐观锁,为什么不能只靠 WHERE version = ?

只加 WHERE version = ? 是乐观锁的必要条件,但不是充分条件。真正起作用的是:每次更新后必须同步递增 version,且应用层要检查 UPDATE 影响行数是否为 1。如果影响行为 0,说明已被别人抢先改过,当前操作应失败重试或提示冲突。

容易踩的坑:version 字段类型用 INT 没问题,但千万别用 TIMESTAMPdatetime——并发下毫秒级重复、时钟回拨都会导致校验失效。

  • 更新语句必须是 UPDATE t SET ..., version = version + 1 WHERE id = ? AND version = ?
  • 不要在同一个事务里多次读取 version 后再拼 SQL,避免中间被其他事务修改;
  • 如果业务允许“最后写入 wins”,乐观锁够用;但如果要求强顺序(如金融转账),它无法替代悲观锁。

高并发下,INSERT ... ON DUPLICATE KEY UPDATE 算悲观还是乐观

它本质是基于唯一索引的“尝试插入 → 冲突则更新”,MySQL 内部会对冲突的唯一键值加锁,属于隐式悲观锁。它比显式 FOR UPDATE 更轻量,适合“有则更新、无则插入”的场景,比如幂等订单状态机、用户积分累计。

但注意:它只锁住触发冲突的那条记录(或索引间隙),不锁扫描范围。所以如果你依赖它来防并发重复下单,必须确保 UNIQUE KEY 覆盖所有判定维度(比如 (user_id, order_type, date)),否则可能漏锁。

  • 冲突时会触发 DUPLICATE KEY 错误,但用 ON DUPLICATE KEY UPDATE 可捕获并转为更新;
  • 如果 UPDATE 部分引用了 VALUES(col),要注意值来自当前 INSERT 语句,不是原记录;
  • 它不适用于需要读取原值做复杂计算的场景(比如“原积分 + 新积分”还得先查一遍),因为没提供原记录上下文。

Redis 分布式锁 + MySQL 本地事务,能凑合代替数据库锁吗

不能。Redis 锁只能协调服务层,无法保证 MySQL 内部的隔离性。典型翻车场景:A 服务拿到 Redis 锁,开始事务,查库存为 10;B 服务没抢到锁,等待;A 提交事务把库存减到 9;B 拿到锁后,仍会基于自己缓存或旧快照再次查出库存为 10,然后也减到 9——超卖了。

根本问题在于:Redis 锁和 MySQL 事务没有共享的锁上下文,无法感知对方的未提交变更。除非你把整个业务逻辑压进一个数据库事务,并用 FOR UPDATE 控制,否则跨系统加锁只是自我安慰。

  • Redis 锁适合控制「非数据库资源」,比如防止重复发消息、限制接口调用频次;
  • 如果非要混合用,至少得让 MySQL 读操作也走当前读(SELECT ... FOR UPDATE),且 Redis 锁的生命周期严格包裹整个数据库事务;
  • 网络分区时 Redis 锁可能假释放,而 MySQL 的行锁由连接生命周期保障,更可靠。

真正难的不是选哪种锁,而是想清楚:你到底要保护哪一行数据的哪一段业务逻辑不被并发破坏。锁只是工具,边界模糊时,再好的锁也救不了设计缺陷。

标签:Mysql