您当前的位置:首页 > 电脑百科 > 程序开发 > 架构

硬核讲解:秒杀设计

时间:2021-03-24 17:00:16  来源:  作者:

原文出自:公众号 sowhat1412

原文链接:
https://mp.weixin.qq.com/s/rbXrhzIJG2NtYt_61OmzTA

1 秒杀场景

秒杀场景

  1. 登陆12306进行火车票抢座
  2. 1599元购入飞天茅台
  3. 周董演唱会的门票
  4. 双十一秒杀活动

秒杀场景关注点

  1. 严格防止超卖:库存1000件卖了1020件,要杀个码农祭天了!防止超卖是秒杀系统设计最核心的部分。
  2. 防止黑产:防止不怀好意的羊毛党薅羊毛。
  3. 保证用户体验:高并发下,给用户提供友善的购物体验,尽可能支持比较高的QPS等等。

接下来就让我们按照关注点,不断细化秒杀场景。

2 第1版-裸奔

硬核讲解:秒杀设计

裸奔秒杀

不加思考,上来直接按照 SpringBoot + MyBatis 模式进行秒杀系统的设计,流程如下:

  1. Controller层获得用户秒杀请求后调用Service层。
  2. Service层获得请求后要要检查已售数据跟库存总量是否一致,一致说明商品卖没了,不一致说明还有库存,那就调用DAO层对已售数量进行加1。
  3. DAO层获得请求后直接通过MyBatis操作数据库实现已售数量加1跟订单创建。

如果你用Postman去测试会发现是OK的,但如果你用专业的并发测试工具JMeter模式多用户并发请求会发现订单创建数量 > 库存量 - 已售量。原因解释下,比如用户A、B并发进行秒杀请求,此时库存=100,已售=64。

  1. A用户进行描述请求,此时调用到了Service层,发现已售不等于库存,此时拿到库存数是64,A将库存更新为63,然后创建订单。
  2. B用户进行描述请求,此时调用到了Service层,发现已售不等于库存,此时拿到库存数是64,B将库存更新为63,然后创建订单。
  3. 此时库存减少了1个但是订单创建多个,卖超了!
    无锁并发请求,卖超了

3 第2版-悲观锁

硬核讲解:秒杀设计

syn悲观锁


遇见 并发问题 很容易想到以前学过并发编程嘛,既然Controller默认是单例模式,那我用 synchronized 将Controller层调用Service层的代码进行加锁同步即可。

这样就可以解决卖超问题了,但是须知,既然是悲观锁,如果有1000个并发请求,那只有1个拿到锁了。有999个会去竞争这个锁的。

@Transactional
@Service
@Transactional
@Slf4j
public class OrderServiceImpl implements OrderService
{
//校验库存
Stock stock = checkStock(id);
//更新库存
updateSale(stock);
//创建订单
return createOrder(stock);
}

当然了你也可以用Spring自带的事务注解来实现悲观锁的操作,因为用了@Transactional就可以实现通过事务来控制,要么全部成功,要么全部失败,用事务时有两点需注意:

  1. 尽可能将MySQL执行语句往方法体后面靠,因为MySQL事务的commit语句是在第一次执行MySQL相关语句开始,一直到方法的结束。
  2. 设置事务的超时时间,如果不设置默认是-1是无限长。并且事务中设置的耗时timeout = 最后一个MySQL语句耗时 + 以及最后一个MySQL之前的所有耗时。

需注意:悲观锁状态下会保证商品卖出去,如果没拿到锁的线程会阻塞的等待拿锁。但是他的阻塞也会给用户带来非常不良好的体验。

4 第3版-乐观锁

硬核讲解:秒杀设计

MySQL版本号


我们为每个数量的已售数据配备个版本号,在Service层调用时获得用户的已售数跟对应版本号,然后更新时将已售数跟版本号同时更新。因为 MySQL在更新时会自带乐观加速机制,如果更新成功则表示抢购成功,更新失败则表示抢购失败,此时你会发现不是手速越快就一定能抢到的哦,但起码保证了不会超卖,

update 库存表 set
已售数=已售数+1,版本号=版本号+1
where 秒杀id =#{id} and 版本号 = #{version}

需注意:乐观锁状态下,由于是随机性的秒杀失败,所以可能活动结束后还会有几个没售出去的!

5 第4版-限流

最核心的超卖问题已经解决了,接下来就是各种优化手段了。在高并发请求中如果不对接口限流会对后台服务器造成极大压力,所以一般秒杀系统为了不影响其他业务会单独部署到个某个服务器上,同时还会设置好限流。

