作者:黄晓萌(学仁)
布景
在企业的商业活动中,订单是指买卖两边的产品或服务买卖意向。买卖下单担任创建这个买卖两边的产品或服务买卖意向,有了这个意向后,买方能够付款,卖方能够发货。
在电商场景下,买卖两边没有面对面买卖,许多情况下需求经过超时处理自动封闭订单,下面是一个订单的流程:
如上图所示,一个订单流程中有许多环节要用到超时处理,包含但不限于:
-
买家超时未付款:比如超越15分钟没有支付,订单自动撤销。
-
商家超时未发货:比如商家超越1个月没发货,订单自动撤销。
-
买家超时未收货:比如商家发货后,买家没有在14天内点击确认收货,则体系默许自动收货。
JDK 自带的延时行列
JDK中供给了一种推迟行列数据结构DelayQueue,其本质是封装了PriorityQueue,能够把元素进行排序。
-
把订单插入DelayQueue中,以超时时刻作为排序条件,将订单依照超时时刻从小到大排序。
-
起一个线程不断轮询行列的头部,假如订单的超时时刻到了,就出队进行超时处理,并更新订单状态到数据库中。
-
为了防止机器重启导致内存中的DelayQueue数据丢掉,每次机器启动的时分,需求从数据库中初始化未完毕的订单,加入到DelayQueue中。
-
长处:简略,不需求凭借其他第三方组件,本钱低。
-
缺陷:
-
-
一切超时处理订单都要加入到DelayQueue中,占用内存大。
-
没法做到分布式处理,只能在集群中选一台leader专门处理,功率低。
-
不适合订单量比较大的场景。
-
RabbitMQ的延时音讯
RabbitMQ的延时音讯主要有两个解决计划:
-
RabbitMQ Delayed Message Plugin
-
音讯的TTL+死信Exchange
RabbitMQ Delayed Message Plugin是官方供给的延时音讯插件,虽然运用起来比较便利,可是不是高可用的,假如节点挂了会导致音讯丢掉。引用官网原文:
Delayed messages are stored in a Mnesia table (also see Limitations below) with a single disk replica on the current node. They will survive a node restart. While timer(s) that triggered scheduled delivery are not persisted, it will be re-initialised during plugin activation on node start. Obviously, only having one copy of a scheduled message in a cluster means that losing that node or disabling the plugin on it will lose the messages residing on that node.
音讯的TTL+死信Exchange解决计划,先要了解两个概念:
-
TTL:即音讯的存活时刻。RabbitMQ能够对行列和音讯分别设置TTL,假如对行列设置,则行列中一切的音讯都具有相同的过期时刻。超越了这个时刻,咱们认为这个音讯就死了,称之为死信。
-
死信Exchange(DLX):一个音讯在满意以下条件会进入死信交换机
-
-
一个音讯被Consumer拒收了,并且reject办法的参数里requeue是false。也就是说不会被再次放在行列里,被其他顾客运用。
-
TTL到期的音讯。
-
行列满了被丢掉的音讯。
-
一个延时音讯的流程如下图:
-
界说一个BizQueue,用来接收死信音讯,并进行事务消费。
-
界说一个死信交换机(DLXExchange),绑定BizQueue,接收延时行列的音讯,并转发给BizQueue。
-
界说一组延时行列DelayQueue_xx,分别装备不同的TTL,用来处理固定延时5s、10s、30s等延时等级,并绑定到DLXExchange。
-
界说DelayExchange,用来接收事务发过来的延时音讯,并根据延时时刻转发到不同的延时行列中。
-
长处:能够支撑海量延时音讯,支撑分布式处理。
-
缺陷:
-
-
不灵敏,只能支撑固定延时等级。
-
运用杂乱,要装备一堆延时行列。
-
RocketMQ的守时音讯
RocketMQ支撑恣意秒级的守时音讯,如下图所示
运用门槛低,只需求在发送音讯的时分设置延时时刻即可,以java代码为例:
MessageBuilder messageBuilder = null;
Long deliverTimeStamp = System.currentTimeMillis() + 10L * 60 * 1000; //推迟10分钟
Message message = messageBuilder.setTopic("topic")
//设置音讯索引键,可根据关键字准确查找某条音讯。
.setKeys("messageKey")
//设置音讯Tag,用于消费端根据指定Tag过滤音讯。
.setTag("messageTag")
//设置延时时刻
.setDeliveryTimestamp(deliverTimeStamp)
//音讯体
.setBody("messageBody".getBytes())
.build();
SendReceipt sendReceipt = producer.send(message);
System.out.println(sendReceipt.getMessageId());
RocketMQ的守时音讯是怎么完成的呢?
在RocketMQ中,运用了经典的时刻轮算法[1]。经过TimerWheel来描述时刻轮不同的时刻,经过TimerLog来记载不一起间的音讯。
TimerWheel中的每一格代表着一个时刻,一起会有一个firstPos指向这个刻度下一切守时音讯的首条TimerLog记载的地址,一个lastPos指向这个刻度下一切守时音讯最后一条TimerLog的记载的地址。并且,关于所处于同一个刻度的的音讯,其TimerLog会经过prevPos串联成一个链表。
当需求新增一条记载的时分,例如现在咱们要新增一个 “1-4”。那么就将新记载的 prevPos 指向当时的 lastPos,即 “1-3”,然后修正 lastPos 指向 “1-4”。这样就将同一个刻度上面的 TimerLog 记载全都串起来了。
- 长处
-
-
精度高,支撑恣意时刻。
-
运用门槛低,和运用普通音讯一样。
-
- 缺陷
-
-
运用约束:守时时长最大值24小时。
-
本钱高:每个订单需求新增一个守时音讯,且不会马上消费,给MQ带来很大的存储本钱。
-
同一个时刻很多音讯会导致音讯推迟:守时音讯的完成逻辑需求先经过守时存储等候触发,守时时刻抵达后才会被投递给顾客。因而,假如将很多守时音讯的守时时刻设置为同一时刻,则抵达该时刻后会有很多音讯一起需求被处理,会形成体系压力过大,导致音讯分发推迟,影响守时精度。
-
Redis的过期监听
Redis支撑过期监听,也能到达和RocketMQ守时音讯一样的才能,具体步骤如下:
- redis装备文件开启”notify-keyspace-events Ex”
- 监听key的过期回调,以java代码为例
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory factory){
RedisMessageListenerContainer container=new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
return container;
}
}
@Component
public class RedisKeyExpirationListerner extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListerner(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String keyExpira = message.toString();
System.out.println("监听到key:" + expiredKey + "已过期");
}
}
运用Redis进行订单超时处理的流程图如下
这个计划外表看起来没问题,可是在实际生产上不引荐,咱们来看下Redis过期时刻的原理
每逢咱们对一个key设置了过期时刻,Redis就会把该key带上过期时刻,存到过期字典中,在redisDb中经过expires字段维护:
typedef struct redisDb {
dict *dict; /* 维护一切key-value键值对 */
dict *expires; /* 过期字典,维护设置失效时刻的键 */
....
} redisDb;
过期字典本质上是一个链表,每个节点的数据结构结构如下:
-
key是一个指针,指向某个键目标。
-
value是一个long long类型的整数,保存了key的过期时刻。
Redis主要运用了守时删去和惰性删去战略来进行过期key的删去
-
守时删去:每隔一段时刻(默许100ms)就随机抽取一些设置了过期时刻的key,查看其是否过期,假如有过期就删去。之所以这么做,是为了经过约束删去操作的履行时长和频率来削减对cpu的影响。否则每隔100ms就要遍历一切设置过期时刻的key,会导致cpu负载太大。
-
惰性删去:不自动删去过期的key,每次从数据库拜访key时,都检测key是否过期,假如过期则删去该key。惰性删去有一个问题,假如这个key现已过期了,可是一向没有被拜访,就会一向保存在数据库中。
从以上的原理能够得知[2],Redis过期删去是不精准的,在订单超时处理的场景下,惰性删去基本上也用不到,无法确保key在过期的时分能够当即删去,更不能确保能当即告诉。假如订单量比较大,那么推迟几分钟也是有可能的。
Redis过期告诉也是不可靠的,Redis在过期告诉的时分,假如运用正好重启了,那么就有可能告诉事情就丢了,会导致订单一向无法封闭,有稳定性问题。假如一定要运用Redis过期监听计划,主张再经过守时使命做补偿机制。
守时使命分布式批处理
守时使命分布式批处理解决计划,即经过守时使命不断轮询数据库的订单,将现已超时的订单捞出来,分发给不同的机器分布式处理:
运用守时使命分布式批处理的计划具有如下优势:
-
稳定性强: 根据告诉的计划(比如MQ和Redis),比较担心在各种极端情况下导致告诉的事情丢了。运用守时使命跑批,只需求确保事务幂等即可,假如这个批次有些订单没有捞出来,或许处理订单的时分运用重启了,下一个批次仍是能够捞出来处理,稳定性非常高。
-
功率高: 根据MQ的计划,需求一个订单一个守时音讯,consumer处理守时音讯的时分也需求一个订单一个订单更新,对数据库tps很高。运用守时使命跑批计划,一次捞出一批订单,处理完了,能够批量更新订单状态,削减数据库的tps。在海量订单处理场景下,批量处理功率最高。
-
可运维: 根据数据库存储,能够很便利的对订单进行修正、暂停、撤销等操作,所见即所得。假如事务跑失利了,还能够直接经过sql修正数据库来进行批量运维。
-
本钱低: 相关于其他解决计划要凭借第三方存储组件,复用数据库的本钱大大下降。
可是运用守时使命有个天然的缺陷:没法做到精度很高。守时使命的推迟时刻,由守时使命的调度周期决议。假如把频率设置很小,就会导致数据库的qps比较高,简略形成数据库压力过大,然后影响线上的正常事务。
所以一般需求抽离出超时中心和超时库来独自做订单的超时调度,在阿里内部,简直一切的事务都运用根据守时使命分布式批处理的超时中心来做订单超时处理,SLA能够做到30秒以内:
怎么让超时中心不同的节点协同工作,拉取不同的数据?
通常的解决计划是凭借使命调度体系,开源使命调度体系大多支撑分片模型,比较适合做分库分表的轮询,比如一个分片代表一张分表。可是假如分表特别多,分片模型装备起来仍是比较费事的。另外假如只要一张大表,或许超时中心运用其他的存储,这两个模型就不太适合。
阿里巴巴分布式使命调度体系SchedulerX[3],不光兼容干流开源使命调度体系和Spring @Scheduled注解,还自研了轻量级MapReduce模型[4],针对恣意异构数据源,简略几行代码就能够完成海量数据秒级别跑批。
- 经过完成map函数,经过代码自行结构分片,SchedulerX会将分片平均分给超时中心的不同节点分布式履行。
- 经过完成reduce函数,能够做聚合,能够判别这次跑批有哪些分片跑失利了,然后告诉下游处理。
运用SchedulerX守时跑批解决计划,还具有如下优势:
-
免运维、本钱低: 不需求自建使命调度体系,由云上托管。
-
可观测: 供给使命履行的历史记载、查看堆栈、日志服务、链路追踪等才能。
-
高可用: 支撑同城双活容灾,支撑多种渠道的监控报警。
-
混部: 能够托管阿里云的机器,也能够托管非阿里云的机器。
总结
假如关于超时精度比较高,超时时刻在24小时内,且不会有峰值压力的场景,引荐运用RocketMQ的守时音讯解决计划。
在电商事务下,许多订单超时场景都在24小时以上,关于超时精度没有那么敏感,并且有海量订单需求批处理,引荐运用根据守时使命的跑批解决计划。
参阅链接:
[1] developer.aliyun.com/article/994…
[2] redis.io/docs/manual…
[3] www.aliyun.com/aliware/sch…
[4] developer.aliyun.com/article/706…