Serializable隔离级别会主动引入串行化冲突,在高并发写密集场景下死锁是设计必然;它不适用于常规业务,仅适合极短事务、极低QPS、绝对不可容忍幻读的核对任务。

Serializable 隔离级别下死锁不是小概率事件,而是设计必然
在高并发写密集场景中,SERIALIZABLE 不是“更安全的选项”,而是主动引入串行化冲突的开关。PostgreSQL 用谓词锁(predicate lock)模拟串行执行,MySQL(InnoDB)则升级为间隙锁+临键锁的组合,只要两个事务访问有重叠的索引范围(哪怕只是 SELECT ... WHERE status = 'pending'),就极可能互相阻塞并最终超时触发死锁。
常见错误现象:ERROR: could not serialize access due to read/write dependencies among transactions(PostgreSQL)、Deadlock found when trying to get lock(MySQL)。这类报错往往在压测后期才爆发,因为低并发时锁等待被掩盖,实际是并发度一上来就卡住。
- 使用场景:仅适用于极短事务 + 极低QPS + 绝对不可容忍幻读的核对类任务(如财务日终校验),而非常规业务更新
- 参数差异:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE在 PostgreSQL 中生效;MySQL 的SET TRANSACTION ISOLATION LEVEL SERIALIZABLE会让所有SELECT自动加临键锁,影响远超预期 - 性能影响:TPS 通常下降 50% 以上,锁等待时间方差极大,监控上表现为慢查询中大量
Lock wait timeout exceeded
乐观锁不是 Serializable 的平替,而是换一种思路防错
乐观锁不解决“多个事务看到同一快照后各自修改”的一致性问题,它只保证“最后一次提交者胜出”,靠的是版本号或时间戳比对。它不阻止并发读,也不加锁,所以不会死锁——但代价是业务层必须接受 UPDATE ... WHERE version = ? 返回 0 行并重试。
典型误用:用乐观锁替代 SERIALIZABLE 做库存扣减,却没处理重试逻辑,导致用户点击下单后无响应或重复下单。
- 适用场景:数据冲突概率低(如用户资料修改)、重试成本可控(前端可自动重放请求)、能容忍短暂不一致(如点赞数)
- 关键参数:
version字段必须是INT或BIGINT,不能用TIMESTAMP(时钟漂移、批量更新时序难保) - MySQL 注意:
UPDATE t SET x = ?, version = version + 1 WHERE id = ? AND version = ?必须确保id是主键或唯一索引,否则可能锁住范围,反而引发新死锁
真正能降死锁的,是缩小事务粒度 + 提前锁定关键资源
比起在隔离级别上硬扛,更有效的是让事务“快进快出”:把 SELECT 判断逻辑提前到事务外,只在事务内做确定性更新;或者用 SELECT ... FOR UPDATE 显式锁住后续要改的行,避免隐式锁扩散。
常见错误现象:在事务里先 SELECT COUNT(*) FROM orders WHERE user_id = ? AND status = 'unpaid',再根据结果决定是否插入新订单——这个 COUNT 可能锁住整个索引区间,尤其当 user_id 索引选择性差时。
- 实操建议:把判断逻辑移到应用层(比如查 Redis 缓存计数),事务内只执行
INSERT INTO orders ...或UPDATE orders SET status = 'paid' WHERE id = ? AND status = 'unpaid' - 如果必须查库判断,用
SELECT id FROM orders WHERE user_id = ? AND status = 'unpaid' LIMIT 1 FOR UPDATE,只锁命中行,不锁间隙 - 注意 MySQL 的
autocommit=1下,单条UPDATE也是事务,但FOR UPDATE必须显式BEGIN,否则报错ERROR 1205 (40001): Deadlock found when trying to get lock
Serializable 和乐观锁混用?别,它们解决的问题根本不在同一维度
SERIALIZABLE 是数据库保证事务执行效果等价于某个串行顺序;乐观锁是应用层实现的“先读后写校验”。两者叠加不仅没收益,反而放大风险:在 SERIALIZABLE 下做乐观更新,可能因谓词锁提前失败,而重试时又撞上新锁。
真正需要混合的场景极少,唯一合理的情况是:用乐观锁做主流程,再用 SERIALIZABLE 包裹一个极小的、必须强一致的补偿步骤(如生成不可逆凭证)。但此时必须确保该步骤不涉及任何乐观锁字段,否则版本号和序列化锁会互相干扰。
- 容易被忽略的点:
SERIALIZABLE事务中执行SELECT ... FOR UPDATE会同时触发谓词锁和行锁,锁开销翻倍,且 PostgreSQL 会直接报错ERROR: cannot use cursor with HOLD in a serializable transaction - MySQL 更隐蔽:开启
innodb_locks_unsafe_for_binlog=OFF(默认)时,SERIALIZABLE下的SELECT会锁所有扫描到的间隙,即使你后面根本没 UPDATE - 测试时别只看单条 SQL——死锁永远发生在两个及以上事务交叉访问时,本地单元测试跑不过,得用
pgbench或sysbench模拟真实竞争路径










