# 关于dpad的事务问题的分析

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

1. 一致性要求较强的：
   1. 引入Seata，使用XA模式或者AP模式
   2. 使用RocketMQ的事务消息机制
   3. 有序操作+事务补偿（类Saga模式）
2. 只保证最终一致的，且只要求同时成功：
   1. 本地消息表（无mq版本）

## 有序操作+事务补偿（类Saga模式）

类似Saga方案，但是Saga需要服务端：

1. 解决一致性要求较高的场景，比如发起变更、发起终止
2. 不用更改三方服务代码，比如流程服务
3. 每个操作必须是原子的，如果失败，必须立即抛出异常，或返回失败标记
4. 如果发生失败，会从当前事件节点倒序执行事务补偿

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

![](https://2351062869-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F7b2CdwBN9liniVJpfEAc%2Fuploads%2Fgit-blob-d7be2b80377aeb0489db56ea66045e790d965d4c%2FPasted%20image%2020240416164746.png?alt=media)

可能会产生的问题：

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之后执行。

## 后续执行

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

```sql
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 '主事务';

```

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

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

## 相关代码保留

请参考：
