1 秒杀场景
秒杀场景
登陆12306进行火车票抢座 1599元购入飞天茅台周董演唱会的门票双十一秒杀活动
秒杀场景关注点
严格防止超卖:库存1000件卖了1020件,要杀个码农祭天了!防止超卖是秒杀系统设计最核心的部分。防止黑产:防止不怀好意的羊毛党薅羊毛。保证用户体验:高并发下,给用户提供友善的购物体验,尽可能支持比较高的QPS等等。
接下来就让我们按照关注点,不断细化秒杀场景。
2 第1版-裸奔5.2 令牌桶算法
令牌桶算法原理:可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。
流程大致:
所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。根据限流大小,设置按照一定的速率往桶里添加令牌。设置桶最大可容纳值,当桶满时新添加的令牌就被丢弃或者拒绝。请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除。如果用户无法获得令牌可以选择一直阻塞等待,也可以选择设置好timeout机制。令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。
工程中一般用令牌桶算法为多,一般用Google的Guava 中 RateLimiter 即可。
//创建令牌桶实例private RateLimiter rateLimiter = RateLimiter.create(20);// 阻塞式获得令牌才继续往下执行rateLimiter.acquire();// 就等3秒看是否可以获得令牌,返回Boolean值。rateLimiter.tryAcquire(3, TimeUnit.SECONDS) 6 第5版- 细节优化
有了乐观锁跟限流,接下来再思考写细节问题。
秒杀要有时间范围限制的,不能再任意时刻都可以接受秒杀请求,要实行限时抢购。如果有懂IT人员通过抓包获取了秒杀接口地址,在秒杀开始时,不通过按钮,直接通过脚本秒杀咋办?要实行秒杀接口隐藏。每个用户单位时间内访问次数要做频率限制。6.1 限时抢购
很简单,将秒杀商品放入Redis并设置超时,比如我们以kill 商品id作为key,以商品id作为value,设置180秒超时。
127.0.0.1:6379> set kill1 1 EX 180OK
加入时间校验:
public Integer createOrder(Integer id) { //redis校验抢购时间 if(!stringRedisTemplate.hasKey(“kill” id)){ throw new RuntimeException(“秒杀超时,活动已经结束啦!!!”); } //校验库存 Stock stock = checkStock(id); //扣库存 updateSale(stock); //下订单 return createOrder(stock);}6.2 秒杀接口隐藏
接口隐藏
用户秒杀前先通过getMd5方法获得一个请求秒杀URL的MD5值。请求getMd5算法,Key = 商品id 用户id,value = 商品id 用户id 盐 。将KV存入redis并且设置过期时间,最终返回value作为md5值。用户请求秒杀URL的时候需携带MD5值,然后Service层会根据商品id 用户id从redis中获取下对应的value,看这个value跟MD5值是否一致,绝对下一步操作。
// 根据商品id 跟 用户id生成个md5。@Overridepublic String getMd5(Integer id, Integer userid) { //检验用户的合法性 User user = userDAO.findById(userid); if(user==null)throw new RuntimeException(“用户信息不存在!”); //检验商品的合法行 Stock stock = stockDAO.checkStock(id); if(stock==null) throw new RuntimeException(“商品信息不合法!”); String hashKey = “KEY_” userid “_” id; //生成md5,此处的 !AW# 是一个盐,可以跟找个Random随机生成。 String key = DigestUtils.md5DigestAsHex((userid id “!AW#”).getBytes()); stringRedisTemplate.opsForValue().set(hashKey, key, 3600, TimeUnit.SECONDS); return key;}
此时如果用户直接请求秒杀接口就会被限制了,但如果黑客技术升级,将请求MD5跟请求秒杀接口写到一起,还是无法防止被薅羊毛!咋办呢?再限制下用户访问频率。
6.3 访问频率限制通过前面请求后根据用户id生成个redis中的key,value为访问次数,默认为0,并且设置好该KV的过期时间。用户在验证是否通过秒杀隐藏接口验证前,先看下他的单位时间内访问次数是多少,如果超过阈值则直接拒绝,没超过再进行隐藏接口的验证。这里只是举例为用户访问次数限制,IP访问次数限制类似。秒杀源码公众号回复秒杀获取。
访问频率限制
7 第6版-众多细节优化CDN加速:为何京东物流快,因为人在全国各地配置了多个仓库。同理,我们可以将前端的一些静态东西配置在全国各个不同的地方,用户请求时,直接请求距离自己最近的前端资源即可。前端按钮灰色化:如果参与过秒杀活动会发现,没到秒杀时间时秒杀按钮是灰色状态的,只有时间到了才是可点击状态。并且秒杀开始咯也不是一直可以点的,可能只允许1秒内点10次那种的。Nginx负载均衡:一个tomcat的QPS一般在200~1000左右,如果淘宝或京东性质的秒杀,就需要搞个Nginx负载均衡来支持几万级别的并发了。信息存储Redis化:单独的MySQL是无法支撑上万的QPS的,既然Redis号称可支持10W级的QPS,我们把数据信息存到Redis中就好咯嘛!有人可能会说MySQL有乐观锁跟事务性啊,Redis不是没有事务性么,其实我们可以通过 Lua 脚本来实现并发情况下Redis的事务性操作。消息中间件-流量削峰:秒杀成功后,如果秒杀的成功量过大,全部订单直接写入MySQL也是不太恰当的,可以把秒杀成功的用户信息写入消息中间件。比如RabbitMQ、Kafka,给用户返回抢购成功信息,然后专门代码消费中间件信息(生成订单,数据持久化),因为是异步消费,为防止用户秒杀成功后无法看到订单信息,在订单生成前给用户提示订单提交排队中,啥时候订单异步消费成功了再告知用户成功。辅助手段:秒杀前做个预演练是必须的吧,系统上线后QPS监控、CPU监控、IO监控、缓存监控也是必须要搞的。同时一旦服务真的扛不住了熔断跟限流也要考虑进去。短URL:有时你别人发给你个超短的URL你打开后就直接跳转为日常看到的购物页面了,这就涉及到短URL映射了,大致思路就是做个链接映射,在此基础上也可以玩出各种花样,反正挺有趣的(有兴趣可以水一篇)。
秒杀大致流程图
工业化秒杀:真正工业化的秒杀绝对不止我前面说的那么简单哦,起码你会接触到 MQ、SpringBoot、Redis、Dubbo、ZK 、Maven、lua等知识点。