01. 生单链路业务流程分析

1,791 阅读6分钟

根据需要获取原图 www.processon.com/view/62416f…

image.png


生单链路数据(不一致)问题分析

生单链路业务流程梳理

  • 第一步,风控检查,用户参数合法检查;
  • 第二步,获取商品信息,计算订单价格;
  • 第三步,营销服务,锁定优惠券;
  • 第四步,库存服务,锁定库存;
  • 第五步,操作本地事务,完成订单创建;
  • 第六步,发送订单延迟消息,支付超时自动关单;

001_订单系统生单链路业务分析.png

比如,第五步操作本地事务,完成订单创建,本地事务提交失败,导致生单失败

经典的分布式事务问题,导致数据不一致


Seata AT模式分布式事务

整体原理分析

原理:seata客户端分支事务保证sql执行(增删改)和插入回滚日志(逆向操作)同时成功或失败;

002_Seata AT模式分布式事务原理分析.png

读写隔离以及超时机制

Seata AT模式分布式事务整体运行(读写隔离)原理,以分布式事务1为例:

  1. 第一步,开启分布式事务1全局事务;
  2. 第二步,执行分布式事务1,首先,获取本地锁,插入undo log,等待服务A的分支事务a提交;
  3. 第三步,服务A的分支事务a提交之前,需要获取全局锁
  4. 第四步,获取全局锁成功,分支事务a提交本地事务,释放本地锁;
  5. 第五步,分支事务a,执行完成;
  6. 第六步,继续后面服务B的分支事务b的执行,运行流程和前面一致;
  7. 第七步,分支事务b,执行完成,提交分布式事务1,释放全局锁

写隔离:整个全局锁只能同时由一个分布式事务持有,其它分布式事务需要等待全局锁释放

对于分布式事务1,分支事务b执行异常,分布式事务1执行回滚逻辑,全局锁未释放; 对于分支事务a来说,基于‘之前更新,进行补偿',但是,同样需要获取本地锁; 此时,假如分布式事务2,分支事务a,持有本地锁,正在等待获取全局锁; 分布式事务1,持有全局锁,分布式事务2,持有本地锁,死锁发生

Seata AT模式引入超时机制,分布式事务2等待全局锁,加入超时机制

一旦超时,回滚本地事务,同时释放本地锁,最终保证全局数据一致,规避死锁问题发生;

003_Seata AT模式分布式事务读写隔离以及超时机制.png

  • 耗时分析:高并发场景下,并发性能考虑,吞吐量降低
  • 适用场景:金融领域
  • 生产实践:结合具体业务代码,深入分析分布式场景下,大量全局锁等待的场景发生的可能性

读隔离:Seata AT模式,全局读隔离,各个分布式事务之间‘读未提交’


生单链路应用Seata AT模式分布式事务

搭建Seata AT模式分布式事务
  • 第一步,安装 seata 服务;
  • 第二步,每个数据库都要新增⼀个seata 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,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
  • 第三步,引⼊ seata 依赖;
<!-- 引入seata整合分布式事务 -->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	<exclusions>
		<exclusion>
			<groupId>io.seata</groupId>
			<artifactId>seata-spring-boot-starter</artifactId>
		</exclusion>
	</exclusions>
</dependency>

<!-- 跟安装的seata-server需要保持版本一致 -->
<dependency>
	<groupId>io.seata</groupId>
	<artifactId>seata-spring-boot-starter</artifactId>
	<version>1.3.0</version>
</dependency>
  • 第四步,各个服务的application.yml中增加seata配置;
#seata配置
seata:
    tx-service-group: ruyuan-eshop-order-group
    service:
        grouplist:
            ruyuan-eshop-seata: 127.0.0.1:8091
        vgroup-mapping:
            ruyuan-eshop-order-group: ruyuan-eshop-seata
  • 第五步,事务发起点增加 seata 全局事务注解,下游的事务使⽤普通的@Transactional即可;

