系统设计之秒杀

系统设计之秒杀

主要的问题

  • 高并发
  • 超卖

高并发

秒杀特点是瞬时用户量大(每秒上万甚至几十万QPS),如果请求都进到数据库层,数据库会崩溃。

主要的解决思路就是不要让并发流量进到数据库层, 层层过滤,逐层减轻瞬时流量压力,利用缓存和消息队列(削峰)。

Redis

redis并发承受能力较高,进一步使用redis 集群(eg sentinel)主从同步,读写分离,提升并发处理能力。

秒杀涉及到的操作:查库存,扣减库存。 可以提前把商品库存加载到redis中,整个秒杀流程可以再redis中做,等秒杀结束了可以异步去数据层真正修改库存。

  • redis 乐观锁,使用事务 watch key, 如果有变化(说明别人改了),则事务失败 ,秒杀请求失败
  • 使用lua脚本 实现类似redis 事务,保证查库存和修改库存的原子操作: 使用脚本原子化查库存+扣减库存,如果库存为0了后面的请求全部返回失败

MQ 消息队列

同步转异步,削峰

秒杀成功的请求进到消息队列排队,根据后面数据库的压力可以定制化消费者的数量; 消费者拿到请求后开始创建订单,扣减库存等数据库操作。

另外设置超时处理,例如超时未付款,恢复商品库存。。。

限流

前段

例如点击按钮后,N秒内不能再次点击。

拦截高频重复请求

网关或者web 服务层实现,来控制同一用户一段时间内请求此时

  • 可以通过redis expire 来做, set. userID value + expire , 请求到来是先get userID,如果查不到,表示为有效请求,放行

  • webserver 或网关 的相关功能support ,eg nginx limit_req来限制同一个IP的每分钟的访问次数,来屏蔽高频恶意请求IP,可以减轻服务器压力。

接口限流

例如令牌桶限流算法,每个请求需要先获取令牌 ,令牌是按照一定恒定速率产生。

eg: guava 中的ratelimiter

  • aquire,issue: 没有获取到请求会一直等待
  • tryAcquire 指定timeout, 等待超时直接拒绝

熔断与降级

应对秒杀过程中某个服务宕机或不可用,可以通过例如Hystrix熔断,备用服务 (例如友好的提示)而不是卡死。

设计优化

URL 连接防止暴露,加盐 hash(类似密码处理),前段访问后台获取url,后台校验

服务单一职责,单独的微服务,即使秒杀系统挂了也不会影响其他系统。

静态资源的处理,前后端分离,动静分离,页面静态资源只是走前段web服务器而不会访问应用服务器,CDN福服务器加速。

可以通过增加大体,削减峰值

数据库设计,单独秒杀数据库(包含秒杀商品表和秒杀订单表),防止秒杀影响其他业务数据库。

另外处理请求的的web服务器,选用高性能的,例如nginx ,反向代理route 到后端的应用服务器集群(集群可弹性伸缩,例如临时添加instance)

nginx

超卖

说明: 针对超卖问题(并发访问,修改共享资源),通常对于大流量的场景并不推荐直接在数据库层面加锁,防止流量击垮数据库。

比如三个用户a,b,c要购买某个商品12345, 先查询库库存,如果库存足够可以下单,最后去更新库存。

并发情况读取的时候可能都发现库存足够,三个trx 中的upate在数据库层面会串行化(排它锁),一次去扣减库存,可能导致库存为负数 - 超卖

select amount from s_store where prodID = 12345
update s_store set amount = amount - quantity where prodID = 12345

数据库乐观锁

数据库记录中添加版本字段,读取的时候一并读出版本,每次修改版本+1, 修改数据时对比版本。 缺点是应用层面的控制,如果是不同应用访问数据库则不work。

--查询数据
SELECT iD,val1,val2,VERSION
FROM theTable
WHERE iD = @theId;
--计算新值
UPDATE
theTable
SET
val1 = @newVal1,
val2 = @newVal2,
VERSION = VERSION + 1
WHERE
iD = @theId
AND VERSION = @oldversion;
--判断影响行数
-- {if AffectedRows == 1 }
-- {继续执行}
-- {else}
-- {数据过期}
-- {endif}

数据库悲观锁

SELECT ...for update 悲观锁, for update仅适用于InnoDB,且必须在事务块(BEGIN/COMMIT)中才能生效

eg select status from t_goods where id=1 for update; 这个时候ID为1的记录被锁定(排它锁),这样其他要修改该记录trx 需要等待本次事务提交结束才可以执行; 另外的事务中如果再次执行select status from t_goods where id=1 for update; 则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,如果是在第二个事务中执行select status from t_goods where id=1;则能正常查询出数据,不会受第一个事务的影响。

说明: mysql innoDB支持行锁,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。

分布式锁

例如两个用户同时买商品A,对于修改库存是 要可以根据商品名称作为KEY 来通过redis (px + nx) 加分布式锁,并行转顺行,保证强一致。

问题在于吞吐率差,操作串行化(查库存,生成订单,扣减库存),假定需要20ms, 则一秒只能处理50个请求。

改进: 分段锁

java 8 longadder相对于atomicLong Cas 在高并发场景下的乐观锁失败问题, longadder 采用类似分段cas, 失败自动迁移到下个分段继续cas

因此可以把库存表分成多个字段,eg stock1,stock2 ... stocks

  • 使用随机算法(eg hash)请求分不到不同分段
  • 如果当前分段库存不足,释放当前分段锁, 然后重新加下个分段锁,重试。

https://mp.weixin.qq.com/s/z2S1EjWQDwKm5Ud36IenNw

https://mp.weixin.qq.com/s/CroroiIrmT3yxlVrEXwH6Q

https://mp.weixin.qq.com/s/ybt6bOKR1fhrB-OsTNICXA