3LCN-TCC模式 · SpringCloud微服务实战 · 看云
导航
本节代码地址
GitHub:https://github.com/xuyisu/fw-spring-cloud/tree/master/fw-cloud-transaction/fw-cloud-transaction-lcn/fw-cloud-transaction-lcn-tcc
GitHub:https://github.com/xuyisu/fw-spring-cloud/tree/master/fw-cloud-transaction/fw-cloud-transaction-lcn/fw-cloud-transaction-lcn-txmanager
1、原理介绍:
TCC事务机制相对于传统事务机制(X/Open XA Two-Phase-Commit),其特征在于它不依赖资源管理器(RM)对XA的支持,而是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。主要由三步操作,Try: 尝试执行业务、 Confirm:确认执行业务、 Cancel: 取消执行业务。
2、模式特点:
- 该模式对代码的嵌入性高,要求每个业务需要写三种步骤的操作。
- 该模式对有无本地事务控制都可以支持使用面广。
- 数据一致性控制几乎完全由开发者控制,对业务开发难度要求高。
3. 环境准备
| 中间件 | 版本 |
|---|---|
| MySQL(可以参照容器化章节) | 5.7+ |
| Redis(可以参照容器化章节) | 4.0+ |
3.1 数据库准备
新建fw-txmanager数据库,并且执行以下脚本创建异常表,表名不能变。
CREATE TABLE `t_tx_exception` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`group_id` varchar(64) DEFAULT NULL,
`unit_id` varchar(32) DEFAULT NULL,
`mod_id` varchar(128) DEFAULT NULL,
`transaction_state` tinyint(4) DEFAULT NULL,
`registrar` tinyint(4) DEFAULT NULL,
`remark` varchar(4096) DEFAULT NULL,
`ex_state` tinyint(4) DEFAULT NULL COMMENT '0 未解决 1已解决',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='tx-manager异常表'
4. 新建TCC 模块
本模块中我们将新建两个模块,我们将模拟电商购物环节中的下单->发货的过程,两个服务通过Feign进行远程调用。两个模块的名称分别为fw-cloud-transaction-lcn-tcc-order和fw-cloud-transaction-lcn-tcc-send
4.1 maven 依赖
两个模块所使用的依赖也是相同的,这里可以看到,我们依赖了上面封装的fw-cloud-transaction-base-dao,不需要再自己写基本的mapper等,因为是基于Eureka 服务发现和Feign 远程调用的,因此引入了响应的包。其中最重要的就是引入txlcn-tc,txlcn-txmsg-netty包,否者不能实现和TxManager的通信及事务控制。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- base-dao -->
<dependency>
<groupId>com.yisu.cloud</groupId>
<artifactId>fw-cloud-transaction-base-dao</artifactId>
<version>${version}</version>
</dependency>
<!-- TC依赖 -->
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-tc</artifactId>
</dependency>
<!-- TC和TM通信依赖于Netty -->
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-txmsg-netty</artifactId>
</dependency>
<!--服务发现-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 数据连接池-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${HikariCP.version}</version>
</dependency>
</dependencies>
并在MySql中新建fw_transaction库,并执行以下脚本,供fw-cloud-transaction-lcn-tcc-order和fw-cloud-transaction-lcn-tcc-send使用
CREATE TABLE `fw_trade_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`status` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '订单状态 1.待支付 2.待发货 3.待收货 4.订单完成 5.订单关闭',
`status_dsc` varchar(100) CHARACTER SET utf8 DEFAULT '' COMMENT '状态描述',
`product_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '商品id',
`product_name` varchar(100) NOT NULL DEFAULT '' COMMENT '商品名称',
`user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户id',
`order_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '订单id',
`order_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单总额',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'
4.2 fw-cloud-transaction-lcn-tcc-order模块
4.2.1 订单接口创建
再订单模块中创建一个订单接口,并在接口中设置支付成功的方法
public interface OrderService{
void saveAndPayOrder(String productName);
}
4.2.2 订单接口实现
订单在支付成功以后会远程调用发货服务进行发货,需要在Service 上加上@TccTransaction,开启TCC模式的分布式事务控制,并且设置订单的状态为待支付,然后编写confirm和cancel 方法,规则是confirm\cancel+业务方法名
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private FwTradeLogService fwTradeLogService;
@Autowired
private RemoteSendServiceFeign remoteSendServiceFeign;
ConcurrentHashMap<String, Long> hashMap = new ConcurrentHashMap<>();
@TccTransaction
@Override
public void saveAndPayOrder(String productName) {
FwTradeLog fwTradeLog =new FwTradeLog(StatusEnum.TWO);
fwTradeLog.setProductId(System.currentTimeMillis());
fwTradeLog.setProductName(productName);
fwTradeLogService.save(fwTradeLog);
log.info("[订单状态{}]=>{},当前商品id=>{},商品名称=>{}",fwTradeLog.getOrderId(), StatusEnum.TWO.getDesc(),fwTradeLog.getProductId(),fwTradeLog.getProductName());
remoteSendServiceFeign.sendOrder(fwTradeLog);
hashMap.put(TracingContext.tracing().groupId(), fwTradeLog.getId());
}
public void confirmSaveAndPayOrder(String productName){
log.info("当前产品{},事务执行成功",productName);
hashMap.remove(TracingContext.tracing().groupId());
}
public void cancelSaveAndPayOrder(String productName){
Long tradeLogId = hashMap.get(TracingContext.tracing().groupId());
log.info("当前产品{},事务执行失败,回滚tradeLogId为{}的数据", productName,tradeLogId);
fwTradeLogService.removeById(tradeLogId);
hashMap.remove(TracingContext.tracing().groupId());
}
}
4.2.3 Feign 接口配置
这里我们配置远程调用的服务名称是fw-transaction-lcn-tcc-send,并且设置了fallback,并且指定了远程调用的方法是sendOrder。
@FeignClient(value = "fw-transaction-lcn-tcc-send", fallbackFactory = RemoteSendServiceFallback.class)
public interface RemoteSendServiceFeign {
@PostMapping("send")
void sendOrder(@RequestBody FwTradeLog tradeLog);
}
4.2.4 Fallback 接口配置
在fallback输出异常日志
@Component
@Slf4j
public class RemoteSendServiceFallback implements FallbackFactory<RemoteSendServiceFeign> {
@Override
public RemoteSendServiceFeign create(Throwable throwable) {
return tradeLog -> log.error("远程调用失败",throwable);
}
}
4.2.5 Controller 控制层
配置订单服务对外的接口,方便演示
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("saveOrder")
public FwResult saveOrder(String productName){
orderService.saveAndPayOrder(productName);
return FwResult.ok();
}
}
4.2.6 启动类配置
这里需要加上@EnableDistributedTransaction开启分布式的调用,加上@EnableFeignClients开启Feign的远程调用,加上@EnableDiscoveryClient开启服务的注册和发现。
@EnableDistributedTransaction
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class FwTransactionLcnTccOrderApplication {
public static void main(String[] args) {
SpringApplication.run(FwTransactionLcnOrderApplication.class, args);
}
}
4.2.7 应用配置
配置中需要配置数据库和Redis的基本信息,以及TxManager的连接信息,并且设置了Eureka的连接信息和开启Feign的远程调用
server:
port: 9002
spring:
application:
name: fw-transaction-lcn-tcc-order
#数据库配置 start
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://${dbIp}:3306/fw_transaction?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
hikari:
connection-timeout: 20000
auto-commit: true
max-lifetime: 1200000
minimum-idle: 5
maximum-pool-size: 12
idle-timeout: 300000
redis:
port: 6379
host: ${redisIp}
database: 0
password: 123456
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
tx-lcn:
client:
manager-address: 127.0.0.1:8888
logger:
enabled: true
feign:
hystrix:
enabled: true
hystrix:
shareSecurityContext: true #设置为true,这样做会自动配置Hystrix并发策略插件挂钩,将SecurityContext从主线程传输到Hystrix命令使用的线程
4.3 fw-cloud-transaction-lcn-tcc-send模块
4.3.1 发货接口创建
配置发货接口,并且创建发货的方法
public interface SendService {
void sendOrder(FwTradeLog fwTradeLog);
}
4.3.2 发货接口实现
这里在实现方法上加上@TccTransaction,并且设置订单的状态为待收货,然后编写confirm和cancel 方法,规则是confirm\cancel+业务方法名
@Service
@Slf4j
public class SendServiceImpl implements SendService {
@Autowired
private FwTradeLogService fwTradeLogService;
ConcurrentHashMap<String, Long> hashMap = new ConcurrentHashMap<>();
@TccTransaction
@Override
public void sendOrder(FwTradeLog fwTradeLog) {
fwTradeLog.setStatus(StatusEnum.THREE.getValue());
fwTradeLog.setStatusDsc(StatusEnum.THREE.getDesc());
fwTradeLogService.save(fwTradeLog);
log.info("[订单状态{}]=>{},当前商品id=>{},商品名称=>{}",fwTradeLog.getOrderId(), StatusEnum.THREE.getDesc(),fwTradeLog.getProductId(),fwTradeLog.getProductName());
hashMap.put(TracingContext.tracing().groupId(), fwTradeLog.getId());
}
public void confirmSendOrder(FwTradeLog fwTradeLog){
log.info("当前产品{},事务执行成功",fwTradeLog.getProductName());
hashMap.remove(TracingContext.tracing().groupId());
}
public void cancelSendOrder(FwTradeLog fwTradeLog){
Long tradeLogId = hashMap.get(TracingContext.tracing().groupId());
log.info("当前产品{},事务执行失败,回滚tradeLogId为{}的数据", fwTradeLog.getProductName(),tradeLogId);
fwTradeLogService.removeById(tradeLogId);
hashMap.remove(TracingContext.tracing().groupId());
}
}
4.3.3 发货控制层实现
提供给订单模块调用的接口,注意方法名和请求方式要保持一致
@RestController
public class SendController {
@Autowired
private SendService sendService;
@PostMapping("send")
public void sendOrder(@RequestBody FwTradeLog tradeLog) {
sendService.sendOrder(tradeLog);
}
}
4.3.4 启动类
需要加上@EnableDistributedTransaction注解开启分布式事务,加上@EnableDiscoveryClient注解开启服务的注册与发现
@EnableDistributedTransaction
@SpringBootApplication
@EnableDiscoveryClient
public class FwTransactionLcnTccSendApplication {
public static void main(String[] args) {
SpringApplication.run(FwTransactionLcnSendApplication.class, args);
}
}
4.3.5 应用配置
配置中需要配置数据库和Redis的基本信息,以及TxManager的连接信息,并且设置了Eureka的连接信息和开启Feign的远程调用
server:
port: 9003
spring:
application:
name: fw-transaction-lcn-tcc-send
#数据库配置 start
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://${dbIp}:3306/fw_transaction?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
hikari:
connection-timeout: 20000
auto-commit: true
max-lifetime: 1200000
minimum-idle: 5
maximum-pool-size: 12
idle-timeout: 300000
redis:
port: 6379
host: ${redisIp}
database: 0
password: 123456
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
tx-lcn:
client:
manager-address: 127.0.0.1:8888
4.4 启动服务
分别启动Eureka、fw-transaction-lcn-tcc-send、fw-transaction-lcn-tcc-order
TxManager 可以看到两个事务注册上来
Postman输入localhost:9002/saveOrder
可以看到订单服务的日志如下
2020-04-06 19:29:22.292 INFO 3829 --- [nio-9002-exec-1] c.y.t.l.o.service.impl.OrderServiceImpl : [订单状态348582]=>待发货,当前商品id=>1586094502191,商品名称=>Mac pro 2019款
发货服务的日志如下
2020-04-06 19:29:23.202 INFO 3841 --- [nio-9003-exec-1] c.y.t.l.s.service.impl.SendServiceImpl : [订单状态348582]=>待收货,当前商品id=>1586094502191,商品名称=>Mac pro 2019款
数据库记录两条记录
现在让订单服务的方法抛出异常
直接在方法中加一个抛异常的语句int i=1/0;,如下所示
@TccTransaction
@Override
public void saveAndPayOrder(String productName) {
FwTradeLog fwTradeLog =new FwTradeLog(StatusEnum.TWO);
fwTradeLog.setProductId(System.currentTimeMillis());
fwTradeLog.setProductName(productName);
fwTradeLogService.save(fwTradeLog);
log.info("[订单状态{}]=>{},当前商品id=>{},商品名称=>{}",fwTradeLog.getOrderId(), StatusEnum.TWO.getDesc(),fwTradeLog.getProductId(),fwTradeLog.getProductName());
remoteSendServiceFeign.sendOrder(fwTradeLog);
hashMap.put(TracingContext.tracing().groupId(), fwTradeLog.getId());
int i=1/0;
}
重启订单服务
Postman输入localhost:9002/saveOrder
可以看到订单服务的日志如下,可以看到实际新增了一条记录,但是因为抛异常,被回滚掉了
2020-04-06 19:29:34.093 INFO 4513 --- [nio-9002-exec-2] c.y.t.t.o.service.impl.OrderServiceImpl : [订单状态652702]=>待发货,当前商品id=>1586172574019,商品名称=>Mac pro 2020款
2020-04-06 19:29:34.696 INFO 4513 --- [-lcn-tcc-send-1] c.n.u.concurrent.ShutdownEnabledTimer : Shutdown hook installed for: NFLoadBalancer-PingTimer-fw-transaction-lcn-tcc-send
2020-04-06 19:29:34.696 INFO 4513 --- [-lcn-tcc-send-1] c.netflix.loadbalancer.BaseLoadBalancer : Client: fw-transaction-lcn-tcc-send instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=fw-transaction-lcn-tcc-send,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2020-04-06 19:29:34.705 INFO 4513 --- [-lcn-tcc-send-1] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
2020-04-06 19:29:34.727 INFO 4513 --- [-lcn-tcc-send-1] c.netflix.config.ChainedDynamicProperty : Flipping property: fw-transaction-lcn-tcc-send.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2020-04-06 19:29:34.729 INFO 4513 --- [-lcn-tcc-send-1] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client fw-transaction-lcn-tcc-send initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=fw-transaction-lcn-tcc-send,current list of Servers=[192.168.0.102:9003],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:192.168.0.102:9003; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@68e61e41
2020-04-06 19:29:35.058 ERROR 4513 --- [nio-9002-exec-2] c.c.txlcn.tc.core.DTXServiceExecutor : business code error @group(8a815c4628537)
2020-04-06 19:29:35.098 INFO 4513 --- [nio-9002-exec-2] c.y.t.t.o.service.impl.OrderServiceImpl : 当前产品Mac pro 2020款,事务执行失败,回滚tradeLogId为9的数据
发货服务的日志如下,因为上游系统发生异常回滚,所以发货服务已经发货的的记录也被回滚了
2020-04-06 19:29:35.014 INFO 4503 --- [nio-9003-exec-1] c.y.t.t.s.service.impl.SendServiceImpl : [订单状态652702]=>待收货,当前商品id=>1586172574019,商品名称=>Mac pro 2020款
2020-04-06 19:29:35.078 INFO 4503 --- [c-rpc-service-0] c.y.t.t.s.service.impl.SendServiceImpl : 当前产品Mac pro 2020款,事务执行失败,回滚tradeLogId为10的数据
数据库记录并没有,说明TxManager 进行了回滚





