基于Vue+SpringCloudAlibaba微服务电商项目实战-聚合支付平台-020:基于seata解决分布式事务

本文探讨了如何利用Seata解决分布式事务中的支付防重复支付问题,通过Feign调用积分接口并异步处理,同时介绍了Seata分布式事务原理和微服务项目中整合Seata的实践步骤,包括创建undo_log表和使用@GlobalTransactional注解。

1 支付服务与积分存在的分布式事务问题

今日课程任务

  1. 支付项目如何用户防止用户重复支付
  2. 使用feign客户端调用积分服务接口增加积分
  3. 基于seata解决分布式事务难题
  4. seata解决分布式事务底层实现原理

支付回调接口:除了修改支付订单状态以外,其他流程一定要异步实现(目的快速响应支付宝,避免接口超时产生的幂等性问题)。

支付服务调用积分服务,可能会遇到幂等性问题。
解决:全局id(支付id+userId组成)数据库中表主键唯一约束

基于feign的形式调用积分接口增加积分,如何积分服务宕机如何处理?
根据日志记录定时或者人工补偿,比较好的办法也可以直接采用MQ实现增加积分。

2 将积分代码拷贝到项目中

新建模块mt-shop-service-api-integral、mt-shop-service-integral

public interface IntegralService {

    /**
     * 增加积分
     * @return
     */
    @PostMapping("/addIntegral")
    BaseResponse<String> addIntegral(@RequestBody IntegralReqDto IntegralReqDto);
}
@RestController
public class IntegralServiceImpl extends BaseApiService implements IntegralService {
    @Autowired
    private IntegralMapper integralMapper;
    @Override
    public BaseResponse<String> addIntegral(IntegralReqDto integralReqDto) {
        // 1.验证参数
        Long userId = integralReqDto.getUserId();
        if(userId==null){
            return  setResultError("userId不能为空!");
        }
        String paymentId = integralReqDto.getPaymentId();
        if(StringUtils.isEmpty(paymentId)){
            return  setResultError("paymentId不能为空!");
        }
        Long integral = integralReqDto.getIntegral();
        if(integral==null){
            return  setResultError("integral不能为空!");
        }
        // 2.dto转换do
        IntegralEntity integralEntity = dtoToDo(integralReqDto, IntegralEntity.class);
        // 插入到数据库中  根据支付id实现唯一约束,无法重复插入,所以不会重复增加积分。
        int insertIntegral = integralMapper.insertIntegral(integralEntity);
        if(insertIntegral<=0){
            // 将该日志信息记录下来,后期采用定时任务实现补偿
            return  setResultError("增加积分失败");
        }
        return setResultSuccess("增加积分成功");
    }
}

积分表

