SQL 乐观锁与悲观锁高级应用与优化

3次阅读

乐观锁和悲观锁需依竞争强度、业务语义、延迟要求动态权衡;悲观锁适用于写多读少、高冲突、强一致场景,如库存扣减;乐观锁通过版本号校验实现延迟冲突检测;混合策略与绕过锁设计可进一步提升性能与可靠性。

SQL 乐观锁与悲观锁高级应用与优化

乐观锁和悲观锁不是非此即彼的选择,而是要根据数据竞争强度、业务语义、响应延迟要求来动态权衡。用错锁机制,轻则性能陡降,重则引发死锁或丢失更新。

什么时候该用悲观锁?

适合写多读少、事务周期短、冲突概率高、且必须强一致的场景。比如库存扣减、账户余额转账、秒杀下单。

  • selectfor UPDATE 显式加行级写锁,但务必确保 WHERE 条件命中索引,否则会升级为表锁
  • 事务粒度要尽量小——加锁后立刻执行更新,避免在锁持有期间做 http 调用、日志打印等耗时操作
  • 设置合理的 innodb_lock_wait_timeout(默认 50 秒),配合应用层超时与重试逻辑,防止线程长时间阻塞
  • 注意死锁风险:多个事务按不同顺序访问同一组行时极易触发。可通过统一访问顺序(如按主键升序)或使用 SELECT … FOR UPDATE SKIP LOCKED 规避

乐观锁不是“不加锁”,而是“延迟校验”

它依赖版本号(version)、时间戳(updated_at)或业务字段(如库存余量)做并发控制,在 UPDATE 时验证是否被修改过。失败不阻塞,由应用决定重试或提示。

  • 推荐用 version 字段(BIGint 或 INT)而非时间戳,避免毫秒级并发下时钟漂移或重复值问题
  • UPDATE 语句必须包含版本条件,例如:
    UPDATE order SET status = ‘paid’, version = version + 1 WHERE id = ? AND version = ?
  • 影响行数为 0 即表示更新失败,需捕获并处理(如重读+重算+重提交,或返回“操作已被他人修改”)
  • 慎用于高频更新+长事务场景:反复重试可能放大数据库压力,此时应评估是否误用了乐观锁

混合策略:乐观校验 + 悲观兜底

对关键路径做轻量乐观校验,失败后再以悲观方式保底执行,兼顾性能与可靠性。

  • 例如优惠券核销:先用 version 字段乐观更新;失败后,查当前库存是否充足,若仍可扣减,则用 SELECT … FOR UPDATE 锁定该券记录再执行
  • 适用于“绝大多数请求无冲突,但极少数需强保障”的混合负载,比全程悲观锁吞吐高,又比纯乐观锁容错强
  • 注意兜底逻辑不能嵌套在原事务中盲目重试,建议拆分为独立子事务或补偿流程,避免锁等待叠加

绕过锁:从设计上减少竞争

最高效的锁优化,是让锁根本不需要出现。

  • 分库分表 + 热点隔离:把用户订单分散到不同库表,避免所有更新集中在单条记录上
  • 状态机驱动:用异步消息+状态流转替代同步强一致性更新(如“支付中→已支付”由 MQ 消费端完成)
  • 缓存预占:redis 中预扣库存(INCRBY)+ 过期时间,成功后再落库;失败则释放缓存,降低 DB 冲突面
  • 批量合并:将多次小更新聚合成一次大更新(如积分累加改为 delta 累计,定时 flush),减少锁频次
text=ZqhQzanResources