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看来是最新的。

 

状态之间有约束的

例如一个订单系统,一个订单的流程:下单->待发货->已发货->待收货->已收货->待评价。 这几个状态机都是有约束关系的,必须完成上一下状态才能进行流转。

那么这种情况要怎么办呢,毕竟这种流程有一定的操作顺序延迟,出现顺序不一致的情况概率比较少,那么我们可以采取等一等的方法。

比如:订单状态已经在【待发货】的状态,这【已收货】的状态先到,我们可以校验状态,如果不对重新入队列,直到状态机正确才进行处理。

思考:这种方式如果出现大量的状态不一致,可能会出现消息堆积的情况,这个时候我们需要一个蓄洪机制,对于消息处理失败的,我们先不入队列,先放入一个数据库存储起来,等高峰期过了在泄洪。