DROP TABLE IF EXISTS `meite_integral`;
CREATE TABLE `meite_integral` (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `USER_ID` int(11) DEFAULT NULL COMMENT '用户ID',
  `PAYMENT_ID` varchar(1024) NOT NULL COMMENT '支付ID',
  `INTEGRAL` varchar(32) DEFAULT NULL COMMENT '积分',
  `AVAILABILITY` int(11) DEFAULT NULL COMMENT '是否可用',
  `REVISION` int(11) DEFAULT NULL COMMENT '乐观锁',
  `CREATED_BY` varchar(32) DEFAULT NULL COMMENT '创建人',
  `CREATED_TIME` datetime DEFAULT NULL COMMENT '创建时间',
  `UPDATED_BY` varchar(32) DEFAULT NULL COMMENT '更新人',
  `UPDATED_TIME` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8 COMMENT=' ';

3 单独开启一个线程调用积分接口

异步使用feign客户端调用积分服务要独立在一个类中处理

@FeignClient("mayikt-integral")
public interface IntegralServiceFeign extends IntegralService {
}
@Component
@EnableAsync
@Slf4j
public class IntegralServiceManage {
    @Autowired
    private IntegralServiceFeign integralServiceFeign;

    @Async
    public void addIntegral(IntegralReqDto integralReqDto) {
        try {
            BaseResponse<String> stringBaseResponse = integralServiceFeign.addIntegral(integralReqDto);
            Integer code = stringBaseResponse.getCode();
            if (!(PayConstant.RPC_RESULT_OK_CODE.equals(code))) {
                // 日志记录 后期定时任务补偿
                addErrorPayLog();
                return;
            }
            // 增加积分成功
        } catch (Exception e) {
            addErrorPayLog();
            // 记录异常日志 后期采用定时任务实现补偿 积分服务接口宕机或者超时
        }
    }
    private void addErrorPayLog(){}
}

AbstractPayCallbackTemplate

@Slf4j
public abstract class AbstractPayCallbackTemplate {
    @Autowired
    private IntegralServiceManage integralServiceManage;
    /**
     * 3.更改订单状态已经支付
     * 4.调用积分服务接口增加积分
     *
     * @return
     */
    private String asyncService(String outTradeNo, Long totalAmount) {
        // 支付订单号码
        PaymentTransactionEntity pte = paymentTransactionMapper.selectByPaymentId(outTradeNo);
        // 以下代码可以重构进入父类里面
        if (PayConstant.PAY_PAID_STATUS.equals(pte.getPaymentStatus())) {
            // 返回成功告诉给支付不要再继续重试
            return resultSuccess();
        }
        // 2.判断支付的金额 是否一致
        Long payAmount = pte.getPayAmount();
        if (!payAmount.equals(totalAmount)) {
            // 返回success,后台将该订单状态修改为异常订单状态,然后分析异常原因用定时任务处理
            return resultSuccess();
        }
        // 3. 在修改为已经支付
        int result = paymentTransactionMapper.updatePaymentStatus(PayConstant.PAY_PAID_STATUS + "", outTradeNo, "ali_pay");
        if (result < PayConstant.DB_FAIL) {
            // 返回失败告诉支付宝重试
            return resultFail();
        }
        // 调用积分服务接口增加积分 存在分布式事务的问题
        IntegralReqDto integralReqDto = new IntegralReqDto(pte.getUserId(), pte.getPaymentId(), 1000L); //积分公式根据业务要求定
        // 采用多线程一步形式调用积分接口增加积分 MQ增加积分更好(自带补偿功能)
        integralServiceManage.addIntegral(integralReqDto);
        return resultSuccess();
}

4 支付宝如何防止用户重复支付问题

如何防止用户重复支付?采用支付的全局id
传递相同的支付id给支付宝,支付宝端会做判重处理,该订单已经支付。

测试联调对接积分服务
在这里插入图片描述

5 简单回顾seata与lcn解决分布式事务原理

Seata简单介绍
Seata:Simple Extensible Autonomous Transaction Architecture,简易可扩展的自治式分布式事务管理框架,其前身是fescar,是一种简单分布式事务的解决方案。
Seata给用户提供了AT、TCC、SAGA和XA事务模式,AT模式是阿里云中推出的商业版本GTS全局事务服务,项目中用的是0.9版本。

https://github.com/seata/seata seata官网
https://yq.aliyun.com/zt/593075 阿里云GTS
LCN和Seata到底用哪个? LCN有事务协调者管理界面但是Seata目前没有

Seata有3个基本组成部分:

  1. 事务协调器(TC):维护全局事务和分支事务的状态,驱动全局提交或回滚,相当于是协调者。
  2. 事务管理器TM:定义全局事务的范围:开始全局事务,提交或回滚全局事务,相当于LCN中发起方。
  3. 资源管理器(RM):管理分支事务正在处理的资源,与TC进行对话以注册分支事务并报告分支事务的状态,并驱动分支事务的提交或回滚,相当于是LCN中的参与方。

如果支付服务调用积分服务接口,积分服务接口返回error,不属于分布式事务问题。

分布式事务的问题产生:支付服务调用积分服务成功之后,支付服务突然报错了,这时候支付服务回滚,但是积分服务是增加了。
在这里插入图片描述
Seata会造成数据脏读,但是可以避免死锁的现象 --支付服务调用积分服务后,积分服务事务先提交,生成undo_log日志(参与方回滚)。
LCN防止数据的脏读,但是会发生死锁的现象–支付服务调完积分服务宕机,没有及时把事务全局id给积分服务,导致积分服务一直等待(假关闭状态)。

6 构建seata服务端项目

Seata环境的安装
下载seata-server-0.9.0
修改conf目录下registry.conf
在这里插入图片描述
双击启动seata-server.bat即可
在需要解决分布式事务的数据库中,手动创建undo_log表 否则的情况下会报错

7 微服务项目整合Seata框架

Seata客户端整合
引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>

配置代理的数据源(注意两个项目都要配置)

@Configuration
public class DataSourceProxyConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }
}

需要要将file.conf、registry.conf 拷贝到项目中(0.9版本以前)
在这里插入图片描述
默认的my_test_tx_group为分组的id,default.grouplist为全局的协调者连接地址
积分服务addIntegral方法上加上注解@GlobalTransactional
积分/支付服务数据库都要建立undo_log表

DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

分别启动积分/支付服务,启动成功
在这里插入图片描述

8 异步回调整合Seata实现分布式事务

效果测试:
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值