本文介绍了笔者的一个提高扣库存接口QPS的思路。
先来说下我司扣库存的业务场景:我司某个商品的库存量可能会很高(比如几十万),对于这个商品,可能会存在类似秒杀的业务场景,大量请求进来后都需要扣库存成功,这个时候,如何提高扣库存接口的性能就成为了重点。
众所周知,库存一般存在数据一张表里,扣库存时会执行类似如下的一条sql
update tb_stock set stock = stock - 1 where id = ? and stock >= 1;
由于数据库行锁等原因,这样的sql并发性能并不高,有些公司会采用把库存分散到多个行中、或者使用redis来扣库存等方法来提高并发,但是这些方法都存在缺点。
笔者最近在思考这个问题时,突然想到mysql数据库有个group commit的特性:
mysql中使用WAL(Write Ahead Log)来实现事务持久性,简单来说就是在提交一个事务前,为了避免磁盘随机写,会先把redo log写到磁盘中并立即flush确保落盘,落盘成功之后才会通知客户端事务执行成功;由于redo log
是顺序写的,能大幅提高性能,但是由于磁盘性能依然是瓶颈,为了提高并发性能,mysql引入了group commit,大概原理就是在写完redo log之后并不马上flush,而是等待短暂的时间,等多个事务的redo log都写完之后再flush,
然后再让这几个事务通知客户端事务执行成功,这样做之后刷盘操作大幅减少,从而使性能得到大幅的提升。
有了group commit特性的借鉴,我们提高扣库存接口性能的思路也就明朗了:
首先我们应该利用异步Servlet,异步Servlet接收到扣库存请求之后,把这些扣库存请求(这个请求内部需要封装当前异步Servlet的上下文信息)放入一个队列中,然后在另一个线程中每隔5至10ms左右从这个队列中获取一批请求(或者达到一次批量请求的上限,比如100个,也强制取出),假如获取到100个请求, 那么就直接执行如下sql:
update tb_stock set stock = stock - 100 where id = ? and stock >= 100;
执行成功后就表示这100个扣库存请求都成功了,此时从这些请求中找出每个请求对应的异步Servlet上下文,并输出扣库存成功的结果;在这种情况下,并发能力差不多提高了100倍。
如果你深入思考过,你可能会问:假如库存数少于100,但业务上希望能充分利用每个库存,该怎么办呢? 我的思路是假如库存数少于100了,这说明剩余库存数不多了,对于接下来的高并发扣库存请求,大概率会是失败的,此时对于这个库存ID,可以降级为采用类似电商秒杀的方案来做(比如对于队列中在排队的请求,直接拒绝大于100个的请求,对于100个以内的请求进行并发处理,每个请求单独扣1个库存,直到有任意一个请求连1个库存都扣不成功后,再次做降级,直接拒绝接下来5秒内的所有该库存ID的扣库存请求)。