在读写比例场景下,如何抉择MySQL悲观锁与乐观锁以优化性能压测?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1132个文字,预计阅读时间需要5分钟。
MySQL中的`SELECT ... FOR UPDATE`是经典的悲观锁实现方式,它会在事务中对选中的行加写锁,直到事务提交或回滚。这在多读少写、操作频繁争夺同一行数据的情况下风险较大——比如数据库库存扣减、账户余额更新等。压测时常见的问题是大量线程在等待表元数据锁,或者直接报错Lock wait timeout exceeded。
实操建议:
- 仅在明确知道冲突概率高、且业务能接受串行化写入时使用,例如金融类强一致性转账
- 务必确保
SELECT ... FOR UPDATE语句命中索引,否则会升级为表锁,压测 QPS 可能断崖式下跌 - 事务粒度要小:避免在锁住行后执行 HTTP 调用、日志写入等耗时操作
- 设置合理
innodb_lock_wait_timeout(默认 50 秒),并捕获Deadlock found when trying to get lock错误做重试
乐观锁更适合读多写少 + 冲突概率低的业务场景
乐观锁本质是“先查后验”,典型做法是在表中加 version 字段或用 UPDATE ... WHERE version = ? AND id = ? 做条件更新。压测数据显示,在读写比 > 9:1 的场景(如商品详情页浏览+少量收藏/加购),乐观锁吞吐量通常比悲观锁高 3–5 倍,因为无锁开销、无等待队列。
但要注意几个硬约束:
-
UPDATE必须带完整校验条件,漏掉version或id会导致数据覆盖 - 不能依赖 MySQL 自增主键做乐观控制——它不反映业务逻辑版本
- 如果业务要求“写失败必须立即反馈”,乐观锁重试逻辑需前置到应用层,避免用户感知不到失败
- 注意
version字段类型:用INT UNSIGNED比BIGINT更省空间,但要防溢出;用TIMESTAMP则需处理时区与精度问题
混合策略:用缓存 + 乐观锁降低数据库压力
纯数据库层锁机制在高并发下很快成为瓶颈。真实压测中,把 Redis 作为前置校验层,能显著改善表现。例如秒杀场景:先 DECR Redis 库存,成功后再走 MySQL 乐观更新,失败则回退 Redis(用 Lua 保证原子性)。
这种组合的关键点在于:
- Redis 不是最终一致性兜底,而是“第一道过滤器”,目标是让 95% 以上的请求不触达 MySQL
- MySQL 仍需保留乐观锁逻辑,用于兜底和防止缓存穿透后的脏写
- 注意 Redis 和 MySQL 的数据双写顺序:先更新 DB,再删缓存(Cache Aside),避免短暂不一致
- 压测时单独测试 Redis 失效后 MySQL 的扛压能力,别被缓存掩盖了真实瓶颈
不要忽略隔离级别对锁行为的隐式影响
很多人以为用了 SELECT ... FOR UPDATE 就一定是行锁,结果压测发现还是锁表——根本原因是当前事务隔离级别是 REPEATABLE READ,且查询条件未命中索引,触发了间隙锁(Gap Lock)。而 READ COMMITTED 下间隙锁只在唯一索引等极少数情况生效。
所以实际选型前必须确认:
- 执行
SELECT @@tx_isolation查看当前会话隔离级别 - 用
EXPLAIN验证所有带锁查询是否走了索引,特别是WHERE中的字段 - 若业务允许,可将隔离级别降为
READ COMMITTED,配合乐观锁,多数场景更稳 - 注意
READ COMMITTED下 binlog 格式必须为ROW,否则主从可能不一致
本文共计1132个文字,预计阅读时间需要5分钟。
MySQL中的`SELECT ... FOR UPDATE`是经典的悲观锁实现方式,它会在事务中对选中的行加写锁,直到事务提交或回滚。这在多读少写、操作频繁争夺同一行数据的情况下风险较大——比如数据库库存扣减、账户余额更新等。压测时常见的问题是大量线程在等待表元数据锁,或者直接报错Lock wait timeout exceeded。
实操建议:
- 仅在明确知道冲突概率高、且业务能接受串行化写入时使用,例如金融类强一致性转账
- 务必确保
SELECT ... FOR UPDATE语句命中索引,否则会升级为表锁,压测 QPS 可能断崖式下跌 - 事务粒度要小:避免在锁住行后执行 HTTP 调用、日志写入等耗时操作
- 设置合理
innodb_lock_wait_timeout(默认 50 秒),并捕获Deadlock found when trying to get lock错误做重试
乐观锁更适合读多写少 + 冲突概率低的业务场景
乐观锁本质是“先查后验”,典型做法是在表中加 version 字段或用 UPDATE ... WHERE version = ? AND id = ? 做条件更新。压测数据显示,在读写比 > 9:1 的场景(如商品详情页浏览+少量收藏/加购),乐观锁吞吐量通常比悲观锁高 3–5 倍,因为无锁开销、无等待队列。
但要注意几个硬约束:
-
UPDATE必须带完整校验条件,漏掉version或id会导致数据覆盖 - 不能依赖 MySQL 自增主键做乐观控制——它不反映业务逻辑版本
- 如果业务要求“写失败必须立即反馈”,乐观锁重试逻辑需前置到应用层,避免用户感知不到失败
- 注意
version字段类型:用INT UNSIGNED比BIGINT更省空间,但要防溢出;用TIMESTAMP则需处理时区与精度问题
混合策略:用缓存 + 乐观锁降低数据库压力
纯数据库层锁机制在高并发下很快成为瓶颈。真实压测中,把 Redis 作为前置校验层,能显著改善表现。例如秒杀场景:先 DECR Redis 库存,成功后再走 MySQL 乐观更新,失败则回退 Redis(用 Lua 保证原子性)。
这种组合的关键点在于:
- Redis 不是最终一致性兜底,而是“第一道过滤器”,目标是让 95% 以上的请求不触达 MySQL
- MySQL 仍需保留乐观锁逻辑,用于兜底和防止缓存穿透后的脏写
- 注意 Redis 和 MySQL 的数据双写顺序:先更新 DB,再删缓存(Cache Aside),避免短暂不一致
- 压测时单独测试 Redis 失效后 MySQL 的扛压能力,别被缓存掩盖了真实瓶颈
不要忽略隔离级别对锁行为的隐式影响
很多人以为用了 SELECT ... FOR UPDATE 就一定是行锁,结果压测发现还是锁表——根本原因是当前事务隔离级别是 REPEATABLE READ,且查询条件未命中索引,触发了间隙锁(Gap Lock)。而 READ COMMITTED 下间隙锁只在唯一索引等极少数情况生效。
所以实际选型前必须确认:
- 执行
SELECT @@tx_isolation查看当前会话隔离级别 - 用
EXPLAIN验证所有带锁查询是否走了索引,特别是WHERE中的字段 - 若业务允许,可将隔离级别降为
READ COMMITTED,配合乐观锁,多数场景更稳 - 注意
READ COMMITTED下 binlog 格式必须为ROW,否则主从可能不一致

