事务越长,锁争用越严重,因为从BEGIN到COMMIT/ROLLBACK期间所有锁均不释放;哪怕仅UPDATE后卡顿2秒(如调用HTTP、文件写入或SLEEP),其他事务就需等待同一行锁,导致阻塞、超时甚至死锁。

为什么事务越长,锁争用越严重
事务不是“开启就完事”,它从 BEGIN 到 COMMIT 或 ROLLBACK 之间,所有加过的锁都不会释放。哪怕只是执行一条 UPDATE 后卡住 2 秒——比如调了外部 HTTP 接口、做了本地文件写入、甚至只是 SLEEP(1),其他想改同一行的事务就得干等。
常见错误现象包括:
-
SHOW ENGINE INNODB STATUS\G中反复出现*** (1) WAITING FOR THIS LOCK TO BE GRANTED: - 应用层报错如
Lock wait timeout exceeded; try restarting transaction - 慢查询日志里
Rows_examined极高,但实际只改一行——说明没走索引,锁了太多行
实操建议:
- 把非数据库操作(RPC、日志、计算)全部移出事务块
- 批量更新必须分页,例如
UPDATE t SET status=1 WHERE id BETWEEN ? AND ?,每次控制在 100–500 行 - 避免在事务中做
SELECT * FROM t WHERE ...再判断逻辑——应直接用带主键或唯一索引的UPDATE ... WHERE id = ? AND version = ?
READ COMMITTED 能省掉多少间隙锁
InnoDB 在默认隔离级别 REPEATABLE READ 下,为防止幻读会自动加 Gap Lock,哪怕你只 update 一行,也可能锁住整个索引区间。而 READ COMMITTED 完全不加间隙锁,只锁命中的记录本身,锁范围大幅收窄。
适用场景很明确:
- 业务能接受“不可重复读”(比如报表类页面刷新两次看到不同数据)
- 高频更新 + 范围条件(如
UPDATE order SET state=2 WHERE create_time ) - 有大量
INSERT ... SELECT或REPLACE INTO的并发写入
注意点:
- 降级前务必确认 binlog 格式为
ROW(STATEMENT模式下 RC 级别可能引发主从不一致) - 某些依赖间隙锁做业务校验的逻辑(如防重复下单)需同步改造,改用唯一索引或应用层幂等
- 可通过
SELECT @@transaction_isolation和SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED动态切换,无需重启
读写分离不是加个从库就自动生效
很多团队上了从库,但 SELECT 还是打到主库,根本原因是没在应用层做路由。MySQL 本身不提供自动读写分离,必须由客户端决定发往哪台实例。
真实使用中容易踩的坑:
- 刚写完立刻读(如注册后跳转个人页),从库延迟导致查不到新数据——需强制走主库或加
SELECT ... FOR UPDATE触发一致性读 - 用
ThreadLocal或 AOP 做数据源路由时,异步线程、定时任务、回调函数容易漏切,仍连主库 - 从库只读但未设
read_only=ON,误写入导致主从冲突
轻量级落地方式:
- 方法名约定:以
get、list、count开头的方法默认走从库;含save、update、delete的走主库 - 注解驱动:如 Spring 的
@ReadOnly+ 自定义AbstractRoutingDataSource - 缓存兜底:对变动不频繁的配置表、字典表,优先查 Redis,彻底绕过 DB 锁
乐观锁不是万能的,但它是热点行的解药
当多个事务反复争抢同一行(比如用户余额、商品库存),悲观锁(SELECT ... FOR UPDATE)只会让等待队列越来越长。这时应该把锁从数据库搬到应用层,用版本号或 CAS 实现重试逻辑。
典型写法:
UPDATE user SET balance = balance - 100, version = version + 1 WHERE id = 123 AND version = 5;
关键判断是 ROW_COUNT() 是否为 1 —— 不是就说明被别人抢先更新了,需要重试或返回失败。
但要注意:
- version 字段必须为
INT或BIGINT,不能是字符串或时间戳(并发下可能相等) - 重试次数要限制(如最多 3 次),避免雪崩
- 不适合强实时一致性要求的场景(如银行转账),此时仍需
FOR UPDATE配合分布式锁 - 和缓存配合时,记得用 Lua 脚本保证 Redis 扣减 + MySQL 更新的原子性
真正难的不是选哪种锁,而是识别哪几行是“热点”。这得靠 information_schema.innodb_trx 和慢日志交叉分析,而不是凭感觉猜。











