关于dpad的事务问题的分析

针对一致性强弱的要求,制定了如下方案:

  1. 一致性要求较强的:

    1. 引入Seata,使用XA模式或者AP模式

    2. 使用RocketMQ的事务消息机制

    3. 有序操作+事务补偿(类Saga模式)

  2. 只保证最终一致的,且只要求同时成功:

    1. 本地消息表(无mq版本)

有序操作+事务补偿(类Saga模式)

类似Saga方案,但是Saga需要服务端:

  1. 解决一致性要求较高的场景,比如发起变更、发起终止

  2. 不用更改三方服务代码,比如流程服务

  3. 每个操作必须是原子的,如果失败,必须立即抛出异常,或返回失败标记

  4. 如果发生失败,会从当前事件节点倒序执行事务补偿

下图为Saga的处理方式,内部通过状态机引擎来实现,每个分支事务结束都会进入新的状态、以及产生新的额外的信息:

可能会产生的问题:

  1. 事务补偿时出现失败的情况,要怎么处理

    1. 为了防止事务补偿失败,所以需要将主事务、分支事务、执行顺序入库到表中,并记录处理状态

    2. 当发生补偿失败时,先返回页面状态给前端,并发送钉钉通知

    3. 再通过定时任务扫描,对事务重新补偿,再次补偿失败,发送钉钉通知

    4. 到达最大重试次数时,发送钉钉通知,手动处理

    5. 超过指定次数不再重试

  2. 事务补偿失败的情况下,数据是不一致的,这个时候怎么对其他操作做限制约束?

    1. 涉及接口做好状态幂等约束,校验关联状态正确才可进行下一步操作

    2. 如果状态包含拷贝的情况下,就很难保证幂等校验的有效性了。例如:

      1. 发起终止时,同步终止中状态到电站服务,如果后续操作失败,需要回滚状态,回滚出错,这时电站服务的终止状态仍是终止中。

      2. 假设电站服务侧有撤销终止操作,且在电站服务,如果使用该状态作为幂等校验就会有问题,实际这笔业务不可进行撤销终止操作。

      3. 解决办法:保留一个状态值,以这个状态值为准,或者两个状态值都进行校验。

  3. 事务补偿成功,但是事件表状态更新失败的情况怎么处理,怎么避免重复补偿?

    1. 如果可以将事件状态更改放入到分支事务的服务中,可以避免事务补偿重复的问题

    2. 事件状态的更改属于数据库操作,发生问题的概率较小

    3. 补偿操作需要幂等

  4. 串行执行效率过慢?

    1. 可以设置可并行执行的分支事务,与串行执行事务做区分

    2. 提交事务时,可并行执行分支事务,可以同时执行,顺序执行事务只能串行执行

本地消息表(无mq版本)

  1. 只保持最终一致即可,比如解决审批成功后的数据处理事务问题

  2. 定时任务会定时处理插入的事件,并记录事件处理结果

  3. 如果事件处理失败,发送钉钉通知,超过指定次数,不再进行重发处理

  4. 消息队列可选,如果使用消息队列,吞吐量更高

如果不拆分新的服务并且吞吐量较小,消息队列则不是必须的。

会产生的问题:

  1. 其他业务操作可能会需要依赖这个事务提交成功的状态,在不一致窗口时间内,如果保证其他业务的正常?

    1. 做好数据幂等校验

    2. 增加中间状态,处理中,如果全部处理结束,才是处理成功状态

    3. 不增加中间状态,以更改处理成功作为最后一个事务操作,所有事务成功结束才更改状态为处理成功

  2. 事件表插入如果不成功,这个事件怎么处理?

    1. 先全部插入事件表,让这个操作处于一个数据库事务,如果插入失败,回滚事务,直接返回页面失败状态

  3. 事件是否需要进行回滚?

    1. 如果事件需要回滚,考虑该种事件方式不合适于你的业务场景。

本地消息表的设计

全局事务表 trans_main

字段类型not null描述

id

bigint

true

主键,主事务的id

name

varchar(20)

true

事务名称

main_status

varchar(20)

true

主事务状态

start_time

datetime

true

事务开始时间

end_time

datetime

true

事务结束时间

分支事务表 trans_branch

字段类型not null描述

id

bigint

true

主键,分支事务id

main_id

bigint

true

主事务id

branch_name

varchar(20)

true

分支事务名称

branch_status

varchar(20)

true

分支事务状态

exec_order

int

true

分支事务在主事务流程中的位置,从小到大执行

