若是观看抽奖或秒杀系统的请求监控曲线,你就会发现这类系统在活动开放的时间段内会出现一个波峰,而在活动未开放时,系统的请求量、机器负载通常都是比较平稳的。为了节省机器资源,咱们不可能时时都提供最大化的资源能力来支持短期的高峰请求。因此须要使用一些技术手段,来削弱瞬时的请求高峰,让系统吞吐量在高峰请求下保持可控。mysql
最近在作一个小型的抽奖系统,用户中奖以后须要调用转帐接口进行虚拟金的转帐。转帐接口有频控的逻辑,所以不能把抽奖瞬间的大量请求都发往转帐系统,必须对请求进行削峰。削峰的方式有不少种,下面就来简单地聊一下。redis
削峰最经常使用的一种方式是请求排队。瞬时的请求量太大,那么就把这些请求先排队存起来,再依据系统所能提供的消费能力按需消费。在量小的时候,抽奖与发货这两个动做能够是同步的(以下左图),这是一种紧耦合系统,SVR B的处理能力必须跟得上SVR A的处理能力。当SVR A 与SVR B 存在处理能力差别时,能够引入消息队列,把对服务的同步调用转化成对队列的异步消费。sql
能够用来做为队列的工具备不少,典型的如Message Queue消息队列,也能够利用数据库Mysql或是Redis来实现分布式队列,跟进业务场景来自行进行选择。例如,我在实现抽奖系统的时候,使用的是Mysql,缘由是SVR A已经把用户的抽奖信息落地到的数据库,那么SVR B就能够利用Mysql做为一个队列,来达到按能力消费的需求。数据库
用户中奖的时候,SVR A 会将用户中奖信息写到数据库中。SVR B按照本身的消费能力,从数据库中把数据select出来执行转帐的逻辑。数据库表中的每一行记录,均可以看做是一个等待被消费的消息。如何保证消息按序(正序或倒序)消费?能够利用update_time 来标记消息入队时间,设定update_time字段:数据结构
update_time timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间'
必须使用一个字段来标记某行记录的消费状态。消费过的消息没必要再select出来处理。另外,在有多个消息消费者的时候(好比有多个线程来消费数据库中的这些中奖信息时),须要保证消息不会重复被消费。可使用二段式提交的方式来保证。以字段present_flag来表示消费状态,present_flag有三个取值:
0:中奖,未转帐
1:一阶段提交(即准备转帐)
2:二阶段提交(转帐完成)异步
对于SVR B ,须要进行以下的操做:
步骤一:将数据库中present_flag 为0 的记录按序捞取出来,这里能够批量拉取,好比一次拉取100条记录
步骤二:按序处理每笔中奖记录的转帐逻辑,调用转帐接口以前,将present_flag设置为1,sql中的条件是present_flag为0;
步骤三:执行转帐逻辑
步骤四:转帐成功,将present_flag设置为2,sql中条件是present_flag为1。分布式
这样即便同一行记录被多个消费者拉取出来,也能保证只有一个可以成功执行步骤三。转帐失败(消费失败)
的记录如何处理?可使用一个定时脚本将present_flag为1的update成present_flag为0,再次进行消费。工具
经过这种异步消费的方式,来保证中奖记录慢慢被消费完。这种方式在极端的状况下,好比刚刚执行完步骤三
机器就挂掉了,那么可能会出现重复消费的状况。根据业务对重复消费的容忍度来进行选择。线程
Redis的list数据结构提供了BLPOP和BRPOP,表示列表的阻塞式弹出。BLPOP的BRPOP的区别仅仅在取元素的位置不一样。使用方式为:设计
BRPOP key timeout
当给定的列表内没有任何元素可供弹出的时候,链接将被阻塞,直到等待超时或发现可弹出的元素为止,超时参数 timeout 接受一个以秒为单位的数字做为值。超时参数设为 0 表示阻塞时间能够无限期延长。相同的key能够被多个客户端同时阻塞,不一样的客户端会被放进一个队列中,按照【先阻塞先服务】的顺序为key执行BRPOP 命令。利用这个特色,能够来实现一个轻量级的消息队列服务。
例如kafka、ActiveMQ、RabbitMQ、ZeroMQ、Kafka、MetaMQ、RocketMQ等消息队列,本就是为异步化消息消费、应用解耦、流量消费而设计。业务根据需求加以选型便可。