本文是秒杀系统的第二篇,主要讲解接口限流措施。接口限流其实定义也非常广,接口限流本身也是系统安全防护的一种措施,在面临高并发的请购请求时,我们如果不对接口进行限流,可能会对后台系统造成极大的压力,尤其是对于下单的接口,过多的请求打到数据库会对系统的稳定性造成影响。所以对于秒杀系统:

  • 会尽量选择独立于公司其他后端系统之外进行单独部署,以免秒杀业务崩溃影响到其他系统
  • 除了独立部署秒杀业务之外,我们能够做的就是尽量让后台系统稳定优雅的处理大量请求。

列举几种容易理解的接口限流的措施:

  • 令牌桶限流
  • 单用户访问频率限流
  • 抢购接口隐藏

因为篇幅会比较长,所以会分两篇文章来进行讲解,本篇主要讲令牌桶限流,后面两种我们一并在后面的一篇文章介绍。

令牌桶限流

令牌桶限流算法

令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。

如图,大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

令牌桶算法与漏桶算法

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

漏桶算法与令牌桶算法在表面看起来类似,很容易将两者混淆。但事实上,这两者具有截然不同的特性,且为不同的目的而使用。漏桶算法与令牌桶算法的区别在于:

  • 漏桶算法能够强行限制数据的传输速率,令牌桶算法能够在限制数据的平均传输速率的同时还允许某种程度的突发传输;
  • 在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。

并不能说明令牌桶一定比漏洞好,它们使用场景不一样:

  • 令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制。
  • 漏桶算法用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。

限流工具类RateLimiter

Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流,非常易于使用。我们利用它在之前讲过的乐观锁抢购接口上增加该令牌桶限流代码:

@Controller
public class OrderController {private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);@Autowiredprivate StockService stockService;@Autowiredprivate OrderService orderService;//每秒放行10个请求RateLimiter rateLimiter = RateLimiter.create(10);/*** 乐观锁更新库存 + 令牌桶限流* @param sid* @return*/@RequestMapping("/createOptimisticOrder/{sid}")@ResponseBodypublic String createOptimisticOrder(@PathVariable int sid) {// 阻塞式获取令牌//LOGGER.info("等待时间" + rateLimiter.acquire());// 非阻塞式获取令牌if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {LOGGER.warn("你被限流了,真不幸,直接返回失败");return "购买失败,库存不足";}int id;try {id = orderService.createOptimisticOrder(sid);LOGGER.info("购买成功,剩余库存为: [{}]", id);} catch (Exception e) {LOGGER.error("购买失败:[{}]", e.getMessage());return "购买失败,库存不足";}return String.format("购买成功,剩余库存为:%d", id);}
}

在代码中做了相关的解释。使用RateLimiter rateLimiter = RateLimiter.create(10)初始化令牌桶类,每秒放行10个请求。使用rateLimiter 获取令牌的方式主要有两种:

  • 阻塞式获取令牌:使用rateLimiter.acquire()实现。请求进来后,若令牌桶里没有足够的令牌,就在这里阻塞住,等待令牌的发放;
  • 非阻塞式获取令牌:使用rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)实现。请求进来后,若令牌桶里没有足够的令牌,会尝试等待设置好的时间(这里写了1000ms),其会自动判断在1000ms后,这个请求能不能拿到令牌,如果不能拿到,直接返回抢购失败。如果timeout设置为0,则等于阻塞时获取令牌。

我们使用JMeter设置200个线程,来同时抢购数据库里库存100个的iphone。相关结构和数据可以看教你从0到1搭建秒杀系统-防超卖。

令牌桶算法实践

首先使用非阻塞式获取令牌的方式进行操作,请求完以后来看看购买结果:


有数据可以看到,最终只有11个被卖出去了。在这种情况下请求能够没被限流的比率在11%左右。

可以看到,200个请求中没有被限流的请求里,由于乐观锁的原因,会出现一些并发更新数据库失败的问题,导致商品没有被卖出。我们再试一试令牌桶算法的阻塞式使用,我们将代码换成rateLimiter.acquire();,然后将数据库恢复成100个库存,订单表清零。开始请求:

可以看到,100个全部卖出。这里首先看一下操作结果和打印日志:




对照着请求的打印日志,有几个问题需要说明一下:

  • 首先,所有请求进入了处理流程,但是被限流成每秒处理10个请求。
  • 在刚开始的请求里,令牌桶里一下子被取了10个令牌,所以出现了第二张图中的,乐观锁并发更新失败,然而在后面的请求中,由于令牌一旦生成就被拿走,所以请求进来的很均匀,没有再出现并发更新库存的情况。这也符合“令牌桶”的定义,可以应对突发请求(只是由于乐观锁,所以购买冲突了)。而非“漏桶”的永远恒定的请求限制。
  • 200个请求,在乐观锁的情况下,卖出了全部100个商品,如果没有该限流,而请求又过于集中的话,会卖不出去几个。

