系统设计之秒杀
系统设计之秒杀
主要的问题
- 高并发
- 超卖
高并发
秒杀特点是瞬时用户量大(每秒上万甚至几十万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)

超卖
说明: 针对超卖问题(并发访问,修改共享资源),通常对于大流量的场景并不推荐直接在数据库层面加锁,防止流量击垮数据库。
比如三个用户a,b,c要购买某个商品12345, 先查询库库存,如果库存足够可以下单,最后去更新库存。
并发情况读取的时候可能都发现库存足够,三个trx 中的upate在数据库层面会串行化(排它锁),一次去扣减库存,可能导致库存为负数 - 超卖
select amount from s_store where prodID = 12345 |
数据库乐观锁
数据库记录中添加版本字段,读取的时候一并读出版本,每次修改版本+1, 修改数据时对比版本。 缺点是应用层面的控制,如果是不同应用访问数据库则不work。
--查询数据 |
数据库悲观锁
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