Nginx+lua实现秒杀

一、秒杀业务特点

1.瞬时高并发

秒杀商品库存一定是有限且价格超级优惠,一定会在秒杀开始的瞬间就会结束,如最近的武汉消费券,基本上在1s内就瞬间抢空了。

2.热点数据

秒杀商品一定会有很多人关注,访问频率会非常高,需要单独存储。

3. 读多写少

秒杀业务是典型的读多写少的场景,假设秒杀商品是iPhone13秒杀价格是1000元,
但库存只有100个,来抢购的人一定是数以几十万计,但最后能成功下单的用户只有100个,
即订单系统实际写成功的次数只有100次。

二、技术难点

1 . 数据一致性

根据以上分析的3个业务特性,为了解决瞬时大量读及系统吞吐量问题,
秒杀商品必须要用缓存技术,那么该如何在不影响系统吞吐量情况下解决缓存和DB一致性问题?
这里缓存技术我会选用业界主流的redis,根据以下库存超卖解决方案,
当库存扣减成功后,采用MQ异步解耦方式发消息给订单服务处理实际下单扣减库存业务,
只要保证MQ消息可靠性(不丢消息)就可以实现缓存和DB数据的最终一致性。
这里我会选用RocketMQ(它支持消息可靠性、消费端限流和延迟队列)。

2. 库存超卖

超大瞬时高并发下,如何保证库存不超卖?
扣减库存需要做2件事:1. 扣减库存;2. 判断库存是否足量。如果能保证这2件事的原子性就可以
保证库存不超卖问题,那么新问题来了如何在分布式环境下保证扣减库存和判断库存的原子性?
对此也有2个方案:1.分布式锁;2.redis+lua脚本(lua脚本在redis可以保证原子性)。
分布式锁是悲观锁会阻塞其他请求,如果考虑吞吐量这块则我会选择redis+lua脚本的方案。

三、秒杀注意事项

1.数据预热

这里会选用redis的hash来作为热key存放秒杀商品(sku_id:库存数;start:0,布尔值表示活动是否开始),
会在活动开始前刷到存放热key的redis中,当活动时间开始后通过定时job将start值刷为1活动正式开始,
这里定时job我会选择xxl-job。

2.请求承载

对于超大流量下业务,需要考虑系统的负载均衡、并发承载数、最大连接、重试策略、熔断机制及系统的隔离等等问题,
这些需要调研数据来圈定负载均衡、并发承载数及最大连接数的边界,
这里可以将秒杀服务单独抽离出来,即可以保证服务隔离性又方便其横向扩展保证他的承载能力,
我会选择阿里的Sentinel来灵活保证秒杀服务的可靠性,它支持限流、熔断、服务降级等策略、业务完全解耦及可视化的web界面。

3.请求拦截

我思考的主要分为4部分:

  1. 前端页面:页面缓存,减少对后端服务压力 ;按钮置灰,防止用户重复提交;验证码,流量错峰(这个就特殊业务会用,如12306)。
  2. nginx:流控,防止比预期大流量直接冲垮系统;白名单,防止用工具模拟请求恶意抢单;缓存,静态资源缓存,如图片、js等(有预算的话也可以用CDN加速静态资源)。
  3. gateway: token鉴权,过滤无效请求;验签,防止用工具模拟请求恶意抢单。
  4. Sentinel:流控、熔断及服务降级,粒度更细保护秒杀服务,保证秒杀服务的可靠性。

大厂会有专门的风控团队帮忙处理请求拦截问题。

四、微服务秒杀设计

根据以上业务分析和技术思考,我画下了如下的架构设计图:
Nginx+lua实现秒杀_第1张图片

补充说明以上的架构设计图:
一、系统瓶颈点
	1. 秒杀微服务性能瓶颈,需要预估最大支撑的流量,以便更好规划申请资源做横向扩展,正常单台tomcat的吞吐量约在2000左右(具体还得看业务复杂度,如果是只有时间戳吞吐量可以到4000);
	2. redis性能瓶颈,虽然单台redis号称吞吐量可以达到10W左右,但实际上吞吐量超过5W后就会有一定性能问题,也是需要预估最大支撑的流量来规划和申请资源;
	3. 订单服务性能瓶颈,由于秒杀服务会瞬间将秒杀商品的库存清空,这部分流量压力会通过MQ来转接给订单服务,则也是有概率直接压垮或影响订单服务性能,这里给方案是在mq的消费者端做限流。
	
二、缓存和DB数据最终一致性
	1. 首先要保证MQ的消息不丢失(rocketMQ可以保证消息可靠性);
	2. redis扣减库存不超卖(在分布式环境下lua脚本可以实现扣减库存和库存判断的原子性来保证库存不超卖);
	3. 保证了前2点则可以实现缓存和DB数据的最终一致性(由于在MQ的消费端有限流,则DB数据会有一定的延迟),
	   由于DB数据有延迟那么前端需要做一下轮训来获取用户秒杀的订单数据。

三、lua脚本业务
	
	local key = KEYS[1];  ---key 商品的sku_id 
	local subNum = tonumber(ARGV[1]) ;  ---需要扣减库存
	local surplusStock = tonumber(redis.call('get',key));   ---使用get命令获取key的value值  商品剩余库存
	--- 库存判断
	if (surplusStock - subNum >-1) 
		redis.call('incrby', KEYS[1], subNum)  ---实际扣减库存
		return 1    ---  扣减后的库存大于-1 则表示库存扣减成功 返回成功  即 1
	else
	    return 0  --- 扣减库存失败返回失败  即 0
	end

-- 让lua脚本执行的指令尽量少,以上执行的redis指令只有2个

四、其他额外问题
	诸如 分布式事务(下单和减库存)、订单号生成(分布式下唯一id)、CDN内容分发、订单数据量大需要做分表分库策略、复杂数据检索等等问题,
	都不是由于秒杀服务导致(即便没有秒杀服务系统也会遇到的问题),则不在秒杀架构设计中涉及。

五、Nginx+lua秒杀设计

在B站上了解到大厂的秒杀架构基本上都是直接使用nginx+lua实现,追求最高的吞吐量,按照这个思路我将我之前画好的图改造一下(即是去掉链路中的gateway和秒杀微服务,让nginx集成lua),具体的架构图如下:
Nginx+lua实现秒杀_第2张图片

改造后的优点:
	1. 更加轻量,nginx能够非常好集成lua,不需要额外加上秒杀微服务;
	2. 链路更短,处理秒杀业务lua离客户请求更近了,优化了一些网络io消耗,性能也更优;
落地问题:
	1. 鉴权问题,去掉gateway如何鉴权? lua也是可以集成JWT做鉴权;
	2. 限流问题,去掉Sentinel如何限流? lua+redis可以更高效实现分布式漏桶算法限流;
	3. 消息投递问题,lua中有模块支持各种MQ的消息投递。

基于以上分析 nginx+lua的方式实现的秒杀是吞吐量最高的。

注意:这种本人没有实践过。

你可能感兴趣的:(脚本,nginx,lua)