本文的目的是探讨一种通过事件触发来命中数据以便在未来进行处理的方法论。通常这种问题是使用定时任务完成的。所以本文旨在能够在系统中消除所有定时任务。
一、定时任务的使用
目前的系统设计中,定时任务是被做为很重要的组件存在的。下面我举两个场景,做为贯穿本文的例子。
1. 单据明细的汇总
比如某家电商超市的销售明细,需要及时根据某些条件(地点,渠道,甚至供应商、商品等)对前一日的数据进行分类汇总出总的销售额。
对于这种场景,使用定时任务几乎是一种思维定式:选一个凌晨系统比较空闲的时间,通过定时任务调度拉取全部需要汇总的数据进行分类汇总。
2. 合同状态的自动变更
比如超市和供应商签署返利合同,合同上面又生效时间字段和当前状态字段。默认状态是“待生效”,一旦当前时间达到生效时间后,合同状态变更为“已生效”。
这种场景也具有明显的按照时间一刀切的特点,所以使用定时任务几乎也是唯一的选择。
二、事件驱动的使用
使用定时任务有什么不好吗?为什么这里我要推荐事件机制呢?
下面我将用更加符合我们思维方式的基于事件触发的逻辑来重新设计前面例子中的功能。通过不同方式的对比,你应该能体会到他们直接明显的差别。
1. 单据明细的汇总
前面说过,汇总就是拿到需要处理的数据,根据特定的汇总条件,将数据分组然后压缩。
这里面有几个关键信息:①需要处理的数据,②根据条件分组压缩。
所以我们先来思考第一个角度:哪些数据是需要处理的。不用想都知道,前一天还没有汇总的数据就是需要汇总的!其实我想问的不是这个,而是如何拿到这些数据。能拿到才能处理。
很简单,使用定时任务的话,我们可以通过时间段和是否被处理过的状态过滤出这批数据。但是如何数据量很大,比如一天有几千万,甚至几十亿,想要一次性捞出来然后在内存里面分类汇总怕是有困难。
有人会说了:不怕,处理海量数据我们有经验:分片处理。使用分片处理的话,我们可以通过一个基础数据的接口获取全部正在营业的门店,比如有10万家,然后将它们按照某种规则分成几组,各组分别在不同的程序实例中处理(或者同一个程序里顺序处理)。如果是关系型数据库的话,给门店加一个索引基本就能解决。
然后还有一个问题需要考虑:门店每天的销售量并不固定,甚至有些门店是一天都没有销售量的;而给门店分片的策略是固定的,每天哪个分片是哪些门店都是固定的。这样导致的问题是不同分片内的数据量差异可能很大,造成的分片计算压力也不同。
如果不使用定时任务,我们还有以下方案可以选择,而且我觉得比定时任务要好。
a. 延时汇总
每当一个门店向系统中写入数据的时候,系统需要判断当前数据是否是当天的第一次写入。如果是第一次写入,则创建一个延时汇总事件(具体的实现可以是定时mq或者定时线程池,但是最好不要使用数据库,否则我们又需要一个定时任务去扫库);这样的话,没有业务量的门店就不会被汇总。
为了降低系统压力,我们具体的延时时间也可以分列开来,不用所有门店都聚集在一起。
但是使用这种方案,相应的一定要做补偿。比如使用Java线程池或者akka acotr,如果系统重启信息就都丢失了;如果使用mq,消息也可能消费失败。所以我们需要监控和告警机制来辅助。
b. 实时汇总
每当数据写入的时候,都判断一下该数据低汇总维度,然后写入汇总记录。如果汇总记录已经存在了,就直接更新。
这个过程主要要考虑的问题是并发安全,我们可以使用分布式锁或者数据库乐观锁,甚至mysql的on duplicate key update(参考[https://www.manxi.info/mysqlDuplicate])。其他问题,比如消峰,可以结合场景判断。
2. 合同状态的变更
这种场景使用定时任务的标志是按某个时间点所有命中某规则的记录都要被处理。所以我们可以创建一个每天凌晨的定时任务,判断一下是否有合同在今天需要变成有效(或者标记过期)。但是实际上一年365天可能只有一天这个任务能真正发挥作用,因为其他日期完全命不中记录 —— 这不是很浪费资源嘛!
针对这种场景,我们可以学习redis对过期键的清理策略之懒惰清理。我们不需要一种机制在精准(准精准)的时机变更合同状态,而是如果这个合同不需要被命中,那么它的状态在数据库中一直是之前的状态:哪怕过了生效期,我们连到数据库中查看依然是没有生效。只有当业务逻辑命中这条合同的时候,返回的数据中才需要找到这种合同进行修改。
这种方法可以有效降低系统逻辑中的轮空,但是查询记录的时候需要把前一种状态的记录也查找到。因为本来我们需要命中的合同是已经生效的,使用懒惰策略就需要把待生效的合同也一起找到,然后从已经生效的合同中剔除已经过期的合同,从待生效的合同中找到已生效的合同;这时候,就需要把已过期和已生效的合同状态都改掉。当然我们可以使用异步逻辑去处理状态变更,因为即使处理失败依然不影响下一次的查询结果。
事件驱动的好处
从上面的例子中我们可以感受到使用事件触发机制代替定时任务实际上有利有弊。正向点是事件机制是按需的,有需求就来,没需要就别来。弊端是为了维护事件机制的正常运行一般需要一套辅助逻辑:但是又有哪种逻辑不需要补偿机制呢?只不过有些是成熟的、现成的,有些是需要我们重新开发的。
从本质上讲,定时任务是一种无状态的行为,也就不符合面向对象的思想。而事件触发是模拟了人去处理一件事的流程。
比如记录的汇总,在有管信之前运维人员是怎么处理的呢?如果等到第二天把前一天挤压的单子一起处理,这就是定时任务。如果单子量很大,人工一下子处理不完,就需要多个人一起处理,这就是分布式任务。如果多个人一时半会也处理不好,需要比较长的时间(人工的压力上来了),他们可能会变通为来一单处理一单,不然一天中大部分的时间也是闲着,这样压力就降下来了。对于系统也一样,实时处理可以有效降低系统压力骤升。
实际上人工处理积压单据依然不是定时任务的原型,这样处理仅仅是因为人工能够处理得过来,是人性的表现而非模型的转换。定时任务难以在人身上找到原型,虽然人类现在也在使用闹钟,但闹钟的任务是提醒而重复任务
再模拟一下合同的处理。
假设没有管信,合同查找员估计会有几个格子,一个放待生效的,一个放生效中的,一个放已过期的。当需要找合同的时候,他就去前两个格子中寻找并把已生效的从第一个格子放到第二个,把已过期的从第二个挪到第三个。
那他可不可能提前挪这些合同呢?可能的,但完全没必要:这依然是人性 —— 他太懒了。而且他今天挪过以后,可能今天一整天都没有出现过需要使用合同的时候,那他为什么不在明天挪呢?所以没有一个合适的时间去挪,干脆用的时候挪。
结语
所以结论是什么?
- 对于流式数据,我推荐使用实时处理,需要解决的问题是并发安全
- 对于批数据,我推荐懒惰策略,需要解决的问题是幂等
写在最后
如果你喜欢这个思想,但是自己遇到了难以摒弃定时任务的场景,欢迎你留言,我们一起来讨论。