全局事务注解,@GlobalTransactional(rollbackFor = Exception.class);

本地事务注解,@Transactional(rollbackFor = Exception.class);

关于 Seata 框架分布式事务运行原理,后续分析,源码层面;

原理分析

004_订单系统生单链路 Seata AT模式分布式事务原理.png

undo log运行原理,插入前镜像 + 后镜像数据,事务回滚处理,生成逆向逻辑;

{
	"branchId": 641789253,
	"undoItems": [{
		"afterImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "GTS"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"beforeImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "TXC"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"sqlType": "UPDATE"
	}],
	"xid": "xid:xxx"
}

整个全局事务提交成功之后,会删除各个分支事务的undo log日志;

并发能力分析

库存锁定,发生全局锁争用,等待获取全局锁,创建订单,执行耗时逻辑,导致并发处理能力降低;

问题分析:库存数据,一个商品就一条库存数据,如果有多个请求同时购买一个商品,必然导致,多个分布式事务都会去竞争和等待同一个商品的一条库存数据的全局锁;

规避全局锁争用问题,提供其它技术方案进行选择;

库存分段思想,落地极为复杂;

RocketMQ 柔性事务,数据最终一致,替换 seata 刚性事务;

其它模式分布式事务方案,Seata TCC、Seata Saga、Seata XA事务方案,Seata AT + Seata TCC混合分布式事务方案;

Seata TCC 异构存储

库存更新逻辑优化升级,库存服务引入Redis缓存,库存数据异构存储,支撑秒杀、大促、抢购等场景; 基于Seata TCC模式实现

  1. 第一步,注册开启全局事务,xid;
  2. 第二步,注册分支事务,branch id;
  3. 第三步,分支事务,执行 try 逻辑,预留资源;
  4. 第四步,分支事务,try 逻辑执行成功,上报分支事务状态到Seata Server;
  5. 如果,所有分支事务的 try 逻辑都执行成功;
  6. 第五步,提交分支事务,分支事务,commit,实际业务操作,事务提交成功;
  7. 如果,存在分支事务的 try 逻辑执行失败,上报分支事务状态到Seata Server;
  8. 第六步,回滚分支事务,分支事务,cancel,预留资源逆向操作,事务提交失败;
/**
 * 执行扣减商品库存逻辑
 */
@GlobalTransactional(rollbackFor = Exception.class)
public void doDeduct(DeductStockDTO deductStock) {
	//1、执行执行mysql库存扣减
	boolean result = lockMysqlStockTccService.deductStock(null,deductStock);
	if(!result) {
		throw new InventoryBizException(InventoryErrorCodeEnum.PRODUCT_SKU_STOCK_NOT_FOUND_ERROR);
	}

	//2、执行redis库存扣减
	result = lockRedisStockTccService.deductStock(null,deductStock);
	if(!result) {
		throw new InventoryBizException(InventoryErrorCodeEnum.PRODUCT_SKU_STOCK_NOT_FOUND_ERROR);
	}
}

具体业务实现参考代码

005_Seata TCC库存异构存储分布式事务原理分析.png

另外,还需要关注一下‘空悬挂’‘空回滚’问题;

场景1,Try操作耗时过长,全局事务,误认为分支事务失败,执行 Cancel 操作,然后又执行 Try 操作;

场景2,Try操作执行失败,全局事务,误以为分支事务成功,执行 Commit 操作,然后又执行 Cancel 操作;

解决方案:本地缓存组件,记录Try操作的状态,记录空回滚记录,同时,也要保证二阶段重试的幂等性;


生单链路 AT + TCC 混合事务

生单链路终极方案,最大的好处就是避免AT模式下,库存锁定出现的全局锁争用问题

思考,库存锁定,能否异步处理

006_Seata AT + TCC混合事务方案升级生单链路技术方案.png

补充一点,没有引入分布式事务方案,纯补偿机制,核心思想:基于操作日志记录,补偿处理

007_生单链路纯补偿方案分析.png