020:基于seata解决分布式事务
1 支付服务与积分存在的分布式事务问题
今日课程任务
- 支付项目如何用户防止用户重复支付
- 使用feign客户端调用积分服务接口增加积分
- 基于seata解决分布式事务难题
- 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个基本组成部分:
- 事务协调器(TC):维护全局事务和分支事务的状态,驱动全局提交或回滚,相当于是协调者。
- 事务管理器TM:定义全局事务的范围:开始全局事务,提交或回滚全局事务,相当于LCN中发起方。
- 资源管理器(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实现分布式事务
效果测试: