1、背景
- 为什么会乱序:在三高的情况下,很多系统都是集群模式。有可以能消息A先发,消息B先被处理;对于一些没有强制性要求的没有问题,但是对于状态流转的就有大问题,所以需要解决这种消费的顺序问题。
思考:建议不要在MQ当中使用消息的投递顺序来保证消息的顺序一致性,因为消息中间件是公用的,保证一致性需要确认,我们只需要保证消息投递的准确性,确定投递完成即可,由业务来做消息的顺序处理,对于kafka如果要保证消息的一致性,必须每一个topic只有一个partition,那么就是端到端的消费,比较慢而且比较脆弱,容易堆积,如果使用多线程去消费一个topic,那么必须在多线程当中去处理消息的顺序,这需要保证多线程的顺序性,这种也不好。
2、栗子
只消费最终状态
账单系统位于各个系统的最下游,监听各个订单的消息来创建账单合修改账单的状态,对于账单内容来说,我只关注这条订单的最终状态,给用户的展示也是最终状态,就没有状态机之前的约束,但是可能存在消息的乱序。
那怎么保障呢,个人思路:
public @interface Column {
/** 数据库字段名称 */
String name();
/** 是否支持版本控制 */
boolean supportVersion() default true;
/** 用于版本控制时,数据标识 */
int versionOrder() default -1;
/** 版本控制的时间戳,消息GmtOccur值*/
boolean timestamp() default false;
}
column注解用于DAO模型的字段:
/**
* This property corresponds to db column <tt>rebate_fee_type</tt>.
*/
@Column(name = "rebate_fee_type", type = DataType.STRING, versionOrder = 16)
private String rebateFeeType;
/**
* This property corresponds to db column <tt>status</tt>.
*/
@Column(name = "status", type = DataType.STRING, versionOrder = 17)
private String status;
/**
* This property corresponds to db column <tt>column_version</tt>.
*/
@Column(name = "column_version", type = DataType.STRING, supportVersion = false)
private String columnVersion;
columnVersion数据数据库的一个字段。
步骤:
如果收到第一条消息,首先根据消息得到DO,那么column_version 的值:
{
"baseTime": 1594952377305, -----①
"versions": [{
"fields": [65, 2, 66, 5, 72, 9, 16, 18, 19, 21, 23, 26, 28, 30, 34, 36, 37, 48, 49, 53, 54], ---②
"version": 0 ---③
}]
}
说明:
① :baseTime 是 insert 操作时,对应MQ的发送的时间(每条MQ 会有一个MQ 产生时间uniformEvent.GmtOccur),数据库中对应字段 GmtTimestamp
②:对应DO模型字段上的注解@Column的versionOrder,如果字段有值都会记录在fields里面
③:第一次新增版本为零
如果收到第二条消息,根据消息得到 DO记做 MA(消息中也会有GmtOccur值)
第一步:将DO数据中的GmtTimestamp值与第一条数据的baseTime值比较,得到差值 记做A
第二步:循环versions,比较version(记做B)和A,如果A小,则说明是过期数据不处理,如果大,上个版本的数据记做MB ,则
{
"baseTime": 1594952377305,
"versions": [{
"fields": [2, 1, 33, 42, 43, 31], ---MA
"version": 47 A
}, {
"fields": [65, ①, 66, 5, 72, 9, 16, 18, 19, 21, 23, 26, 28, 30, 34, 36, 37, 48, 49, 53, 54], ---MB
"version": 0 B
}]
}
1 被删除
循环比较MA的fields中的versionOrder是不是在MB中存在,如果存在则删除B中对应的字段versionOrder,保留MA中的,表明此字段在MA中有更新,version 为 差值A
如果收到第三条消息 根据消息得到DO 记做MC (消息中也会有GmtOccur值)
第一步:同上
第二步:同上
如果此次比较得到的version值在MA的version和MB 的version之间,记做MAB
{
"baseTime": 1594952377305,
"versions": [{
"fields": [2, 1, 33, 42, 43, 31], ---MA
"version": 47
}, {
"fields": [②,79,80], ---MAB
"version": 40
}, {
"fields": [65,①, 66, 5, 72, 9, 16, 18, 19, 21, 23, 26, 28, 30, 34, 36, 37, 48, 49, 53, 54], ---MB
"version": 0
}]
}
1 在第二条消息的时候已经被删除
2 在MA中存在了,但是MAB 的版本小,则此值应该取MA中的值,MAB中被删除
循环比较MAB的fields中的versionOrder是不是在MB中存在,如果存在则删除B中对应的字段versionOrder,保留MA中的,表明此字段在MA中有更新,version 为 差值A
此种情况还需要与MA比较,因为MAB的版本比MA的小,MAB中的部分字段虽然相对MB来说是新的需要更新,但是相对MA来说是过期数据,但是MAB相较于MA中不存在的数据,在MA看来是最新的。
状态之间有约束的
例如一个订单系统,一个订单的流程:下单->待发货->已发货->待收货->已收货->待评价。 这几个状态机都是有约束关系的,必须完成上一下状态才能进行流转。
那么这种情况要怎么办呢,毕竟这种流程有一定的操作顺序延迟,出现顺序不一致的情况概率比较少,那么我们可以采取等一等的方法。
比如:订单状态已经在【待发货】的状态,这【已收货】的状态先到,我们可以校验状态,如果不对重新入队列,直到状态机正确才进行处理。
思考:这种方式如果出现大量的状态不一致,可能会出现消息堆积的情况,这个时候我们需要一个蓄洪机制,对于消息处理失败的,我们先不入队列,先放入一个数据库存储起来,等高峰期过了在泄洪。