1、Demo项目依赖
- springboot-2.4.7
- spring-cloud 2020.0.1
- spring-cloud-alibaba 2.21.1
- openfeign 3.0.1
- seata 1.4.2
- spring-cloud-starter-alibaba-nacos-discovery 2.21.1
mybatis-springboot-starter 2.1.2
2、Demo案例设计
本demo设计了一个转账交易的业务场景,分为两个部分,第一部分是本地调用取款服务,第二部分是远程调用存款服务,远程调用通过FeignClient实现,以nacos做注册中心,以seata支持分布式事务,使用本地配置。
3、Demo工程搭建
搭建Springboot项目,并引入相关依赖,分别搭建2个项目demo01和demo02,并启动一个seata-server实例,启动一个nacos实例。
demo的配置文件application.yml如下: ```java server: port: 8081
spring: application: name: seata-demo-01 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://139.198.170.206:3306/seata-demo-bank01?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8 username: root password: Xw159753. cloud: nacos: discovery: server-addr: 139.198.170.206:8848 group: SEATA_GROUP namespace: b0e7a06f-af32-436b-97c0-1639e7e03b49
seata: enabled: true application-id: ${spring.application.name} enable-auto-data-source-proxy: true tx-service-group: seata-demo-01-group registry: type: nacos nacos: server-addr: 139.198.170.206:8848 group: SEATA_GROUP namespace: b0e7a06f-af32-436b-97c0-1639e7e03b49 username: “” password: “” config: type: file file: name: file.conf
logging: level: io: seata: info
mybatis: mapperLocations: classpath:mapper/.xml configuration: map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
- demo中seata的配置文件file.conf如下:
```java
transport {
# tcp udt unix-domain-socket
type = "TCP"
#NIO NATIVE
server = "NIO"
#enable heartbeat
heartbeat = true
# the client batch send request enable
enableClientBatchSendRequest = true
#thread factory for netty
threadFactory {
bossThreadPrefix = "NettyBoss"
workerThreadPrefix = "NettyServerNIOWorker"
serverExecutorThread-prefix = "NettyServerBizHandler"
shareBossWorker = false
clientSelectorThreadPrefix = "NettyClientSelector"
clientSelectorThreadSize = 1
clientWorkerThreadPrefix = "NettyClientWorkerThread"
# netty boss thread size,will not be used for UDT
bossThreadSize = 1
#auto default pin or 8
workerThreadSize = "default"
}
shutdown {
# when destroy server, wait seconds
wait = 3
}
serialization = "seata"
compressor = "none"
}
service {
#transaction service group mapping
#该配置项要与seata-server的cluster配置项的值一样
vgroupMapping.seata-demo-01-group = "bank-demo"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
client {
rm {
asyncCommitBufferLimit = 10000
lock {
retryInterval = 10
retryTimes = 30
retryPolicyBranchRollbackOnConflict = true
}
reportRetryCount = 5
tableMetaCheckEnable = false
reportSuccessEnable = false
}
tm {
commitRetryCount = 5
rollbackRetryCount = 5
}
undo {
dataValidation = true
logSerialization = "jackson"
logTable = "undo_log"
}
log {
exceptionRate = 100
}
}
4、Demo业务场景开发
采用经典的springMVC架构,在AccountCotroller中暴露服务接口,在Service中编写服务逻辑,在mapper中进行数据库交互。重点不同的是,需要开发seata-tcc相关的接口。
- 工程结构如下:
4.1、开发AccountTccAction接口和实现
AccountTccAction接口如下:
@LocalTCC
public interface AccountTccAction {
/**
* 取款
* 使用 @BusinessActionContextParameter 注解的参数数据会被存入 BusinessActionContext
* @param businessActionContext BusinessActionContext 上下文对象,用来在两个阶段之间传递数据
* @param accountId
* @param money
* @return
*/
@TwoPhaseBusinessAction(name = "fetchBalanceTcc",commitMethod = "fetchBalanceCommit",rollbackMethod = "fetchBalanceRollback")
public String fetchBalanceTry(BusinessActionContext businessActionContext,
@BusinessActionContextParameter(paramName = "accountId") String accountId,
@BusinessActionContextParameter(paramName = "money") int money);
/**
* 取款服务 二阶段提交
* @param businessActionContext
*/
public void fetchBalanceCommit(BusinessActionContext businessActionContext);
/**
* 取款服务 二阶段回滚
* @param businessActionContext
*/
public void fetchBalanceRollback(BusinessActionContext businessActionContext);
}
AccountTccActionImpl实现类如下:
@Service
public class AccountTccActionImpl implements AccountTccAction {
private static final Logger log = LoggerFactory.getLogger(AccountService.class);
@Autowired
AccountMapper accountMapper;
@Autowired
AccountServiceFeign accountServiceFeign;
@Override
public String fetchBalanceTry(BusinessActionContext businessActionContext, String accountId, int money) {
Account account = accountMapper.getAccountInfoById(accountId);
int balance = account.getBalance() - money;
accountMapper.transformMoney(accountId,balance);
return "取款成功,账户余额:"+balance;
}
@Override
public void fetchBalanceCommit(BusinessActionContext businessActionContext) {
//为了演示,仅打印日志,并未进行实际的业务二次提交操作
String accountId = (String)businessActionContext.getActionContext("accountId");
int money = (int) businessActionContext.getActionContext("money");
log.info(">>>>>>>>>>>>>fetchBalanceCommit running, accountId:{}, money:{}",accountId,money);
}
@Override
public void fetchBalanceRollback(BusinessActionContext businessActionContext) {
//为了演示,仅打印日志,并未进行实际的业务回滚操作
String accountId = (String)businessActionContext.getActionContext("accountId");
int money = (int) businessActionContext.getActionContext("money");
log.info(">>>>>>>>>>>>>fetchBalanceRollback running, accountId:{}, money:{}",accountId,money);
}
}
4.2、开发远程调用的FeignClient
AccountServiceFeign如下: ```java @FeignClient(value = “seata-demo-02”, path = “/account”) @LocalTCC public interface AccountServiceFeign { /**
- 存款
- 使用 @BusinessActionContextParameter 注解标注的参数会自动注入到businessActionContext中
- @param accountId
- @param money
- @return
*/
@RequestMapping(“/depositBalance”)
@TwoPhaseBusinessAction(name = “remoteDepositBalance”,commitMethod = “remoteDepositBalanceCommit”,rollbackMethod = “remoteDepositBalanceRollback”)
public String remoteDepositBalance(@RequestBody BusinessActionContext businessActionContext,
@RequestMapping(“/depositBalanceCommit”) public Boolean remoteDepositBalanceCommit(@RequestBody BusinessActionContext businessActionContext); @RequestMapping(“/depositBalanceRollback”) public Boolean remoteDepositBalanceRollback(@RequestBody BusinessActionContext businessActionContext);@BusinessActionContextParameter(paramName = "accountId") @RequestParam("accountId")String accountId,
@BusinessActionContextParameter(paramName = "money") @RequestParam("money")int money);
}
<a name="ZPXiJ"></a>
## 4.3、在业务代码中调用转账服务,并开启全局事务
- AccountService如下:
```java
@Service
public class AccountService {
private static final Logger log = LoggerFactory.getLogger(AccountService.class);
@Autowired
AccountServiceFeign accountServiceFeign;
@Autowired
AccountTccAction accountTccAction;
/**
* 远程转账 使用LocalTcc模式
* @param localAccountIdSrc
* @param remoteAccountIdDest
* @param money
* @return
*/
@GlobalTransactional
public String remoteTransformMoneyTcc(String localAccountIdSrc,String remoteAccountIdDest, int money){
//本地取款
accountTccAction.fetchBalanceTry(null,localAccountIdSrc,money);
//远程调用存款
String result = accountServiceFeign.remoteDepositBalance(null,remoteAccountIdDest,money);
return result;
}
4.4、在Controller中暴露转账接口
AccountController如下:
@RestController
@RequestMapping(value="/account")
public class AccountController {
@Autowired
AccountService accountService;
/**
* 跨行转账,使用Tcc模式
* @param localAccountIdSrc
* @param remoteAccountIdDest
* @param money
* @return
*/
@RequestMapping("/remoteTransformMoneyTcc")
public String remoteTransformMoneyTcc(@RequestParam("localAccountIdSrc") String localAccountIdSrc,
@RequestParam("remoteAccountIdDest") String remoteAccountIdDest,
@RequestParam("money")int money){
return accountService.remoteTransformMoneyTcc(localAccountIdSrc,remoteAccountIdDest,money);
}
4.5、远程存款服务开发
RemoteAccountController如下: ```java @RestController @RequestMapping(value=”/account”) public class AccountController { private static final Logger log = LoggerFactory.getLogger(AccountController.class); @Autowired AccountService accountService;
/**
- 存款 一阶段try
- @param accountId
- @param money
- @return
*/
@RequestMapping(“/depositBalance”)
public String depositBalance(@RequestBody BusinessActionContext businessActionContext,
String xid = (String)businessActionContext.getXid(); log.info(“>>>>>>>>>>>>>>>>>>depositBalanceCommit running, xid:{}”,xid); return accountService.depositBalance(accountId,money); } /**@RequestParam("accountId") String accountId,
@RequestParam("money")int money){
- 存款 二阶段提交
- @param businessActionContext
- @return / @RequestMapping(“/depositBalanceCommit”) public Boolean depositBalanceCommit(@RequestBody BusinessActionContext businessActionContext){ String accountId = (String)businessActionContext.getActionContext(“accountId”); int money = (int)businessActionContext.getActionContext(“money”); String xid = (String)businessActionContext.getXid(); log.info(“>>>>>>>>>>>>>>>>>>depositBalanceCommit running, accountId:{}, money:{}, xid:{}”,accountId,money,xid); return true; } /*
- 存款 二阶段回滚
- @param businessActionContext
- @return */ @RequestMapping(“/depositBalanceRollback”) public Boolean depositBalanceRollback(@RequestBody BusinessActionContext businessActionContext){ String accountId = (String)businessActionContext.getActionContext(“accountId”); int money = (int)businessActionContext.getActionContext(“money”); String xid = (String)businessActionContext.getXid(); log.info(“>>>>>>>>>>>>>>>>>>depositBalanceRollback running, accountId:{}, money:{}, xid:{}”,accountId,money,xid); return true; }
5、总结
- @LocalTcc注解必须使用在interface上,不能直接在class上使用,否则不生效
- BusinessActionContext是seata-tcc模式的上下文,相关业务参数和分布式事务的参数都可以通过该上下文传递
- 使用@BusinessActionContextParameter(paramName = “accountId”)注解标注的参数,seata会自动注入到上下文BusinessActionContext中
- 需要使用@GlobalTransactional注解开启全局事务,否则不生效