电商中秒杀系统设计
总体设计:
常见的电商中都会存在秒杀业务,下面我们针对秒杀业务做分析设计:
痛点:针对痛点进行设计
- 瞬间并发量大,大量用户会在同一时间进行抢购,网站瞬时流量激增
- 库存有限,访问请求数据量远大于库存数量,只有少部分用户能够秒杀成功。
- 不能超卖。
我们需要对秒杀业务的各个层面进行设计:
前端访问页面:
- 秒杀商品页面静态化处理(商品商品页面不是请求多个接口组成的,比如图片,商品规格,详情。而是一个整个的静态页面),并且放到CDN(css、js和图片等静态资源文件提前缓存到CDN),减轻服务端处理静态资源的压力。这样在秒杀商品的页面,只有点击秒杀按钮,请求才能访问服务端,获取静态页面只需要访问CDN。
- 活动前禁用按钮
- 点击后禁用按钮
- 也可以增加滑动验证码,防止羊毛党
负载均衡层:多层负载均衡
F5/LVS -> Nginx集群(每台nginx大概2~3万的并发) -> 服务网关 ->秒杀服务
nginx做限流防止羊毛党,爬虫,恶意的dos攻击。
服务网关也可以设置限流熔断
秒杀服务层:
- 通过MQ进行削峰填谷,防止流量激增对下游的压力。意思就是拆分秒杀和下单,秒杀成功后再通过MQ异步下单,通过中转页面引导支付。支付也是异步,用户在订单列表找到秒杀订单完成支付。若没有支付,则下游调用秒杀服务回退redis中的缓存。
- 秒杀商品信息,库存信息预热到redis中
- 库存扣减问题:redisClient.incrby(productId, -1)<0,若小于0为true,则没库存了。因为需要记录用户秒杀次数,为保证记录次数和扣减库存的原子性,防止超卖和少卖问题,秒杀逻辑可以使用lua脚本。
lua脚本:
local userid=KEYS[1]; local prodid=KEYS[2]; local qtkey="sk:"..prodid..":qt"; local usersKey="'sk:"..prodid.":usr'; local userExists=redis.call("sismember",usersKey,userid); if tonumber(userExists)==1 then return 2 end local num= redis.call("get" ,qtkey); if tonumber(num)<=0 then " return 0; else redis.call("decr",qtkey); redis.call("sadd",usersKey,userid); end return 1;
运维支持: docker + k8s ,支持动态伸缩的功能,根据需要增加或者减少秒杀服务节点的数量
如何保证多层缓存的数据一致性呢?
从打开秒杀页开始:
- 用户层,也就是APP层,浏览器层。这一层也需要对秒杀 商品详情进行缓存
- 从CDN拿到商品详情,CDN存储整个详情页的静态资源,包含js,css,图片等。
- LVS (负载均衡)+ HA(故障切换)
- Nginx转发层集群,IP限流 + 根据URL进行业务转发。
- Nginx业务层(商品层Nginx,订单层Nginx,结算层Nginx等等),Nginx业务层有本地cache存放热点数据,通过lua脚本访问分布式缓存(redis,是从库)。如果在转发层进行本地cache,不要业务层Nginx了呢?
- 服务层,服务层也可以有JVM本地cache如ehcache,guavacache等。本地cache也没有先访问一次分布式缓存主库,防止主从延迟导致从库更细腻不及时,也可能上一次访问已经从DB同步到分布式缓存主库了,就不用再访问DB了。
- DB层,访问DB获取数据更新到分布式缓存和所有实例的JVM本地cache(jvm本地cache主要存储更新频率很低的数据)。访问DB要分布式加锁,要双重检查,防止大量请求打入DB。
从以上我们知道一共有五级缓存:用户APP缓存 CDN缓存 Nginx热点缓存 分布式缓存 jvm进程内本地缓存。不过主要是Nginx热点缓存与分布式缓存的一致性,如果使用Nginx热key了,就没必要使用jvm本地缓存存储热点key了。
首先jvm进程内本地缓存,都是存储一些零散的基本不会改变的数据(比如商品的规格属性),不涉及秒杀流程中的缓存一致性问题。对于服务进程内本地缓存的更新,可以使用定时任务,或者通过后台管理操作进行更新。
现在有一个问题,Nginx怎么知道哪些是热key呢?那就需要进行热key检测:需要有一个服务专门热key的检测和管理。可以使用HotKey框架构建一个热可以检测服务。服务层每次访问redis数据,都把这个key异步通过mq发送到热key检测服务,通过滑动时间窗口检测这个key是否是热key(比如1s内访问次数超过5000就是热key),如果检测出热key,就把这个key推送到Nginx,Nginx可以通过lua脚本到分布式缓存拿到value。当然Nginx本地缓存也需要设置缓存淘汰策略,设置本地一个比较短的缓存过期时间,本地过期后再去分布式缓存获取最新value,获取不到则删除本地缓存。有可能这个key已经不是热key了, 但是还在Nginx缓存里,不过由于设置缓存淘汰策略LFU(一段时间内,被使用的次数最少的数据,优先淘汰掉),可以自动从Nginx缓存中淘汰掉。
上面说的是动态热点数据,对于动态热点数据检测比较复杂。还有一种是静态热点数据,就是明确参加了秒杀活动的,需要卖家提前报名的,这部分热点数据,可以提前进行缓存预热。