分布式消息事务最终一致性幂等框架msgtx

本文介绍了一个适用于分布式调用的框架,该框架支持必须成功处理的业务请求、最终一致性、幂等性和重复调用处理。框架特点包括自动重试、告警通知、自动清理以及支持子调用幂等性。通过注解和配置实现消息事务组,确保业务流程的原子性和一致性。此外,还提供了运维端点和异常通知功能,并计划进一步完善定时重试和清除策略。
摘要由CSDN通过智能技术生成

框架适用场景

1、某个业务请求需要涉及多个子系统的分布式调用。例如订单签收需要保存签收记录、推送结算子系统、推送财务子系统、推送报表子系统、推送短信子系统。

2、该业务请求接收到之后,必须处理直到成功为止,不能返回给调用者失败。例如接收到订单签收状态业务请求,订单签收这个请求就属于必须处理成功为止的,不能失败,因为客户签收订单属于已经发生的客观事实,对于系统来说必须处理直到成功为止。

3、该业务请求数据一致性允许存在一定的延时,符合最终一致性的要求。

4、该业务请求可能存在被重复调用,需要支持幂等性,例如订单签收请求可能是通过MQ对接的,可能存在消息重复推送或者消息重复消费的情况。

5、该业务请求涉及的所有子调用的下游子系统接口,同时也支持幂等性。

6、该业务请求子调用之间不需要保持一致性,例如推送结算子系统成功、但推送报表子系统失败,这时推送结算子系统成功是可以不回滚的,等待下次重试推送报表子系统调用,直到也成功即可。

框架项目结构

53dc64c9f49b3211d51359e2edcc0d99.png

框架源码地址

1、搜索VX公众号【Java软件编程之家】或扫码文末二维码,关注后回复”msgtx”关键字,即可获取。

2、下载源码maven构建项目成功后,直接执行example项目单元测试下的OrderMainTest#testCreateOrder单元测试方法即可,所有测试需要的表都会自动创建。

快速开始

1、下载源码后,可以直接通过maven的deploy命令推送到内部私服上。

2、推送到内部私服后,其它项目需要使用时,直接通过加入pom.xml坐标:

<dependency>
  <groupId>com.lazy.msgtx</groupId>
  <artifactId>lazy-msgtx-core</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</dependency>

3、目前支持的所有配置项:

# 表名称,默认message_log
lazy.msgtx.table-name=message_tx
# 启动时是否自动创建表,默认:false
lazy.msgtx.auto-create-table=true

# 序列化器,支持 GSON、FASTJSON,默认:FASTJSON
lazy.msgtx.serializer=FASTJSON

# 开启告警通知,默认false
lazy.msgtx.enable-warn-notify=true
# 告警通知最大线程数量,建议最大不超过10,默认是5
lazy.msgtx.warn-notify-thread-count=10

# 开启自动重试,默认
lazy.msgtx.enable-auto-retry=true
# 每间隔多久执行重试,默认:0 0/10 * * * ?
lazy.msgtx.auto-retry-cron=0 0/1 * * * ?
# 每次重试拉取多少条数据,默认50
lazy.msgtx.auto-retry-batch-size=10
# 重试分布式锁ID,默认随机数
lazy.msgtx.retry-lock-id=example
# 重试次数上限,默认5
lazy.msgtx.retry-count-limit=5

# 开启自动清理,默认false
lazy.msgtx.enable-auto-clear=false
# 保留最大成功日志天数,默认50
lazy.msgtx.max-log-day=10
# 每次重试拉取多少条数据,默认500
lazy.msgtx.auto-clean-batch-size=200
# 每间隔多久执行清理,默认cron=0 0 2 * * ?
lazy.msgtx.auto-clear-cron=0 0/10 * * * ?
# 保存根消息,暂停子流程调用 时间范围,月结时间
lazy.msgtx.save-root-suspend-sub-process-times=2022-11-29 18:55:00,2022-11-29 19:00:00

