从秒杀系统窥见数据层的解耦设计

在开始之前,我们先思考一个问题:秒杀系统的本质是什么?

1 秒杀系统的问题本质是什么?

1 秒杀系统的两大特点:大流量、高并发;

2 电商系统 = 数据库的增删改查

3 电商系统 = 小请求量 + 秒杀系统

4 秒杀系统 = 海量请求 + 电商系统

其实说白了,秒杀系统不过是一个瞬时流量很大的电商系统而已。在普通的电商系统中,数据库并不太算是一个短板,因为数据量和请求速率并不能压垮数据库。但是在秒杀系统中不一样,如果直接把海量的请求和数据扔给数据库,数据库会立刻原地爆炸的。

1.1 数据库在高并发下会遇到什么问题?

  1. 首先,MySQL 自身对于高并发的处理就有性能问题。一般来说,MySQL 的处理性能会随着并发 thread 上升而上升,但是到了一定的并发度之后会出现明显的拐点,之后一路下降,最终甚至会比单 thread 的性能还要差;
  2. 其次,超卖的根结在于减库存操作是一个事务操作,需要先 select,然后 insert,最后 update - 1。最后这个 -1 操作是不能出现负数的,但是当多用户在有库存的情况下并发操作,出现负数这是无法避免的;
  3. 最后,当减库存和高并发碰到一起的时候,由于操作的库存数目在同一行,就会出现争抢 InnoDB 行锁的问题,导致出现互相等待甚至死锁,从而大大降低 MySQL 的处理性能,最终导致前端页面出现超时异常。

1.2 所以秒杀系统的问题本质是什么?

保证海量请求情况下数据库的可用性。

2 可以从哪些方面提升数据库性能?

2.1 引入队列,串行处理

引入队列,然后将所有数据库写操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不向消费队列继续添加请求,并关闭购买功能,这就解决了超卖问题。

优点:解决超卖问题,略微提升性能。

缺点:性能受限于队列处理机处理性能和 DB 的写入性能中最短的哪个,另外多商品同时抢购的时候需要准备多条队列。

2.2 两段式操作

将提交操作变成两段式,先申请后确认。然后利用 Redis 的原子自增操作,同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。然后数据异步更新到DB中。

优点:解决超卖问题,库存读写都在 Redis 中,故同时解决性能问题。

缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。

2.3 写操作前移到内存中

将写操作前移到内存中,同时利用 JVM 的轻量级的锁机制来实现减库存操作。

优点:读写在内存中,操作性能快,引入轻量级锁之后可以保证同一时刻只有一个写入成功,解决减库存问题。

缺点:加锁之后对性能有影响,而且内存中如果堆积大量数据会造成严重的内存资源占用,降低单机承载能力。

2.4 读写 Redis,黑盒数据层

业务层 => Redis <==> DB

将存库从 DB 前移到 Redis 中,所有的读写操作放到 Redis 上,业务层不直接读写数据库,所有的数据同步由数据层进行封装并同步。由于 Redis 中不存在锁故不会出现互相等待,并且 Redis 的读写性能都远高于 MySQL,这就解决了高并发下的性能问题。最后通过队列等异步手段,将 Redis 中变化的数据异步写入到数据库中。

优点:极大程度上解决性能问题,结合 Redis Lua 的原子操作可以解决超买问题。

缺点:数据从 Redis 同步到 DB 有延迟,需要通过额外手段保证数据一致性,避免数据的脏读。

IInfinity

IInfinity

大道虽简,知易行难。
CN