微服务实战(十四)微服务分布式事务之集成 Nacos + Feign + Seata_AT

本章主要内容

我们在上一章已经测试了Seata的AT模式的分布式事务,不过每个SpringBoot中都是直接用IP+端口的模式去调用其他服务的,现在我们将上一章中的4个微服务注册到Nacos,并且通过Feign来实现远程调用。

接入Nacos和Feign

首先是在父类工程“springboot-mybatis”的 pom.xml 中引入 springcloud的依赖。

定义新的版本号:

<spring.cloud.version>Greenwich.SR2</spring.cloud.version>
<spring.cloud.alibaba.version>2.1.0.RELEASE</spring.cloud.alibaba.version>

在 <dependencyManagement> 节点中追加:

            <dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring.cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
			<dependency>
				<groupId>com.alibaba.cloud</groupId>
				<artifactId>spring-cloud-alibaba-dependencies</artifactId>
				<version>${spring.cloud.alibaba.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>

然后在 4个 子工程pom.xml 追加依赖:

         <dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>
		<dependency>
			<groupId>com.alibaba.cloud</groupId>
			<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
		</dependency>

然后改造 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

发布了36 篇原创文章 · 获赞 134 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/u011177064/article/details/104546645
今日推荐