MySQL单机行锁在分布式环境下完全失效,因不同节点连接不同实例导致FOR UPDATE互不感知,引发超卖等问题;需用Redis分布式锁配合MySQL带条件的UPDATE和约束兜底。

MySQL单机行锁在分布式下完全失效
MySQL的SELECT ... FOR UPDATE或UPDATE ... WHERE自带行锁,但只在单个MySQL实例内有效。一旦服务部署多副本、读写分离或分库分表,不同应用节点连接的是不同MySQL实例(或主从不同节点),FOR UPDATE互不感知——A节点锁了某行,B节点照样能改,根本起不到分布式协调作用。
常见错误现象:超卖、重复扣减余额、并发生成重复单号。你以为加了事务和行锁就安全了,其实只是“本地安全”。
- 不要把
innodb_lock_wait_timeout调高来“等锁”,它解决不了跨节点冲突 - 不要依赖
INSERT IGNORE或ON DUPLICATE KEY UPDATE替代分布式锁——它们只能防重复插入,无法保护读-改-写逻辑(如“查余额→判断是否足够→扣减”) - 分库分表后,即使同一逻辑记录路由到同一DB,若该DB有主从延迟,
SELECT ... FOR UPDATE可能在从库执行(尤其用了读写分离中间件),直接报错或静默失败
Redis锁不是万能的,必须带自动续期和原子校验
用Redis实现分布式锁,核心是SET key value EX seconds NX:设置带过期时间的唯一值,且仅当key不存在时成功。但光这样远远不够——网络分区、业务执行超时、锁被误删,都会导致数据不一致。
最容易踩的坑是“锁释放不匹配”:A拿到锁,执行慢,锁自动过期;B趁机获取锁;A结束时却用自己老的value去DEL,结果删掉了B的锁。
- 必须为每个锁生成唯一随机
value(如UUID),释放时用Lua脚本比对value再删除:if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end - 业务耗时不确定时,得加看门狗机制:启动独立线程/协程,在锁过期前一半时间尝试用Lua脚本
GETSET续期(新过期时间),并校验仍是自己的锁 - Redis集群模式下,
SET ... NX不保证原子性——若key落在不同slot,需用Redlock?别试。实际推荐用单节点Redis(主从+哨兵)或支持RedLock语义的Redisson,避免CP权衡带来的复杂度
MySQL + Redis锁配合的关键断点:哪里读、哪里写、哪里校验
典型场景:用户下单扣库存。不能“先Redis加锁→查MySQL库存→扣减→删锁”,因为查库存和扣减之间仍有窗口——其他请求可能已修改库存但未提交,或你读的是旧快照(RR隔离级别下)。必须让MySQL承担最终一致性校验。
正确做法是把Redis锁当作“粗粒度协调器”,MySQL承担“细粒度原子操作”:
- Redis锁范围尽量小:按商品ID锁,而不是锁整个库存表或用户ID
- MySQL执行扣减必须用
UPDATE stock SET quantity = quantity - 1 WHERE product_id = ? AND quantity >= 1——把条件判断和更新合并为一条语句,返回affected_rows是否为1 - 只有
affected_rows == 1才算真正扣成功;否则说明库存不足或已被他人扣完,此时应立即释放Redis锁并返回失败,而不是重试或忽略 - 不要在Redis锁内做耗时操作(如调第三方API、生成PDF),锁持有时间越短越好,否则成为系统瓶颈
Redis锁失效时,MySQL怎么兜底不丢数据
网络抖动、Redis短暂不可用、客户端崩溃……任何环节都可能导致Redis锁没加上或没释放。这时候如果MySQL毫无防御,就会裸奔。
兜底的核心是:**让MySQL自己拒绝非法状态变更**,而不是依赖外部锁保证顺序。
- 所有关键字段加校验列:比如订单表加
status和version,更新时用UPDATE order SET status = 'paid', version = version + 1 WHERE id = ? AND status = 'unpaid' AND version = ? - 库存表必须有
CHECK (quantity >= 0)约束,并开启严格SQL模式,让UPDATE在超扣时直接报Check constraint violation而非静默失败 - 关键业务表加唯一索引防重:如支付流水号
pay_no设为UNIQUE,重复插入直接报Duplicate entry,比锁更可靠 - 不要用
SELECT ... FOR UPDATE去“提前占位”,除非你100%确定后续一定执行UPDATE——否则空锁浪费资源,还可能阻塞别人
分布式锁的本质不是“保证顺序”,而是“降低冲突概率”。真正扛住并发的,永远是MySQL里那条带条件的UPDATE语句和背后的约束。Redis锁只是帮你在高并发下少走几趟数据库而已。


