令牌桶限流算法说完了,我们再回头思考超卖的问题,在海量请求的场景下使用乐观锁,会导致大量的请求返回抢购失败,用户体验极差。然而使用悲观锁,比如数据库事务,则可以让数据库一个个处理库存数修改,修改成功后再迎接下一个请求,所以在不同情况下,应该根据实际情况使用悲观锁和乐观锁。两种锁各有优缺点,不能单纯的定义哪个好于哪个:

  • 乐观锁比较适合数据修改比较少,读取比较频繁的场景,即使出现了少量的冲突,这样也省去了大量的锁的开销,故而提高了系统的吞吐量;
  • 但是如果经常发生冲突(写数据比较多的情况下),上层应用不不断的retry,这样反而降低了性能,对于这种情况使用悲观锁就更合适。

悲观锁实践

我们为了在高流量下,能够更好更快的卖出商品,我们实现一个悲观锁(事务for update更新库存),看看悲观锁的结果如何。在Controller中,增加一个悲观锁卖商品接口:

/*** 事务for update更新库存* @param sid* @return*/
@RequestMapping("/createPessimisticOrder/{sid}")
@ResponseBody
public String createPessimisticOrder(@PathVariable int sid) {int id;try {id = orderService.createPessimisticOrder(sid);LOGGER.info("购买成功,剩余库存为: [{}]", id);} catch (Exception e) {LOGGER.error("购买失败:[{}]", e.getMessage());return "购买失败,库存不足";}return String.format("购买成功,剩余库存为:%d", id);
}

在Service中,给该卖商品流程加上事务:

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
@Override
public int createPessimisticOrder(int sid){//校验库存(悲观锁for update)Stock stock = checkStockForUpdate(sid);//更新库存saleStock(stock);//创建订单int id = createOrder(stock);return stock.getCount() - (stock.getSale());
}/*** 检查库存 ForUpdate* @param sid* @return*/
private Stock checkStockForUpdate(int sid) {Stock stock = stockService.getStockByIdForUpdate(sid);if (stock.getSale().equals(stock.getCount())) {throw new RuntimeException("库存不足");}return stock;
}/*** 更新库存* @param stock*/
private void saleStock(Stock stock) {stock.setSale(stock.getSale() + 1);stockService.updateStockById(stock);
}/*** 创建订单* @param stock* @return*/
private int createOrder(Stock stock) {StockOrder order = new StockOrder();order.setSid(stock.getId());order.setName(stock.getName());int id = orderMapper.insertSelective(order);return id;
}

这里使用Spring的事务,@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED),如果遇到回滚,则返回Exception,并且事务传播使用PROPAGATION_REQUIRED–支持当前事务,如果当前没有事务,就新建一个事务。
我们依然设置100个商品,清空订单表,开始用JMeter更改请求的接口/createPessimisticOrder/1,发起200个请求:


可以看到,200个请求,100个返回了抢购成功,100个返回了抢购失败。并且商品卖给了前100个进来的请求,十分的有序。所以,悲观锁在大量请求的请求下,有着更好的卖出成功率。但是需要注意的是,如果请求量巨大,悲观锁会导致后面的请求进行了长时间的阻塞等待,用户就必须在页面等待,很像是“假死”,可以通过配合令牌桶限流,或者是给用户显著的等待提示来优化。

猜你感兴趣
教你从0到1搭建秒杀系统-防超卖
教你从0到1搭建秒杀系统-限流
教你从0到1搭建秒杀系统-抢购接口隐藏与单用户限制频率
教你从0到1搭建秒杀系统-缓存与数据库双写一致
教你从0到1搭建秒杀系统-Canal快速入门(番外篇)
教你从0到1搭建秒杀系统-订单异步处理

更多文章请点击:更多…

