JeeSite 4.x

Spring Boot 最好的快速开发平台

微服务分布式事务解决方案 Seata 框架(AT模式)

Seata 是什么?

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。

Seata 文档地址:http://seata.io/zh-cn/docs/overview/what-is-seata.html

JeeSite 中如何使用?

适用于 JeeSite V4.2.3+ 版本。

配置 Seata 服务端

启动 Seata 服务

找到 /jeesite-cloud-module-seata/../SeataApplication.java 类,运行 Application 程序。

启动完成后,日志无报错,即可。

配置 Seata 客户端

下面依照 Seata 的 AT 模式实现客户端,举例如下:

1、执行数据库脚本

Seata 要求 AT 客户端需要建立一张日志表,执行脚本:

  • /jeesite-cloud-module-seata/db/client/at/mysql.sql
  • /jeesite-cloud-module-seata/db/client/at/oracle.sql
  • /jeesite-cloud-module-seata/db/client/at/postgresql.sql

2、关闭 JTA 开启 Seata 事务

打开分布式统一配置文件 /jeesite-cloud-config/../jeesite-cloud-43/application.yml 如果使用 Nacos 请在 Nacos 里配置该文件。

# 关闭 JTA XA 事务
jdbc:
  jta:
    enabled: false

# 开启 Seata
seata:
  enabled: true

JeeSite 里的 JTA (XA) 事务是有 Atomikos 实现的,为了不对 LCN 事务所影响,所以需要关闭 JTA 事务。

2、关闭 Ribbon 的重试机制

打开分布式统一配置文件 /jeesite-cloud-config/../jeesite-cloud-43/application.yml

# v4.3 之前版本需设置,之后 ribbon 已经移除,无需设置
ribbon:
  MaxAutoRetriesNextServer: 0

为什么要关闭服务调用的重试。远程业务调用失败有两种可能: (1),远程业务执行失败 (2)、远程业务执行成功,网络失败。对于第 2 种,事务场景下重试会发生,某个业务执行两次的问题。如果业务上控制某个事务接口的幂等,则不用关闭重试。

3、pom.xml 增加依赖

<!-- 分布式事务 -->
<dependency>
    <groupId>com.jeesite</groupId>
    <artifactId>jeesite-cloud-module-seata-client</artifactId>
    <version>${project.parent.version}</version>
</dependency>

添加到每个 Web 微服务的 pom.xml 文件中。

4、编写业务测试代码

由于 Seata 代码嵌入性非常低,不需要你关注太多事务方面的东西。开发模式和你本地开发模式一致。

在需要处理事务的 Controller 的方法上添加 @GlobalTransaction 注解即可,本地事务注解保留。

JeeSite Cloud 已为你编写好的测试代码,依次打开如下文件:

  • jeesite-cloud-module-core/../TransTestService.java
  • jeesite-cloud-module-test1/../TestDataService.java
  • jeesite-cloud-module-test2/../TestTreeService.java

全局搜索 @GlobalTransaction 并将每个文件里的 @GlobalTransaction 前的 // 注释去掉,并导入包即可(v4.3版本默认已经去掉注释)

为了方便理解,下面将关键测试代码进行展示,如下:

jeesite-cloud-module-core/../TransTestService.java

/**
 * 事务测试,第二个接口调用故意抛出异常
 */
@GlobalTransaction
@Transactional(readOnly=false)
public void transTest(TestData testData) {
	
	// 正常保存 testData 数据
	testData.setIsNewRecord(true);
	testData.setId(IdGen.randomBase62(5));
	testData.setTestInput(testData.getId());
	testData.setTestTextarea(testData.getId());
	testDataService.save(testData);
	
	// 保存 testTree 失败,抛出异常,testData 应回滚
	TestTree testTree = new TestTree();
	testTree.setIsNewRecord(true);
	testTree.setTreeCode(testData.getId());
	testTree.setTreeName(testData.getId());
	TestTree where = new TestTree();
	where.setParentCode(TestTree.ROOT_CODE);
	TestTree last = testTreeService.getLastByParentCode(where);
	if (last != null){
		testTree.setTreeSort(last.getTreeSort()+30);
	}
	// 设置一个超出数据库范围的值,抛出数据库异常
	StringBuilder sb = new StringBuilder();
	for (int i=0; i<500; i++){
		sb.append("transTest" + i);
	}
	testTree.setTreeName(sb.toString());
	testTreeService.save(testTree);

	// 有些情况可能需要手动回滚事务,调用该方法即可
	//try {
	//	GlobalTransactionContext.reload(RootContext.getXID()).rollback();
	//} catch (TransactionException e) {
	//	e.printStackTrace();
	//}
}

/**
 * 事务验证,返回空,则事务回滚成功
 */
public boolean transValid(TestData testData) {
	if (StringUtils.isBlank(testData.getId())){
		return true;
	}
	return testDataService.get(testData.getId()) == null;
}

jeesite-cloud-module-core/../TransTestController.java

@Controller
@RequestMapping(value = "${adminPath}/trans")
public class TransTestController extends BaseController {

	@Autowired
	private TransTestService transTestService;
	
	/**
	 * 查询列表数据
	 */
	@RequestMapping(value = "test")
	@ResponseBody
	public String test(TestData testData) {
		try{
			transTestService.transTest(testData);
		}catch (Exception e) {
			logger.debug("事务测试信息,报错回滚:" + e.getMessage(), e);
		}
		boolean bl = transTestService.transValid(testData);
		return renderResult(Global.TRUE, "事务测试"+(bl?"成功,数据已":"失败,数据未")+"回滚!");
	}

}

jeesite-cloud-module-test1/../TestDataService.java

@GlobalTransaction
@Transactional(readOnly=false)
public void save(TestData testData) {
		super.save(testData);
}

jeesite-cloud-module-test2/../TestTreeService.java

@GlobalTransaction
@Transactional(readOnly=false)
public void save(TestTree testTree) {
		super.save(testTree);
}

注意:Seata 的 @GlobalTransaction 注解,需要在 Controller 层上去加,否则可能不生效。

5、启动各个微服务

访问分布式事务,测试地址:http://127.0.0.1:8980/js/a/trans/test

若提示 “事务测试成功” 即正常运行。

关注 JeeSite 公众号,了解最新动态