问题描述:做一个电商平台,如何设置一个在买家下订单后的”第60秒“发短信通知卖家发货,需要考虑的是像淘宝一样的大并发量的订单。
原问题链接
最基础设计
最直观能想到的解决办法就是使用延迟队列的实现原理,其就是一个按时间排好序的队列,每次put的时候排序,然后take的时候就计算时间是否过期,如果过期则返回队列第一个元素进行消费,否则当前线程阻塞X秒后弹出第一个元素,这个就是DelayQueue 的思路。
这种实现是最基础的,但是也是问题最多的
1.DelayQueue 的最大容量是有上限的,承受不住过多的订单
2.每次来新的订单都要进行排序插入合适位置,订单量级过大时性能会很低
3.这种方案只适合单机,无法横向进行分布式扩展
升级版
针对上述的问题,我们采用Redis集群来替代DelayQueue 的设计
1.生成订单后,立即往redis群集写订单信息(信息包含下单时间)。
2.根据redis集群的结点数量,开启相应倍数(大于等于1)的线程数,n个线程扫描一个结点
3.各线程每次扫描得到的都是下单时间等于60s的订单,再对这些订单发送短信,并相应的从redis移除
这种方法解决了基础版的三个问题,在升级版里还有一些点可以进行细化优化
1.订单量爆发时,可以将每个订单直接扔进redis,将压力分给集群
2.订单流量中低时,可以利用redis的SortedSet(有序集合)来进行操作,增大redis的扫描线程的颗粒度,进而提升处理效率
3.Redis并发时的数据一致性问题,可以通过redis事务灵活解决
到这里基本就可以解决本文所说的高并发量的带时间延迟的问题了,我们再深入想一想这种设计的问题,如果时间不固定跨度广的情况下其实轮询的方式是不那么理想的会空转cpu
时间轮(TimingWheel)
Kafka中存在大量的延迟操作,比如延迟生产、延迟拉取以及延迟删除等。Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定时器(SystemTimer)。
JDK的Timer和DelayQueue和redis的SortedSet插入和删除操作的平均时间复杂度为O(nlog(n)),并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(1)。
时间轮的应用并非Kafka独有,其应用场景还有很多,在Netty、Akka、Quartz、Zookeeper
等组件中都存在时间轮的踪影。
参考下图,Kafka中的时间轮(TimingWheel)是一个存储定时任务的环形队列,每个元素可以存放一个定时任务列表(TimerTaskList)。TimerTaskList是一个双向链表,元素是(TimerTaskEntry),其中封装了真正的定时任务TimerTask。
时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度
(tickMs)
。时间轮的时间格个数是固定的,可用wheelSize
来表示,那么整个时间轮的总体时间跨度(interval)
可以通过公式 tickMs × wheelSize计算得出。
时间轮还有一个表盘指针(currentTime)
,currentTime
当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的TimerTaskList
的所有任务。
时间轮就像时钟一样,可以通过秒表指向谁就执行谁,当时间跨度大时,可以增加时间轮的级别,如图
第一层的时间轮tickMs=1ms, wheelSize=20, interval=20ms。
第二层的时间轮的tickMs为第一层时间轮的interval,即为20ms。每一层时间轮的wheelSize是固定的,都是20,那么第二层的时间轮的总体时间跨度interval为400ms。
以此类推,这个400ms也是第三层的tickMs的大小,第三层的时间轮的总体时间跨度为8000ms。
时间轮的优点
1.把任务轮询的多个线程改装为了秒针的单一轮询
2.从毫秒级或者秒级任务获取执行改装为批量的范围获取
3.扩展性极好,颗粒度可以根据业务场景自适应
4.插入和删除操作的时间复杂度都降为O(1)