本文是秒杀系统的第二篇,经过实际代码讲解,帮助你快速的了解秒杀系统的关键点,上手实际项目。mysql
本篇主要讲解接口限流措施,接口限流其实定义也很是广,接口限流自己也是系统安全防御的一种措施,暂时列举这几种容易理解的:git
此外,前文发出后不少同窗对于乐观锁在高并发时没法卖出所有商品提出了“严正抗议”,因此仍是在本篇中补充讲解下乐观锁与悲观锁。github
前文回顾和文章规划:面试
欢迎关注个人我的公众号获取最全的原创文章:后端技术漫谈(二维码见文章底部)算法
妈妈不再用担忧只看文章不会实现啦:sql
能够翻阅该系列的第一篇文章,这里再也不回顾:segmentfault
在面临高并发的请购请求时,咱们若是不对接口进行限流,可能会对后台系统形成极大的压力。尤为是对于下单的接口,过多的请求打到数据库会对系统的稳定性形成影响。设计模式
因此秒杀系统会尽可能选择独立于公司其余后端系统以外进行单独部署,以避免秒杀业务崩溃影响到其余系统。
除了独立部署秒杀业务以外,咱们可以作的就是尽可能让后台系统稳定优雅的处理大量请求。
令牌桶限流算法网上已经有了不少介绍,我摘抄一篇介绍过来:
令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并容许突发数据的发送。
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。若是令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中能够保存的最大令牌数永远不会超过桶的大小。
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以必定的速度出水,当水流入速度过大会直接溢出,能够看出漏桶算法能强行限制数据的传输速率。
令牌桶算法不能与另一种常见算法漏桶算法相混淆。这两种算法的主要区别在于:
漏桶算法可以强行限制数据的传输速率,而令牌桶算法在可以限制数据的平均传输速率外,还容许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就容许突发地传输数据直到达到用户配置的门限,所以它适合于具备突发特性的流量。
Guava是Google开源的Java工具类,里面一应俱全,也提供了限流工具类RateLimiter,该类里面实现了令牌桶算法。
咱们拿出源码,在以前讲过的乐观锁抢购接口上增长该令牌桶限流代码:
OrderController:
@Controller public class OrderController { private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class); @Autowired private StockService stockService; @Autowired private OrderService orderService; //每秒放行10个请求 RateLimiter rateLimiter = RateLimiter.create(10); @RequestMapping("/createWrongOrder/{sid}") @ResponseBody public String createWrongOrder(@PathVariable int sid) { int id = 0; try { id = orderService.createWrongOrder(sid); LOGGER.info("建立订单id: [{}]", id); } catch (Exception e) { LOGGER.error("Exception", e); } return String.valueOf(id); } /** * 乐观锁更新库存 + 令牌桶限流 * @param sid * @return */ @RequestMapping("/createOptimisticOrder/{sid}") @ResponseBody public 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个请求。
在接口中,能够看到有两种使用方法:
咱们使用JMeter设置200个线程,来同时抢购数据库里库存100个的iphone。(数据库结构和JMeter使用请查看从零开始搭建简易秒杀系统(一):防止超卖)
咱们将请求响应结果为“你被限流了,真不幸,直接返回失败”的请求单专断言出来:
咱们使用rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)
,非阻塞式的令牌桶算法,来看看购买结果:
能够看到,绿色的请求表明被令牌桶拦截掉的请求,红色的则是购买成功下单的请求。经过JMeter的请求汇总报告,能够得知,在这种状况下请求可以没被限流的比率在15%左右。
能够看到,200个请求中没有被限流的请求里,因为乐观锁的缘由,会出现一些并发更新数据库失败的问题,致使商品没有被卖出。这也是上一篇小伙伴问的最多的问题。因此我想再谈一谈乐观锁与悲观锁。
再谈锁以前,咱们再试一试令牌桶算法的阻塞式使用,咱们将代码换成rateLimiter.acquire();
,而后将数据库恢复成100个库存,订单表清零。开始请求:
此次的结果很是有意思,先放几张结果图(按顺序截图的),爱思考的同窗们能够先推测下我接下来想说啥。
总结:
令牌桶的实现原理,本文中再也不班门弄斧了,仍是以实战为主。
毕竟Guava是只提供了令牌桶的一种实现,实际项目中确定还要根据需求来使用或者本身实现,你们能够看看这篇文章:
讲完了令牌桶限流算法,咱们再回头思考超卖的问题,在海量请求的场景下,若是像第一篇文章那样的使用乐观锁,会致使大量的请求返回抢购失败,用户体验极差。
然而使用悲观锁,好比数据库事务,则可让数据库一个个处理库存数修改,修改为功后再迎接下一个请求,因此在不一样状况下,应该根据实际状况使用悲观锁和乐观锁。
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了不少这种锁机制,好比行锁,表锁等,读锁,写锁等,都是在作操做以前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可使用版本号等机制。乐观锁适用于多读的应用类型,这样能够提升吞吐量,像数据库若是提供相似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不能单纯的定义哪一个好于哪一个。
上一篇文章中,个人乐观锁创建在更新数据库版本号上,这里贴出一种不用额外字段的乐观锁SQL语句。
<update id="updateByOptimistic" parameterType="cn.monitor4all.miaoshadao.dao.Stock"> update stock <set> sale = sale + 1, </set> WHERE id = #{id,jdbcType=INTEGER} AND sale = #{sale,jdbcType=INTEGER} </update> 复制代码
咱们为了在高流量下,可以更好更快的卖出商品,咱们实现一个悲观锁(事务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–支持当前事务,若是当前没有事务,就新建一个事务
,关于Spring事务传播机制能够自行查阅资料,之后也想出一个总结文章。
咱们依然设置100个商品,清空订单表,开始用JMeter更改请求的接口/createPessimisticOrder/1
,发起200个请求:
查看结果,能够看到,HMeter给出的汇总报告中,200个请求,100个返回了抢购成功,100个返回了抢购失败。而且商品卖给了前100个进来的请求,十分的有序。
因此,悲观锁在大量请求的请求下,有着更好的卖出成功率。可是须要注意的是,若是请求量巨大,悲观锁会致使后面的请求进行了长时间的阻塞等待,用户就必须在页面等待,很像是“假死”,能够经过配合令牌桶限流,或者是给用户显著的等待提示来优化。
最后一个问题,我想证实下个人事务真的在执行for update后锁住了商品库存,不让其余线程修改库存。
咱们在idea中打断点,让代码运行到for update执行完成后。而后再mysql命令行中,执行 update stock set count = 50 where id = 1;
试图偷偷修改库存,再回车以后,你会发现命令行阻塞了,没有返回任何消息,显然他在等待行锁的释放。
接下里,你手动继续运行程序,把该事务执行完。在事务执行完成的瞬间,命令行中成功完成了修改,说明锁已经被线程释放,其余的线程可以成功修改库存了。证实事务的行锁是有效的!
本项目的代码开源在了Github,你们随意使用:
下一篇,将会继续讲解接口限流(单用户限流 + 抢购接口隐藏)。
如今有点累,休息休息。
但愿你们多多支持个人公主号:后端技术漫谈。
我是一名后端开发工程师。
主要关注后端开发,数据安全,物联网,边缘计算方向,欢迎交流。
若是文章对你有帮助,不妨收藏,转发,在看起来~