高并发下作余额扣减的一些经验
前一段时间参加了优化一个老的计费系统,学习了一些高并发下做余额扣减的常用手段,也做了一些尝试,因此在这里总结记录一下。
问题描述
对于一个计费系统来说,并发问题事实上分为两类,一类是应用并发高,也就是纯粹的用户量大,访问量多,这类问题和一般的高并发问题没有区别,用分布式等手段就可以解决;另外一类问题则是一般分布式手段无法解决的用户高并发问题,也是本文要着重说的。
这类问题源自对某些高频账号,大量的并发访问,会导致瓶颈首先出现在某些数据库记录上,大量操作由于无法竞争到数据库的行锁而导致等待,这些等待中的操作又会占用其他资源,最终导致系统不可用。
针对这类问题,下边介绍一些常用的处理办法。
不设置余额字段
由于对于一个稳定的计费来说,一定是会记录计费流水明细的,所以完全可以不设置余额字段,而采用根据流水明细计算的方式来获取余额。
不过这种方法不是万能的,比如拿广告业务的计费系统来说,频率非常高,而每次的金额很小,这时候想通过计算求和去算余额,显然是不现实的。
合并与拆分
这是两种方式,因为有一些相似之处,都是要降低对单条数据库记录的访问压力,所以也就放到一起说了。
合并,就是对单个账号的数次请求作合并处理,再往数据库写,这样就等于降低了数倍的压力。
拆分,则是把一个主账号拆分成数个子账号,然后把请求分配到各个子账号上,这样单个账号的压力就小了。然后再用其他手段把子账号的数据合并成主账号数据,返回给用户。
减少行锁占用时间
这是个代码层面的优化,前面说了,高频账号之所以会导致系统的性能问题,就是因为要竞争行锁,所以,如果我们能减少每次请求占用行锁的时间,系统性能也就会大幅度提升了。
所以,首先,要尽量加快从获取行锁到事务提交这个阶段的运行速度,将不必要的操作,尤其是一些耗时的操作,放到其他地方执行,比如获取行锁之前或者事务以外。
然后,尽量避免用
select ... for update
的方式去获取行锁,二是采用下面这种方式:
update xxx set amount=amount-1 where id=x and amount>=1
如果业务层面允许余额扣减成负数的话,就可以不使用where条件中对金额的校验;否则就需要在将要把余额扣成负数时不去更新数据库,并在程序中返回异常。
限流
既然高频账号是单账号的并发达到一定程度后才会导致系统的性能问题,所以我们就可以强制控制这个并发量,使它永远保持在系统可接受的范围内。
缓存
缓存也是一个常用的解决高频账号的方法,在缓存中对余额作操作,然后定时向数据库内同步。
上边介绍了很多方法,当然每一种都会有它的适用条件,也会有其局限性。比如像合并啊,限流啊,实际上都会造成延迟扣费,而在延迟的这个时间段内,可能账户余额已经耗尽,所以如果是严格余额不能为负也不能丢弃记录的业务场景下,其实并不适合用这种方式。
所以说,最重要的还是根据业务场景选择合适的方案。