这是一个典型的数据库热点数据更新的问题。
我们都知道,当多个并发事务同时尝试更新同一行热点数据时,可能会导致锁竞争和冲突。这会增加事务的等待时间和冲突概率,导致性能下降,并可能引发死锁问题。
详见以下两个问题,都是热点数据并发更新导致的:
这类问题的解决思路可以分为3类,分别是排队、拆分以及批次执行。
首先,我们常见的排队方案就是要么加锁、要么单线程执行。
随意,如果能使用Redis的话,那么就可以利用他的高并发、单线程特点来解决这个问题,因为题目要求是只能使用数据库,所以这个就不展开讲了,这种方案,在秒杀场景中用的还挺多的,可以参考:
除了上面这种基于Redis排队的方案,其实基于数据库也是可以的。
正常的update竞争抢锁的时候,也是排队的,但是这个会让事务持续自旋抢锁,严重耗费数据库CPU
给大家提供一个思路,那就是在数据库层面做改造。比如腾讯云Mysql和阿里云Mysql都做过类似的优化。
腾讯云数据库MySQL热点更新: https://cloud.tencent.com/document/product/236/63239
阿里云数据库Inventory Hint: https://www.alibabacloud.com/help/zh/apsaradb-for-rds/latest/inventory-hint
主要思路就是,针对于频繁更新或秒杀类业务场景,大幅度优化对于热点行数据的update操作的性能。当开启热点更新自动探测时,系统会自动探测是否有单行的热点更新,如果有,则会让大量的并发 update 排队执行,以减少大量行锁造成的并发性能下降。
也就是说,他们改造了MySQL数据库,让同一个热点行的更新语句,在执行层进行排队。这样的排队相比update的排队,要轻量级很多,因为他不需要自旋,不需要抢锁。
阿里的数据库还提供了事务性Hint来控制并发和快速提交/回滚事务,提高业务吞吐能力,上面的链接中也有介绍,可以自行了解下。
具体原理参见:
除了排队,还可以做一些拆分操作,比如说把一个大的库存拆分成多个小库存,有点类似于concurrentHashMap中的分段锁的机制,通过拆分的手段降低锁的粒度来提升并发度。
拆分后,一次扣减动作就可以分散到不同的库、表中进行。
但是这个方案存在一个比较大的问题,那就是可能会存在碎片。
假如一共有1000个库存,在一起的话,是可以扣减999个的。但是如果分散到多个子库存中,那么就会没办法一次性扣减999。
当然,也是可以通过一些手段来解决的,比如最库存的腾挪或者超占等,但是整体看上去方案并不完美。所以这个方案用得不多。
除了拆分以外,还有一种合并批量执行的方式。
多条update在一起会有锁争抢,那如果把多个UPDATE合成一个UPDATE不就可以降低锁冲突了么。
比如一个用户,有10个占用库存请求,每次占用1个,那么就可以提供一个批量占用的接口,让上游一次性把10个占用合并一起,这样数据库只需要做一次更新就行了。
这个方案有一些局限性,就是不是所有请求都可以合并的,有些场景,如电商的秒杀,用户需要很快的知道反馈,而批量执行就需要有个窗口来聚合,用户是不能接受这种等待窗口的。
所以,在一些异步链路上,是可以用这种方案的。
当然,还有一些其他的方案,在一些简单场景也可以考虑。
1、异步化,通过异步更新的方式,将高并发的更新削峰填谷掉。
2、把update转换成insert,直接插入一次占用记录,然后异步统计剩余库存,或者通过SQL统计流水方式计算剩余库存。
3、SQL限流,这是一种保护数据库的手段,就是不要让这些请求都打到数据库上。提前拒绝掉。