4、定义消息事务组根方法入参,必须继承MessageProvide抽象类,并且实现抽象方法messageId和bizId,其中messageId必须是该业务方法中可以识别唯一的,框架会通过这个字段作为该业务方法的幂等性校验,例如源码example例子:

package com.lazy.msgtx.example.dto;


import com.lazy.msgtx.core.provide.MessageProvide;
import com.lazy.msgtx.example.entity.OrderMain;
import lombok.Data;


import java.math.BigDecimal;


/**
 * <p>
 * 创建订单DTO
 * </p>
 *
 * @author lzy
 * @since 2022/6/3.
 */
@Data
public class OrderCreateDto extends MessageProvide {


    //订单参数
    private OrderMain orderMain;


    //本次订单需要增加的积分数据
    private BigDecimal addIntegral;


    //本次订单生成的物流单据数据
    private LogisticsOrderDto logisticsOrderDto;


    //本次订单生成的报表推送数据
    private OrderReport orderReport;


    @Override
    public String messageId() {
        return orderMain.getOrderNo();
    }




    @Override
    public String bizId() {
        return orderMain.getOrderNo();
    }
}

5、定义消息事务组根方法,入参前面继承MessageProvide抽象类对象,然后根方法以及所有子调用注解上@MessageTransaction,注意:整个注解的messageType + 前面的messageId必须在同一个项目全局唯一,示例代码如下:

//消息事务组-根
    @MessageTransaction(messageType = Cost.CREATE_ORDER)
    public void createOrder(OrderCreateDto createDto) {


        //启动类必须配置@EnableAspectJAutoProxy(exposeProxy = true)
        //获取AOP切面代理对象,对当前类方法的调用一定要使用AOP切面代理对象才行
        OrderMainService orderMainService = ((OrderMainService) AopContext.currentProxy());


        //消息事务组-分支子调用-保持订单
        orderMainService.save(createDto);


        //消息事务组-分支子调用-保存订单明细
        orderMainService.saveDetail(createDto);


        //消息事务组-分支子调用-推送物流子系统
        orderMainService.toLgst(createDto);


        //消息事务组-分支子调用-推送积分子系统
        orderMainService.toIngl(createDto);


        //消息事务组-分支子调用-推送报表子系统
        orderMainService.toReport(createDto);


    }

6、包括其它消息事务组分支子方法完整的服务类代码示例:

package com.lazy.msgtx.example.service;


import com.alibaba.fastjson.JSON;
import com.lazy.msgtx.core.MessageTransaction;
import com.lazy.msgtx.example.common.Cost;
import com.lazy.msgtx.example.dao.OrderDetailRepository;
import com.lazy.msgtx.example.dao.OrderMainRepository;
import com.lazy.msgtx.example.dto.LogisticsOrderDto;
import com.lazy.msgtx.example.dto.OrderCreateDto;
import com.lazy.msgtx.example.dto.OrderReport;
import com.lazy.msgtx.example.entity.OrderMain;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


import java.math.BigDecimal;


/**
 * <p>
 *  订单服务
 * </p>
 *
 * @author lzy
 * @since 2022/6/3.
 */
@Slf4j
@Service
public class OrderMainService {


    @Autowired
    OrderMainRepository orderMainRepository;
    @Autowired
    OrderDetailRepository orderDetailRepository;




    //消息事务组-根
    @MessageTransaction(messageType = Cost.CREATE_ORDER)
    public void createOrder(OrderCreateDto createDto) {


        //启动类必须配置@EnableAspectJAutoProxy(exposeProxy = true)
        //获取AOP切面代理对象,对当前类方法的调用一定要使用AOP切面代理对象才行
        OrderMainService orderMainService = ((OrderMainService) AopContext.currentProxy());


        //消息事务组-分支子调用-保持订单
        orderMainService.save(createDto);


        //消息事务组-分支子调用-保存订单明细
        orderMainService.saveDetail(createDto);


        //消息事务组-分支子调用-推送物流子系统
        orderMainService.toLgst(createDto);


        //消息事务组-分支子调用-推送积分子系统
        orderMainService.toIngl(createDto);


        //消息事务组-分支子调用-推送报表子系统
        orderMainService.toReport(createDto);


    }


