flash sale project details

Wed, May 10, 2023 2-minute read

introduce flash sale project details

瞬间高并发 : short time with Highly concurre.

Static websites

用户浏览商品等常规操作,并不会请求到服务端。只有到了秒杀时间点,并且用户主动点了秒杀按钮才允许访问服务端, 过滤大部分无效请求.

CDN

enable users to get access to content nearby, reduce network congestion, and improve user access response speed

heavily read compare to write

use cache redis to check the stock.

那如何保证cache redis的安全性

情况1:当缓存没有数据时,需要从数据库拿第一次数据,一时间太多请求,就同时访问数据库,数据库挂了

redis key 可以解决这问题

client –> product id –> check from cache | – if exist –> flash sale | –if not exist –> get redis key –>got keyget produce info from database

上述更好的方式当然是提前把商品信息放入缓存里面

那还需要加锁吗?

如果缓存中设置的过期时间不对,缓存提前过期了,或者缓存被不小心删除了,如果不加速同样可能出现缓存击穿。多个锁,多个保险

情况2 前面这个缓存和数据库都没有库存了,用户请求会穿透过缓存,而直接访问数据库。加了锁,所以即使这里的并发量很大,也不会导致数据库直接挂掉。但是处理性能不好。

Bloom Filter 过滤

系统根据商品id,先从布隆过滤器中查询该id是否存在,如果存在则允许从缓存中查询数据,如果不存在,则直接返回失败。

那如何保证 Bloom Filter和缓存数据一样?真解决一个问题带来新的问题

可是跨数据源频繁更新数据保证两个保持同步,挺难的,所以

把不存在的商品id也缓存起来

该商品id的请求过来,则也能从缓存中查到数据,只不过该数据比较特殊,表示商品不存在。需要特别注意的是,这种特殊缓存设置的超时时间应该尽量短一点。

库存问题

你拍到了但是钱没了,那这库存怎么办? 回滚回去。

这短时间发生了一系列的库存不足了和超卖问题??

普通情况

update product set stock=stock-1 where id=111;

还伴随着一段代码


int stock = mapper.getStockById(111);
if(stock > 0) {
  int count = mapper.updateStock(111);
<a id="markdown-int-count-%3D-mapper.updatestock111%3B" name="int-count-%3D-mapper.updatestock111%3B"></a>
  if(count > 0) {
    addOrder(123);
  }
}

非原子行,在并发场景下,会出现卖太多的情况。。

不要用synchronized,性能不大好

方法1:乐观锁

update product set stock=stock-1 where id=product and stock > 0;

在高并发的场景下,可能会造成系统雪崩。而且,容易出现多个请求,同时竞争行锁的情况,造成相互等待,从而出现死锁的问题.

方法2:Redis 扣除库存

先判断该用户有没有秒杀过该商品,如果已经秒杀过,则直接返回-1。

扣减库存,判断返回值是否小于0,如果小于0,则直接返回0,表示库存不足。

如果扣减库存后,返回值大于或等于0,则将本次秒杀记录保存起来。然后返回1,表示成功。

高并发下, 会让库存为负, synchronized依然不是好选择。

由于这里是预减库存,如果负数值负的太多的话,后面万一要回退库存时,就会导致库存不准。

方法3:lua脚本扣减库存

lua脚本,是能够保证原子性的

先判断商品id是否存在,如果不存在则直接返回。

获取该商品id的库存,判断库存如果是-1,则直接返回,表示不限制库存。

如果库存大于0,则扣减库存。

如果库存等于0,是直接返回,表示库存不足。

这里就解决了扣减库存问题。

有个情况是,缓存里面没有,大量请求去查一个不存在的商品,会直接连接数据库,引发数据库挂掉。Redis Key lock

redis 分布锁

setNx

该命令不好,和后面的设置超时时间是分开的,并非原子操作。

假如加锁成功了,但是设置超时时间失败了,该lockKey就变成永不失效的了。在高并发场景中,该问题会导致非常严重的后果。

set加锁

set加锁该命令只有一步,所以它是原子操作

释放锁,记录requestId

如果用userId的话,假设本次请求流程走完了,准备删除锁。此时,巧合锁到了过期时间失效了。而另外一个请求,巧合使用的相同userId加锁,会成功。而本次请求删除锁的时候,删除的其实是别人的锁了。

使用lua脚本,它能保证查询锁是否存在和删除锁是原子操作。

业务拆分: mq来了

秒杀 –> 下单 – >支付,三个流程, 真正并发量大的是秒杀功能,下单和支付功能实际并发量很小。所以,我们在设计秒杀系统时,有必要把下单和支付功能从秒杀的主流程中拆分出来,特别是下单功能要做成mq异步处理的。而支付功能,比如支付宝支付,是业务场景本身保证的异步

秒杀 –> 发送mq消息 –》 mq服务器 –》 消费mq消费 –》下单

网络问题,mq服务端磁盘问题等。这些情况,都可能会造成消息丢失

消息发送表

秒杀 –> 写入消息发送表, 初始状态是待处理–>发送mq消息 –》 mq服务器 –》 消费mq消费 –》下单 –》回调生产者的一个接口–》修改消息状态为已处理

秒杀 –> 写入消息发送表, 初始状态是待处理–>发送mq消息 --》 mq服务器 --》 消费mq消费 --》下单 –》回调生产者的一个接口–》修改消息状态为已处理

把消息写入消息发送表之后,再发送mq消息到mq服务端的过程中失败了怎么办?

使用job,增加重试机制。

job --- > 定期查询消息发送表待处理数据 --》 发送mq消息 --》 mq服务器

重复消费问题

消息处理表

秒杀 –> 写入消息发送表, 初始状态是待处理–>发送mq消息 –》 mq服务器 –》 消费mq消费 –》下单 –》回调生产者的一个接口–》修改消息状态为已处理

秒杀 –> 写入消息发送表, 初始状态是待处理–>发送mq消息 –》 mq服务器 --》 消费mq消费 --》查询消息处理表 --》是否存在? --》 否 --》 下单 --》修改消息状态为已处理 |–> 是 –》 返回

下单和写消息处理表,要放在同一个事务中,保证原子操作

垃圾消息问题

每次在job重试时,需要先判断一下消息发送表中该消息的发送次数是否达到最大限制,如果达到了,则直接返回。如果没有达到,则将次数加1,然后发送消息。

这样如果出现异常,只会产生少量的垃圾消息,不会影响到正常的业务

15分钟内未完成支付,订单被自动取消的功能

延迟队列rocketmq

生成订单:此时状态为待支付 –> 发送mq消息 –> 延迟队列 –> 消息mq消息 –》查询订单状态是否为待支付 –》if是 –>取消状态 –> 返回 查询订单状态是否为待支付 –》if否 –> 返回

用户完成支付之后,会修改订单状态为已支付

如何限制刷单

一般情况下,一秒钟只能点击一次秒杀按钮,如果是服务器,一秒钟可以请求成上千接口。

加验证码