常用的限流方法有我们在 redis 中曾经说过,主要有漏桶算法、令牌桶算法。而google开源项目Guava中RateLimiter使用的就是令牌桶控制算法。在开发高并发系统时有三把利器用来保护系统:缓存、降级、限流

  1. 缓存:缓存的目的是提升系统访问速度和增大系统处理容量。
  2. 降级:降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
  3. 限流:限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理。

5.1 漏桶算法

漏桶算法思路:把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

硬核讲解:秒杀设计

 

5.2 令牌桶算法

令牌桶算法原理:可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。

硬核讲解:秒杀设计

 


流程大致

  1. 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。
  2. 根据限流大小,设置按照一定的速率往桶里添加令牌。
  3. 设置桶最大可容纳值,当桶满时新添加的令牌就被丢弃或者拒绝。
  4. 请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除。
  5. 如果用户无法获得令牌可以选择一直阻塞等待,也可以选择设置好timeout机制。
  6. 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。

工程中一般用令牌桶算法为多,一般用Google的Guava 中 RateLimiter 即可。

//创建令牌桶实例
private RateLimiter rateLimiter = RateLimiter.create(20);
// 阻塞式获得令牌才继续往下执行
rateLimiter.acquire();
// 就等3秒看是否可以获得令牌,返回Boolean值。
rateLimiter.tryAcquire(3, TimeUnit.SECONDS)

6 第5版- 细节优化

有了乐观锁跟限流,接下来再思考写细节问题。

  1. 秒杀要有时间范围限制的,不能再任意时刻都可以接受秒杀请求,要实行限时抢购。
  2. 如果有懂IT人员通过抓包获取了秒杀接口地址,在秒杀开始时,不通过按钮,直接通过脚本秒杀咋办?要实行秒杀接口隐藏。
  3. 每个用户单位时间内访问次数要做频率限制。

6.1 限时抢购

很简单,将秒杀商品放入Redis并设置超时,比如我们以kill + 商品id作为key,以商品id作为value,设置180秒超时。

127.0.0.1:6379> set kill1 1 EX 180
OK

加入时间校验:

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 秒杀接口隐藏

硬核讲解:秒杀设计

接口隐藏

  1. 用户秒杀前先通过getMd5方法获得一个请求秒杀URL的MD5值。
  2. 请求getMd5算法,Key = 商品id + 用户id,value = 商品id + 用户id + 盐 。将KV存入redis并且设置过期时间,最终返回value作为md5值。
  3. 用户请求秒杀URL的时候需携带MD5值,然后Service层会根据商品id + 用户id从redis中获取下对应的value,看这个value跟MD5值是否一致,绝对下一步操作。

// 根据商品id 跟 用户id生成个md5。
@Override
public 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 访问频率限制

  1. 通过前面请求后根据用户id生成个redis中的key,value为访问次数,默认为0,并且设置好该KV的过期时间。
  2. 用户在验证是否通过秒杀隐藏接口验证前,先看下他的单位时间内访问次数是多少,如果超过阈值则直接拒绝,没超过再进行隐藏接口的验证。
  3. 这里只是举例为用户访问次数限制,IP访问次数限制类似。
硬核讲解:秒杀设计

访问频率限制

7 第6版-众多细节优化

  1. CDN加速:为何京东物流快,因为人在全国各地配置了多个仓库。同理,我们可以将前端的一些静态东西配置在全国各个不同的地方,用户请求时,直接请求距离自己最近的前端资源即可。
  2. 前端按钮灰色化:如果参与过秒杀活动会发现,没到秒杀时间时秒杀按钮是灰色状态的,只有时间到了才是可点击状态。并且秒杀开始咯也不是一直可以点的,可能只允许1秒内点10次那种的。
  3. Nginx负载均衡:一个Tomcat的QPS一般在200~1000左右,如果淘宝或京东性质的秒杀,就需要搞个Nginx负载均衡来支持几万级别的并发了。
  4. 信息存储Redis化:单独的MySQL是无法支撑上万的QPS的,既然Redis号称可支持10W级的QPS,我们把数据信息存到Redis中就好咯嘛!有人可能会说MySQL有乐观锁跟事务性啊,Redis不是没有事务性么,其实我们可以通过 Lua 脚本来实现并发情况下Redis的事务性操作。
  5. 消息中间件-流量削峰:秒杀成功后,如果秒杀的成功量过大,全部订单直接写入MySQL也是不太恰当的,可以把秒杀成功的用户信息写入消息中间件。比如RabbitMQ、Kafka,给用户返回抢购成功信息,然后专门代码消费中间件信息(生成订单,数据持久化),因为是异步消费,为防止用户秒杀成功后无法看到订单信息,在订单生成前给用户提示订单提交排队中,啥时候订单异步消费成功了再告知用户成功。
  6. 辅助手段:秒杀前做个预演练是必须的吧,系统上线后QPS监控、CPU监控、IO监控、缓存监控也是必须要搞的。同时一旦服务真的扛不住了熔断跟限流也要考虑进去。
  7. 短URL:有时你别人发给你个超短的URL你打开后就直接跳转为日常看到的购物页面了,这就涉及到短URL映射了,大致思路就是做个链接映射,在此基础上也可以玩出各种花样,反正挺有趣的。


