我们在上一章已经测试了Seata的AT模式的分布式事务,不过每个SpringBoot中都是直接用IP+端口的模式去调用其他服务的,现在我们将上一章中的4个微服务注册到Nacos,并且通过Feign来实现远程调用。
首先是在父类工程“springboot-mybatis”的 pom.xml 中引入 springcloud的依赖。
定义新的版本号:
Greenwich.SR2
2.1.0.RELEASE
在
org.springframework.cloud
spring-cloud-dependencies
${spring.cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring.cloud.alibaba.version}
pom
import
然后在 4个 子工程pom.xml 追加依赖:
org.springframework.cloud
spring-cloud-starter-openfeign
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
然后改造 sbm-order-service 工程,将原先的 accountClient 改为 Feign方式远程调用
/sbm-order-service/src/main/java/io/seata/samples/order/service/OrderService.java
//旧的实现
//accountClient.debit(userId, orderMoney);
//改为Feign远程调用
accountFeignClient.debit(userId, orderMoney);
//accountClient 中的实现
public void debit(String userId, BigDecimal orderMoney) {
String url = "http://127.0.0.1:8083?userId=" + userId + "&orderMoney=" + orderMoney;
try {
restTemplate.getForEntity(url, Void.class);
} catch (Exception e) {
log.error("debit url {} ,error:", url, e);
throw new RuntimeException();
}
}
//accountFeignClient的实现 , 改成Feign的形式
@FeignClient(name = "sbm-account-service")
public interface AccountFeignClient {
@GetMapping("/")
public void debit(@RequestParam("userId") String userId,@RequestParam("orderMoney") BigDecimal orderMoney) ;
}
改造 sbm-business-service ,将旧的client 改为 feign模式的client
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
// storageClient.deduct(commodityCode, orderCount);
// orderClient.create(userId, commodityCode, orderCount);
storageFeignClient.deduct(commodityCode, orderCount);
orderFeignClient.create(userId, commodityCode, orderCount);
}
@FeignClient(name = "sbm-order-service")
public interface OrderFeignClient {
@GetMapping("/api/order/debit")
public void create(@RequestParam("userId") String userId,
@RequestParam("commodityCode") String commodityCode,
@RequestParam("count") int orderCount);
}
@FeignClient(name = "sbm-storage-service")
public interface StorageFeignClient {
@GetMapping("/api/storage/deduct")
public void deduct(@RequestParam("commodityCode") String commodityCode,
@RequestParam("count") int orderCount) ;
}
在sbm-common-service 工程中添加一个 Feign的拦截器
因为seata是使用xid来作为1个分布式事务标识,xid首先保存在事务开端的微服务的ThreadLocal中,需要将这个xid在整个调用过程中都传递下去。
可以参考一下 seata 在不同RPC框架中的 适配
https://seata.io/zh-cn/docs/user/microservice.html
以下代码放入sbm-common-service中的 interceptor 包下就可以了。
package io.seata.samples.common.interceptor;
import org.springframework.context.annotation.Configuration;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import io.seata.common.util.StringUtils;
import io.seata.core.context.RootContext;
@Configuration
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String xid = RootContext.getXID();
if (StringUtils.isNotBlank(xid)) {
System.out.println("feign xid:"+xid);
}
requestTemplate.header(RootContext.KEY_XID, xid);
}
}
最后把所有的子工程的 application.yml 中的spring配置添加 nacos 的服务发现地址。
spring:
application:
name: “对应的工程名 例如 sbm-account-service”
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
万事俱备,只欠东风了!
把4个微服务跑起来吧,哦对了,还需要先开启 nacos服务,seata服务。
接下来我们到nacos后台瞅一眼。
看看,我们这4个微服务也已经注册到nacos啦!
然后我们再调用之前的 下单总接口
数据库方面,调用前是这样子的:
调用后:
,
测试一下回滚:
看一下具体的异常:(xid已经成功打印出来,此次请求,每个微服务都应该保持相同xid)
查看数据库:
三个表的数据没有变化。
后续我又执行过commit正常的接口,发现主键ID中间跳过了几个数值,说明之前插入后又回滚了。
实际上AT模式下,我们需要明白它的特性,避免采坑
AT模式并不是直接采用的数据库事务锁,而是利用undo_log来实现数据的存档和回滚,即在事务开始时进行SQL的解析,把涉及到的修改进行了快照,然后再执行完事务后判断是提交还是回滚,若是提交,则直接删掉快照;而若是回滚,则把涉及到的数据还原到快照状态。
AT的一阶段
AT的二阶段-提交操作
AT的二阶段-回滚操作
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。
我们来实验一下,首先我们让回滚接口在执行库存扣减后,休眠30秒,再执行下单操作。
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
// storageClient.deduct(commodityCode, orderCount);
// orderClient.create(userId, commodityCode, orderCount);
storageFeignClient.deduct(commodityCode, orderCount);
//休眠30秒,期间手动去修改数据库模拟脏写
try {
Thread.sleep(1000*30);
} catch (InterruptedException e) {
e.printStackTrace();
}
orderFeignClient.create(userId, commodityCode, orderCount);
}
然后执行 回滚接口 http://localhost:8084/api/business/purchase/rollback
库存数据库中生成了undo_log记录
此时库存由990 变为 989,注意 接口现在还没执行完,(此时其他事务是可以读取到中间数据的,会产生脏读)
如果此时,将这个989 修改掉,改成991。
会发现事务结束后,会停留在991,而不是990(正确回滚应该是990)
并且undo_log记录不会被删除,另外新的请求想要修改这份数据,无法修改。
也就是上面提到的,出现脏写的情况,只能手动处理。(此时seata一直在监测对比,如果这个值重回 989, 则seata会重新执行回滚操作,即将值回滚到990,并删除undo_log,解除数据锁)
2020-02-28 13:25:05,734 INFO [rpcDispatch_RMROLE_1_8] io.seata.rm.AbstractRMHandler [AbstractRMHandler.java : 122] Branch Rollbacking: 192.168.2.108:8091:2036587567 2036587568 jdbc:mysql://127.0.0.1:3306/seata_storage
2020-02-28 13:25:05,737 INFO [rpcDispatch_RMROLE_1_8] i.s.r.d.undo.AbstractUndoExecutor [AbstractUndoExecutor.java : 238] Field not equals, name count, old value 989, new value 991
2020-02-28 13:25:05,739 INFO [rpcDispatch_RMROLE_1_8] i.s.rm.datasource.DataSourceManager [StackTraceLogger.java : 38] branchRollback failed reason [Branch session rollback failed and try again later xid = 192.168.2.108:8091:2036587567 branchId = 2036587568 Has dirty records when undo.]
2020-02-28 13:25:05,739 INFO [rpcDispatch_RMROLE_1_8] io.seata.rm.AbstractRMHandler [AbstractRMHandler.java : 130] Branch Rollbacked result: PhaseTwo_RollbackFailed_Retryable
重新回滚成功,删除undo_log