教你从0到1搭建秒杀系统-限流相关推荐

  1. 教你从0到1搭建秒杀系统-订单异步处理

    前面几篇我们从限流角度,缓存角度来优化了用户下单的速度,减少了服务器和数据库的压力.这些处理对于一个秒杀系统都是非常重要的,并且效果立竿见影,那还有什么操作也能有立竿见影的效果呢?答案是下单的异步处理 ...

  2. 教你从0到1搭建秒杀系统-Canal快速入门(番外篇)

    Canal用途很广,并且上手非常简单,小伙伴们在平时完成公司的需求时,很有可能会用到.本篇介绍一下数据库中间件Canal的使用. 很多时候为了缩短调用延时,我们会对部分接口数据加入了缓存.一旦这些数据 ...

  3. 教你从0到1搭建秒杀系统-缓存与数据库双写一致

    本文是秒杀系统的第四篇,我们来讨论秒杀系统中缓存热点数据的问题,进一步延伸到数据库和缓存的双写一致性问题. 在秒杀实际的业务中,一定有很多需要做缓存的场景,比如售卖的商品,包括名称,详情等.访问量很大 ...

  4. 教你从0到1搭建秒杀系统-抢购接口隐藏与单用户限制频率

    在前两篇文章的介绍下,我们完成了防止超卖商品和抢购接口的限流,已经能够防止大流量把我们的服务器直接搞炸,这篇文章中,我们要开始关心一些细节问题.对于稍微懂点电脑的,点击F12打开浏览器的控制台,就能在 ...

  5. 教你从0到1搭建秒杀系统-防超卖

    各位读者好,最近笔者学了很多东西,其实都想跟大家进行分享,奈何需要将所学习的知识整理出来需要耗费大量的时间,包括总结,或各种图形以及写代码示例,所以可能更新的速度会比较慢.但大家放心,只要有时间我就会 ...

  6. 分布式系统限流策略/秒杀系统限流设计

    目录 概述 限流算法 令牌桶算法 漏桶算法 应用级限流 限制总并发数/连接/请求数 限制接口的总并发/请求数 限流接口每秒的请求数 平滑限流接口的请求数 平滑突发限流(SmoothBursty) 平滑 ...

  7. 如何从0到1搭建物联网系统?

    如何从0到1搭建物联网系统? 2019年是一个好的开端,在互联网行业混迹3年,充分理解互联网行业关于用户思维.平台思维的诠释后,以及对于敏捷研发.协同工作等新思想新管理模式实践后,我准备回身物联网产业 ...

  8. 你真的懂Spring Cloud+Nginx秒杀实战,Nginx高性能秒杀和限流吗?

    Nginx高性能秒杀和限流 从性能上来说,内部网关Zuul限流理论上比外部网关Nginx限流的性能会差一些.和Zuul一样,外部网关Nginx也可以通过Lua脚本的形式执行缓存在Redis内部的令牌桶 ...

  9. 手把手教你从0到1搭建vue3+ts+vite+element-plus简易后台管理系统

    准备工作 首先请确保你的node.js版本>=12.0.0 因为vite的兼容性,Vite 需要 Node.js 版本 >= 12.0.0. 如果你不知道你的node.js的版本是多少,请 ...

最新文章

  1. 观察多个线程同时运行|| 查看进程线程的方法——未完待续
  2. redis的hash操作在集中式session中的应用
  3. 串口示波器---伏特加 彻底的爱了
  4. js 父子标签同时设置onclick,字标签触发父标签onclick解决办法
  5. AI风向标:发改委重大工程项目公布,首个无人车路测试点落户亦庄
  6. python3入门代码-Python3 入门教程 简单但比较不错
  7. 手机通话记录统计分析
  8. 会员制营销系统_想提升门店经营水平?会员制营销法可以帮到你
  9. 安装vc6出现couldn't find acme setup的解决办法
  10. 无线承载根据承载的内容不同分为SRB和DRB EPS承载根据用户业务需求和Qos的不同可以分为GBR/ Non-GBR 承载...
  11. Apache commons-exec的使用
  12. Ubuntu18.04 鼠标键盘失灵
  13. 毕业论文写作中致谢词的常见写法及优秀范文
  14. 中国象棋棋盘java_如何用java swing 实现中国象棋的棋盘。棋盘就好。。。
  15. _findnext()报错写入位置 0x000000000BE2A940 时发生访问冲突。
  16. 九河 打结_舌头打结? 与视觉效果更快地沟通
  17. mybatis-plus模板
  18. python-爬虫:获取163邮箱的收件箱信息列表
  19. 计算机系统分析员论文12篇
  20. 利用GDB进行多线程调试

热门文章

  1. 基于移位寄存器的序列密码
  2. Android用shareUserID实现多个Activity显示在同一界面
  3. update yum 到指定版本_yum语法及常用命令汇总
  4. GetModuleFileName 获取当前进程主模块完整路径
  5. 一款功能强大,可扩展端到端加密反向Shell的工具
  6. Windbg无源码调试驱动
  7. 4、Java Swing JLable:标签组件
  8. 【PAT乙级】1040 有几个PAT (25 分)
  9. Django值应用和分布式路由
  10. 【堆】堆的基本操作总结