Tags:秒杀设计   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
严格防止超卖:库存1000件卖了1020件,要杀个码农祭天了!防止超卖是秒杀系统设计最核心的部分。防止黑产:防止不怀好意的羊毛党薅羊毛。...【详细内容】
2021-03-24  Tags: 秒杀设计  点击:(272)  评论:(0)  加入收藏
▌简易百科推荐
为了构建高并发、高可用的系统架构,压测、容量预估必不可少,在发现系统瓶颈后,需要有针对性地扩容、优化。结合楼主的经验和知识,本文做一个简单的总结,欢迎探讨。1、QPS保障目标...【详细内容】
2021-12-27  大数据架构师    Tags:架构   点击:(3)  评论:(0)  加入收藏
前言 单片机开发中,我们往往首先接触裸机系统,然后到RTOS,那么它们的软件架构是什么?这是我们开发人员必须认真考虑的问题。在实际项目中,首先选择软件架构是非常重要的,接下来我...【详细内容】
2021-12-23  正点原子原子哥    Tags:架构   点击:(7)  评论:(0)  加入收藏
现有数据架构难以支撑现代化应用的实现。 随着云计算产业的快速崛起,带动着各行各业开始自己的基于云的业务创新和信息架构现代化,云计算的可靠性、灵活性、按需计费的高性价...【详细内容】
2021-12-22    CSDN  Tags:数据架构   点击:(10)  评论:(0)  加入收藏
▶ 企业级项目结构封装释义 如果你刚毕业,作为Java新手程序员进入一家企业,拿到代码之后,你有什么感觉呢?如果你没有听过多模块、分布式这类的概念,那么多半会傻眼。为什么一个项...【详细内容】
2021-12-20  蜗牛学苑    Tags:微服务   点击:(8)  评论:(0)  加入收藏
我是一名程序员关注我们吧,我们会多多分享技术和资源。进来的朋友,可以多了解下青锋的产品,已开源多个产品的架构版本。Thymeleaf版(开源)1、采用技术: springboot、layui、Thymel...【详细内容】
2021-12-14  青锋爱编程    Tags:后台架构   点击:(20)  评论:(0)  加入收藏
在了解连接池之前,我们需要对长、短链接建立初步认识。我们都知道,网络通信大部分都是基于TCP/IP协议,数据传输之前,双方通过“三次握手”建立连接,当数据传输完成之后,又通过“四次挥手”释放连接,以下是“三次握手”与“四...【详细内容】
2021-12-14  架构即人生    Tags:连接池   点击:(16)  评论:(0)  加入收藏
随着移动互联网技术的快速发展,在新业务、新领域、新场景的驱动下,基于传统大型机的服务部署方式,不仅难以适应快速增长的业务需求,而且持续耗费高昂的成本,从而使得各大生产厂商...【详细内容】
2021-12-08  架构驿站    Tags:分布式系统   点击:(23)  评论:(0)  加入收藏
本系列为 Netty 学习笔记,本篇介绍总结Java NIO 网络编程。Netty 作为一个异步的、事件驱动的网络应用程序框架,也是基于NIO的客户、服务器端的编程框架。其对 Java NIO 底层...【详细内容】
2021-12-07  大数据架构师    Tags:Netty   点击:(16)  评论:(0)  加入收藏
前面谈过很多关于数字化转型,云原生,微服务方面的文章。虽然自己一直做大集团的SOA集成平台咨询规划和建设项目,但是当前传统企业数字化转型,国产化和自主可控,云原生,微服务是不...【详细内容】
2021-12-06  人月聊IT    Tags:架构   点击:(23)  评论:(0)  加入收藏
微服务看似是完美的解决方案。从理论上来说,微服务提高了开发速度,而且还可以单独扩展应用的某个部分。但实际上,微服务带有一定的隐形成本。我认为,没有亲自动手构建微服务的经历,就无法真正了解其复杂性。...【详细内容】
2021-11-26  GreekDataGuy  CSDN  Tags:单体应用   点击:(35)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条