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

先说一个核心判断:MySQL的SELECT ... FOR UPDATE或UPDATE ... WHERE自带的行锁,其效力范围仅限于单个MySQL实例内部。一旦你的服务架构走向分布式,比如部署了多副本、引入了读写分离或者实施了分库分表,情况就完全不同了。不同应用节点连接的是不同的MySQL实例(甚至是主从架构中的不同节点),这时,一个节点发出的FOR UPDATE锁,其他节点根本感知不到——节点A锁定了某行数据,节点B照样能执行修改,所谓的行锁在分布式环境下完全失去了协调作用。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
由此引发的现象,想必不少人都遇到过:超卖、重复扣减余额、并发生成重复单号。你以为在事务里加了行锁就万无一失了?其实那只是“本地安全”,在分布式场景下形同虚设。
innodb_lock_wait_timeout参数来“等待锁释放”,这解决不了跨节点的锁冲突。INSERT IGNORE或ON DUPLICATE KEY UPDATE这类语法来替代分布式锁——它们只能防止重复插入,却保护不了“先读、再判断、最后写”这类复合逻辑(比如经典的“查询余额→判断是否足够→执行扣减”流程)。SELECT ... FOR UPDATE发到了从库,那么结果要么是直接报错,要么就是静默地失败,锁根本没加上。于是,大家很自然地转向Redis来实现分布式锁。核心命令是SET key value EX seconds NX:设置一个带过期时间的唯一值,并且仅在键不存在时操作成功。然而,如果只做到这一步,那离真正的安全还差得远。网络分区、业务执行超时、锁被意外删除,任何一个环节都可能让数据陷入不一致的境地。
其中最容易踩的坑,莫过于“锁释放不匹配”:请求A拿到了锁,但由于业务执行缓慢,锁在过期时间后自动释放了;此时请求B趁机获取了锁;可当请求A最终执行完毕时,却用它自己持有的那个旧的value去执行删除,结果误删了请求B的锁,导致锁保护彻底失效。
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
GETSET操作来续期(设置新的过期时间),并且必须校验当前持有者是否仍是自己。SET ... NX命令并不能保证跨槽位的原子性。这时候有人会想到Redlock算法,但实话实说,除非你非常清楚其背后的复杂性和CP权衡,否则不建议轻易尝试。实践中更推荐使用单节点Redis(配合主从和哨兵)或者直接使用实现了RedLock语义的成熟客户端(如Redisson),以避免引入不必要的复杂度。我们来看一个典型场景:用户下单扣减库存。一个常见的错误流程是“先在Redis加锁→然后查询MySQL库存→执行扣减→最后释放Redis锁”。这个流程的问题在于,查询库存和实际执行扣减这两个操作之间,仍然存在一个时间窗口——其他请求可能已经修改了库存但尚未提交,或者你读取到的可能是一个旧的快照(特别是在RR隔离级别下)。
正确的做法是,让Redis锁扮演“粗粒度协调器”的角色,而将“细粒度原子操作”的职责牢牢交给MySQL:
UPDATE stock SET quantity = quantity - 1 WHERE product_id = ? AND quantity >= 1。将条件判断和数值更新合并到一条SQL中,并通过判断返回的affected_rows是否等于1来确定是否成功。affected_rows == 1才代表扣减真正成功;否则,就意味着库存不足或已被其他请求扣完,此时应该立即释放Redis锁并返回失败,而不是盲目重试或忽略。分布式环境充满不确定性:网络抖动、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做“提前占位”:除非你能百分百确定后续一定会执行UPDATE操作,否则这种“先锁住再说”的做法,不仅浪费连接资源,还可能无谓地阻塞其他正常请求。说到底,分布式锁的本质目标并非“严格保证操作的全局顺序”,而是“在分布式环境下,尽可能地降低并发冲突的概率”。真正扛住高并发、保证数据最终一致性的,永远是MySQL里那条带条件的UPDATE语句,以及表结构背后那些坚实的约束。Redis分布式锁,更多时候只是在高并发洪峰前,帮你减轻数据库压力、少走几次弯路的“协调员”而已。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述