    //消息事务组-分支子调用-保持订单
    @MessageTransaction(messageType = Cost.CREATE_ORDER_SAVE)
    public void save(OrderCreateDto createDto) {


        orderMainRepository.save(createDto.getOrderMain());


        //模拟异常,会回滚当前订单
//        int a = 1 / 0;


        log.info("保存订单成功");
    }


    //消息事务组-分支子调用-保存订单明细
    @MessageTransaction(messageType = Cost.CREATE_ORDER_SAVE_DETAIL)
    public void saveDetail(OrderCreateDto createDto) {


        //模拟计算积分金额
        createDto.setAddIntegral(BigDecimal.valueOf(10));


        //计算报表,给后面方法用
        OrderReport orderReport = new OrderReport();
        orderReport.setOrderNo(createDto.getOrderMain().getOrderNo());
        orderReport.setAmount(createDto.getOrderMain().getTotalAmount());
        orderReport.setCustomerCode(createDto.getOrderMain().getCustomerCode());
        createDto.setOrderReport(orderReport);


        orderDetailRepository.saveAll(createDto.getOrderMain().getOrderDetails());


        //模拟异常,会回滚当前订单明细,但是不会回滚上面的订单
        //可以通过将这里代码放进上面创建订单方法就可以保存要么一起成功,要么一起失败,而不是独立子调用
//        int a = 1 / 0;
        log.info("save detail success");


    }


    //消息事务组-分支子调用-推送物流子系统
    @MessageTransaction(messageType = Cost.CREATE_ORDER_TO_LGST)
    public void toLgst(OrderCreateDto createDto) {


        //模拟创建物流需要的数据
        OrderMain orderMain = createDto.getOrderMain();
        LogisticsOrderDto lgstOrder = new LogisticsOrderDto();
        lgstOrder.setAddress(orderMain.getAddress());
        lgstOrder.setMobile("13222222222");
        lgstOrder.setCustomerName(orderMain.getCustomerName());
        createDto.setLogisticsOrderDto(lgstOrder);


        //模拟异常,只会回滚该子调用,前面的子调用不会回滚
//        int a = 1 / 0;


        log.info("推送物流单据:{}", JSON.toJSONString(lgstOrder));
    }


    //消息事务组-分支子调用-推送积分子系统
    @MessageTransaction(messageType = Cost.CREATE_ORDER_TO_INGL)
    public void toIngl(OrderCreateDto createDto) {


        //模拟异常,只会回滚该子调用,前面的子调用不会回滚
//        int a = 1 / 0;
        log.info("推送积分:" + createDto.getAddIntegral());
    }


    //消息事务组-分支子调用-推送报表子系统
    @MessageTransaction(messageType = Cost.CREATE_ORDER_TO_REPORT)
    public void toReport(OrderCreateDto createDto) {


        //模拟异常,只会回滚该子调用,前面的子调用不会回滚
//        int a = 1 / 0;
        log.info("推送报表:{}", JSON.toJSONString(createDto.getOrderReport()));
    }


}

7、单元测试该消息事务组根业务方法调用,可能发生以下几种情况:

7.1、所有本地或远程的分支子调用均成功,则消息事务表process_status均为1,场景示例截图如下:

44f93701c0de69ef51f455ab675ce07e.png

其中可以看到pid=-1表示根,其它pid为对应根的主键。

7.2、如果其中某个分支子A调用失败,则对应该A子调用和根的process_status均为0,A前面成功的子调用均为1,A后面的子调用不会执行,需要下次重试直到该A子调用成功后才会继续补偿执行后面的子调用,场景示例截图如下:

47b624f60964630d062ebf8ffc643026.png

