场景描述
秒杀,抢coupon,大转盘等业务,会吸引大量用户同时高并发访问,而这些业务都需要对数据库有写操作,在高并发下,这个类似的场景一般都有下面特点:
一、写库前逻辑校验限制,比如秒杀的库存检验,coupon发放量检验等逻辑复杂。
二、更新数据库时候,集中更新在某些特定的记录上面,造成少量数据同时大量写请求。
抢coupon写操作的实现方案
分布式锁方案
这种方式是最常用的方式,一般并发量不大的时候适合用这种方式,这种分布式锁特点:
- 实现简单:通过第三方的原子操作锁(redis, zookeeper)在业务开始之前加锁,业务结束后释放锁来完成。
- 性能低:在分布式锁的这个阶段中,需要相同锁的资源相当于串行执行。锁中间的业务耗时多,或者锁的粒度大会严重降低服务性能。
关键点数据校验DB乐观锁方案
这个方式在业务逻辑上,区分上一种方式,将校验数据部分区分开,找出关键点数据的校验。关键点数据或者叫做易变数据,是指经常变化的,一般每次写操作都会导致这个数据发生变化。
找出这个关键点数据,将关键点数据的校验和更新操作合并为一个数据库原子操作,例如库存检查操作update table set count = count-1 where count-1 > 0 and item=?,将这个检验更新操作,在数据库层面添加乐观锁方式实现,如果执行更新成功,再执行后续操作,如果失败表示校验失败,不通过检查。
特点:
- 业务实现不复杂,关键是找出可以合并更新为一个的原子数据库操作。找不出的话就不适合这个方案。
- 性能瓶颈点在于更新操作,一般是大量并发,修改相同的记录,例如大量用户领取相同的一种优惠券,关键点合并检查更新在数据库里面都会串行等待这个记录的写锁。
关键点数据横向分表校验DB乐观锁方案
本方式是优化了上面方案的关键点并发更新的情况,扩展数据库中的关键点数据表,从一个数据表横向扩展为10,100个,这样相比于上面方案,这个的并发量相当于提升了10倍,100倍。
在数据创建阶段就将关键点的数据(库存,领取量)按照一点规则分配到各个表中。在执行绑定coupon的时候,会随机或者其他规则从一个关键点表中更新数据,如果不成功,重试2次其他数据表更新。
特点:
- 性能提升:通过横向扩展表,将并发提高n倍
- 实现比较服务,在数据库层面将关键点数据横向拆分成多表数据,业务处理时候要更新不同表中数据。
- 数据可靠:上面几种方式都是直接操作数据库,在极端情况下服务宕机不可用,数据都会保存数据库中。
通过redis原子操作校验关键点数据方案
该方案是通过redis高性能incr, decr原子操作实现关键点数据检查的方式实现。在数据创建时候将关键点数据保存到redis中,处理业务时候,执行到检查易变数据时,直接更新通过redis.decr操作校验数据,通过后将后续操作通过可靠MQ异步处理。MQ的消费端在更新关键点数据时候,将多条记录合并一次更新,比如50条扣减数量,一次update更新,提高数据库的写性能。后续的其他数据库操作也都批量操作。
特点:
- 数据可靠性依赖redis, mq,要保证redis可靠稳定。
- 数据库层面不需要扩展,业务相对简单些。
无关键点数据检查更新合并操作的业务
这个情况,不能将关键点数据的检查和更新使用同一个数据库原子操作完成,咱们现在coupon服务就是这种情况,每个coupon的已经发放数量在 coupon表里面没有记录,而是通过每次count关联表来计算出来的,这个关键点数据检查就是每次绑定前都count关联表数量,然后对比发放最大人群数量。这种情况都是在获取数量前加锁,领取完成后释放锁,性能很低。
该方案是上面方案的简化方式,通过在数据创建阶段,将初始数据更新到redis中,每次有领取coupon的请求过来,在关键点数据检查时候,redis.incr/decr方式,判断数据是否检查通过。通过之后执行后续数据库操作,这种情况不存在高并发修改少量数据情况,性能不受写锁限制。
特点:
- 数据可靠性依赖redis,要保证redis可靠稳定。
- 方案简单。
- 查询数据略慢,需要每次count关联表数量。