branch_type

varchar(20)

true

分支事务类型(本地事务、接口)

last_start_time

datetime

false

分支事务上次执行时间

end_time

datetime

false

分支事务执行结束时间

retry_count

int

true

分支事务重试次数

exec_commit

text

false

事务提交执行内容

定时任务设计

TODO

通用方法设计

TODO

具体接口改造分析

投建决策变更保存时同步变更状态

变更保存的操作目前可分为两类:

  1. 数据库事务:motion、contract、change表的写入

  2. 同步变更状态

事务处理方法:

  1. 替换声明式事务为编程式事务

  2. 将接口放置操作的最后一步

  3. 已将接口操作放在最后

投建决策发起变更时同步变更状态

  1. 数据库事务:contract、change表的写入

  2. 同步变更状态

  3. 发起审批流

目前有事务问题,且代码执行顺序为 1 3 2。选择的事务处理方法:有序操作+事务补偿。

分支事务提交出错补偿

数据库事务:contract、change表的写入

commit

rollback

同步变更状态

更新变更状态为当前业务的变更状态

更新变更状态为当前业务的上一个变更状态

发起审批流

投建决策新增审批通过时同步电站信息

采用最终一致方案,将审批通过后的操作,细分为如下主事务和分支事务,主事务不受分支事务失败的影响:

  1. 主事务:更改决策、变更状态

  2. 分支事务1:生成会议纪要和待办任务 (不重试,告警)

  3. 分支事务2:生成投资主体待办任务 (不重试,告警)

  4. 分支事务3:生成多个土地业务,一个土地业务是一个事务 (重试,告警)

  5. 分支事务4:生成多个工程业务,一个工程业务是一个事务 (重试,告警)

  6. 分支事务5:为多个工程业务同步电站信息,一个业务作为一个事务

事务5一定要在事务4之后执行。

后续执行

经过套路,项目只需要保证同时成功的场景即可,不考虑第一种情况,也就是说没有回滚,只保证审批通过后,每个步骤的成功,设计表如下:

create table rhdk_sso.trans_branch
(
    id                            bigint                               not null comment '自增id'
        primary key,
    name                          varchar(50)                          not null comment '分支事务名称',
    main_id                       bigint                               not null comment '所属的主事务',
    branch_status                 varchar(20)                          not null comment '分支事务状态',
    exec_order                    int(4)                               null comment '分支事务执行顺序',
    continue_when_error           tinyint(1) default 1                 not null comment '当当前分支事务出错时,是否继续执行后续分支事务',
    commit_exec_serialize_type    varchar(20)                          null comment '分支事务执行内容序列化类型',
    commit_exec_serialize_content text                                 null comment '分支事务执行内容的序列化',
    last_start_time               datetime                             null comment '最近一次事务开始时间',
    last_end_time                 datetime                             null comment '最近一次事务结束时间',
    create_by                     bigint                               not null comment '创建人',
    update_by                     bigint                               null comment '修改人',
    create_date                   datetime   default CURRENT_TIMESTAMP null comment '创建日期',
    update_date                   datetime   default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改日期',
    del_flag                      tinyint(1) default 0                 null comment '逻辑删除(0:正常 1:删除)',
    org_id                        bigint                               null comment '所属公司ID'
)
    comment '分支事务';

create table rhdk_sso.trans_main
(
    id          bigint                               not null comment '自增id'
        primary key,
    name        varchar(50)                          not null comment '主事务名称',
    main_status varchar(20)                          not null comment '主事务状态',
    service_id  varchar(50)                          not null comment '服务id',
    start_time  datetime                             null comment '事务开始时间',
    end_time    datetime                             null comment '事务结束时间',
    create_by   bigint                               not null comment '创建人',
    update_by   bigint                               null comment '修改人',
    create_date datetime   default CURRENT_TIMESTAMP null comment '创建日期',
    update_date datetime   default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改日期',
    del_flag    tinyint(1) default 0                 null comment '逻辑删除(0:正常 1:删除)',
    org_id      bigint                               null comment '所属公司ID'
)
    comment '主事务';

将成功后的每个步骤都预先记录到这两张表中,然后按照次序依次开始执行。通用代码已经开发完毕,也已自测,但是在改造时发现投建决策这边改造内容过多,时间不够,原有代码过于混乱,不得不进行代码重构,故终止此需求。

目前每个接口操作在发生错误时,都会发送钉钉报错,以此加人工处理来保证数据一致性。

相关代码保留

请参考:

最后更新于