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