由之前的文章,我们可以看到数据库为保证数据持久化,需要落盘,而该操作将会成为秒杀系统的瓶颈所在。那在数据库层面如何进行优化呢,可以分为以下几点来考虑:
库存拆分
将同一个商品的库存记录拆分为多行甚至多个表,降低并发冲突。举一个简单的例子:对业务请求中的userId计算hash取模后确定查询哪个库那张表的哪行记录,然后在做库存更新操作。这样能够在业务层极大的降低并发冲突,不需要数据库做相关优化,是成本较低收效较大的一种方案。但是该方案也有一个缺点:由于同一个商品的库存记录分散到了不同库表中,那这些商品的库存扣减速度不均衡(热点商品在短时间内被秒光,这个问题并不严重),给总库余额计数带来的复杂度。同时这个优化又要求业务提前感知到哪些商品是热点商品,并制定有针对性的拆分库存操作。
批处理
修改数据库内核代码,将相互冲突的事务,合并为一个事务或者优化为组提交,达到批处理的效果,AliSQL的做法是在MySQL server层识别这类update语句,将它们解析后合并成为一条SQL再执行。比如10个扣减库存语句,合并为一个扣减库存的语句一次性扣减数量为10,这样相当于提高了数据库10倍的吞吐量。
这个做法的优势是对数据库内核代码修改不多、复杂度可控,劣势是只能在特定语句的基础上进行优化,没有比较好的普适性。
请求排队
在数据库内核层面引入上述“批处理”的优化后,对热点数据的并发扣减库存业务仍然会面临多个事务并发进入临界区的情况,并发等锁的事务会占据宝贵的连接和线程资源。为避免并发事务因抢占锁而造成的资源浪费,可以在数据库内核层面将冲突事务排到一个队列处理,这样就可以解决并发冲突,降低系统负载。
存储过程或类似命令
业务复杂的场景里,一个事务往往可能包含多个语句,这样就会扩大临界区,严重影响并发度。一个最有效的方案是数据库层面支持存储过程,多个语句放在存储过程里一次性提交给数据库;但是MySQL并不支持存储过程,因此可以针对具体场景引入一些类似存储过程的优化,当然核心仍然是将一个事务中的多条语句合并,实现与数据库在一次交互中完成。比如AliSQL的Commit on success,可以用在扣减库存+生成订单的场景中,即开启事务后先执行几乎没有并发冲突的insert语句生成订单,然后带上Commit on success标记执行扣减库存命令,库存扣减成功后就立即提交事务,不需要等待客户端再发commit,这样一来热点行冲突的临界区仍然与单行事务一样了。