7.3、对于失败的情况,可以通过定时任务根据重试次数进行重试,直到根的process_status为1表示整个消息事务组执行成功,注意:重试的情况下,对于process_status为1的子调用是不会再执行的。另外重试子调用A时,获取的消息体message_body是对应子调用A前面的执行结果作为重试A的入参来执行的。

运维端点

1、引入pom坐标后,默认支持两个端点。

1.1、条件分页查询端点路径:/msgtx/page

入参示例:

{
    "messageLog":{
        "pid":-1,
        "messageId":"",
        "bizId":"",
        "messageType":"SALE",
        "processStatus":"1",
        "messageBody":"S10"
    },
    "page":1,
    "size":10
}

出参示例:

1.2、重试端点路径:/msgtx/retry/{id} 

可以配套项目前端按Restful方式开发运维页面,参考示例:

 异常通知

1、如果希望发生异常后,能够邮件及时通知给开发或运维,可以通过实现以下接口进行邮件发送:

public interface AbstractWarnNotifyProvide {

    /**
     * 告警通知
     *
     * @param rootMessageLog 根消息日志
     */
    void notify(MessageLog rootMessageLog);

}

2、同时打开以下开关配置:

# 开启告警通知,默认false
lazy.msgtx.enable-warn-notify=true

钩子函数

目前实现两个埋点钩子函数:

1、根消息保存并提交事务后回调(这里可以做例如手动确认ACK动作)。

2、提交整个事务组后回调。

注意事项

1、必须定义消息事务组根入口方法,其它分支子调用必须在该根方法内进行编排调用,根和分支子调用必须在方法上注解@MessageTransaction,并指定messageType,messageType必须在同一个消息事务组内是唯一的。

2、同一个消息事务组根和分支子调用入参必须是同一个对象,且所有入参只能是一个参数对象,该参数对象必须继承MessageProvide抽象类,并且实现抽象方法messageId(),提供无参构造器。

3、其中同一个消息事务组messageId + messageType必须是项目全局唯一的,框架会通过这两个字段在执行每个被注解的业务方法前进行幂等性校验,

4、幂等的条件是:(从入参中获取messageId + 从方法注解中获取messageType + process_status值为1);

5、如果所有分支子调用都是在同一个Spring的Bean类对象进行this.xxxx的方式进行编排调用,那么启动类必须配置@EnableAspectJAutoProxy(exposeProxy = true),并且在子调用时通过获取AOP切面代理对象的方式(AopContext.currentProxy()),对当前类方法的调用。

6、如果某个子调用失败,则根和该子调用process_status均为0,且该子调用消息体message_body保存的是前面方法执行的结果,下次重试是直接拿到对应失败子调用的message_body作为入参来执行的,所以这里就要注意业务方法编排的顺序如果被调整,就要考虑重新发布系统后,对于编排顺序调整前的那些失败的子调用进行重试时如何去兼容的问题。

7、使用该框架,如果序列化器选择fastjson,那么框架会默认修改fastjson跳过JPA的@Transient注解为false,表示对注解这个的属性也会包含进来,而不是跳过它。

JSON.DEFAULT_GENERATE_FEATURE = SerializerFeature.config(JSON.DEFAULT_GENERATE_FEATURE, SerializerFeature.SkipTransientField, false);

8、该消息事务框架本质上还是依赖Spring本地事务,所以使用该框架项目必须开启Spring事务。

9、该消息事务框架基于SpringBoot,必须是SpringBoot项目才能引入使用。

10、根方法和子调用方法上不能注解Spring的事务@Transaction,但是子方法对其它业务方法的调用,是可以注解Spring事务的。

11、每个子调用框架内部都是新开一个Spring事务的方式去执行,一旦某个子调用失败,会回滚该子调用本地事务,不会影响前面的子调用事务提交。

后续完善计划

1、新增定时重试的自动配置和机制。

2、新增定时清除策略的配置,例如支持配置为将3个月前执行成功的数据定时物理删除。

3、目前只支持JDBC协议的存储,后续可能会新增nosql的支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值