J9游戏|Home

国家高新技术企业
服务热线:400-6688-605
Mysql数据库怎么优雅的解决并发超卖的问题?
发布来源:J9游戏
发布时间:2026-06-2809:58

大部分人▣遇到的并‣发问题,根本不需▣要分布式··锁、不需要Redis、不需要消♧息队列。一条写对❀的SQL就够了。

但问题是,大部分人··写不对这〓条SQL。

那条”看起来没问题”的代码

库存扣减,最常见的★写法:

@Transactional

public void buyProduct(Long productId) {

    Product product = productDao.selectById(productId);

    if (product.getStock() > 0) {

        product.setStock(product.getStock() - 1);

        productDao.update(product);

    }

}

加了 @Transactional,先查后改,还判断了⊕⊕库存大于0。看起来滴♦水不漏。

但这段代■码在并发✩下一定会◈超卖。

原因在于”先查后改”这个模式▪本身就有✪问题。A和B同时进来,都读到库••存是1,都通过了 stock > 0 的判断,都去执行 stock - 1。结果库存❀❀变成了-1。

有人会说:不是加了♡事务吗?事务不是▣保证原子◣性吗?

这是对事✩务最大的❖误解。MySQL默认的REPEATABLE READ隔离级别,读的时候✧拿到的是▾快照(snapshot),不加锁。两个事务▲可以同时∷读到 stock=1,各自觉得✪可以扣。等到执行UPDATE的时候MySQL确实会加✯行锁,但问题是 if (stock > 0) 这个判断▸是在应用✪层基于快►照数据做♡的,锁都没来‣得及介入,判断就已❀经通过了。

两个事务✯✯各自读到 stock=1,各自通过✩了应用层◢的判断,各自提交——每一步都▣▣在事务里,每一步都”正确”,但组合起▸来就是个bug。这就是竞❖态条件(race condition),问题出在”基于快照♢做判断,基于当前▵值做更新”这个错位❖上。

最简单的正确写法

别先查再■改,直接改:

UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0

一条SQL,没有中间✪状态。stock = stock - 1 是在数据❖库引擎内‣部完成的,MySQL会对这行▹加行锁,保证同一♦时刻只有❋一个UPDATE在执行。stock > 0 确保不会▣扣成负数。

执行完看 affected rows,返回1就是扣减✪成功,返回0就是库存❖不够。

就这么简◥单。不需要version字段,不需要 SELECT ... FOR UPDATE,不需要任∷何额外的✫锁机制。对于绝大◇多数业务▽系统——QPS几十到几•百的那种——这一条SQL就是最优◢解。

很多人不▲信。觉得并发▼丨丨问题♦哪有▾▾这么简单,肯定要上❉点”高级”的东西才‣靠谱。

但你想想,MySQL的InnoDB引擎处理▣行锁已经♦♦二十多年◇了,这是它最✩基本的能▾力。你不需要◣在应用层▣重新发明❈一遍锁机◇制。

什么时候这条SQL不够用

当同一行✭数据的写•入竞争非▴常激烈的丨时候。

比如一个❈❈丨爆款商‣品,每秒有几✦千个请求❋都在扣同♧一行库存。虽然MySQL能保证正❖确性,但所有请▽求都在排◎队等这一✭丨行的行∷锁,吞吐量上⊙不去。数据库连◉接池被占▼▼满,连带着其▿▿他不相关▣的查询也▿变慢了。

这时候有▾两个方向。

一个是乐◤观锁。表里加个version字段:

UPDATE product 

SET stock = stock - 1, version = version + 1

WHERE id = 123 AND version = 10 AND stock > 0

读的时候◈◈不加锁,谁都可以○读。更新的时■■候靠version字段做冲⊙突检测——只有version匹配的那♧个请求能♢成功,其他的拿✯到 affected rows = 0,业务层决☆定是重试◤还是放弃。好处是读✭不阻塞,坏处是冲◇◇丨突激烈❖时▼大量请求※在空转重◎试,数据库CPU会飙上去。

另一个方▽向是把热◥点数据从◎数据库里✩搬出来。库存预加▴载到Redis,用 DECR 扣减:

DECR product:123:stock

Redis单线程执▾行命令,DECR 天然原子。返回值 >= 0 就是成功,< 0 就是没库△存了。绝大部分✭✭请求在Redis这一层就·被过滤掉☆了,只有扣减✫成功的请❖求才会去▿▿写数据库。

这也是各◤◤大电商秒▽杀系统的✿基本思路。阿里在2019年的一篇技▲术博客里‣提到过,双11的库存扣♦减全部在‣缓存层完▹成,数据库只✦做最终的▵异步持久♢化。

但代价是❈系统复杂△度翻了好▪几倍:Redis和数据库∷的一致性✩怎么保证?Redis扣成功了►数据库写✩失败怎么♦♦补偿?Redis宕机了库✦存数据怎◉么恢复?每一个问•题都能再✯写一篇长△文。

一个很多人不愿意承认的事实

大部分系♢统根本到•不了需要Redis扣减库存◣的量级。

一个内部▲管理系统、一个小型✧电商、一个课程◤设计项目,并发量可♦♦能也就个‣‣位数。这种场景◈下搞分布❈式锁、搞Redis预扣减、搞消息队♡列异步补◣偿,就像开着■■坦克去菜♦市场买菜。

技术选型❉❉最难的不∷是”怎么解决▴▴问题”,而是”克制住不〓去解决不❋存在的问■■题”。

Knuth那句”过早优化❋是万恶之⊙源”都被说烂▾了,但很多人❈只记住了✦优化,没想过架◆构设计也▾▾一样。你为一个◥日活200的系统设◤计了能扛◆住10万QPS的库存方▸案,这不叫技♡术能力强,这叫浪费◦时间。

那条带 WHERE stock > 0 的UPDATE,够用就先☆☆用着。等哪天真❋的扛不住❉了,你自然会►知道该往◢哪个方向★演进——因为到那◇时候,监控和日▴志会告诉◆你瓶颈在❀哪。