spring cloud + 云原生 应用【seata】

seata

Seata关键字

XID:一个全局事务的唯一标识,由ip:port:sequence组成

Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。

Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

46fcd35d87e04c718bc3c1fce9cc2763.png

Seata 中 TM、RM 中 xid 传递过程,如果不了解 Seata 中的 xid,可以理解为全局的事物 ID,我们都知道 Seata 中分了三个角色,TC、TM、RM,其中 TC 为全局事物的协调者,TM 则为全局事物的发起者,RM 为全局事物的参与者,其中 TM 和 RM 我们可以看作一个组,有发起者必定有参与者,不然也没必要使用分布式事物了。那 TC 怎么知道谁和谁是一个组的呢,那就是本篇文章说的 xid,TC 主要通过 xid 来对当前的不同的连接来分组处理。

那 xid 怎么来的怎么传递的呢,下面我梳理了下过程:

首先 TM 注册到 TC 中, 要发起全局事物时,先向 TC 发送一个通知,然后TC 就会生成一个唯一的 ID 返还给 TM,这个ID 就是 xid。

TM 收到 xid 后放入当前线程的 ThreadLocal 中存放,在业务逻辑中我们使用 OpenFeign 调用其他服务的接口时,Seata 重写了 feign客户端,如果是RestTemplate的方式,Seata也写了请求拦截器,将当前 ThreadLocal 中的 xid 放入 header 中进行传递。

RM 收到请求后,首先在拦截器中尝试获取 header 中的 xid ,如果获取成功就将 xid 再放入当前 ThreadLocal 中。

然后 RM 通知 TC 自己是该 xid 的事物参与者,也就是注册该分支事务。

后面不管是提交事物和回滚事物,TC 通过该 xid, 都能准确的通知相应的服务了。

下载seata服务 Releases · seata/seata (github.com)

f0a859b2829d4e7188d313182e0e5ba8.png

修改 seata服务端配置文件application.yml

#  Copyright 1999-2019 Seata.io Group.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

server:
  port: 7091

spring:
  application:
    name: seata-server

logging:
  config: classpath:logback-spring.xml
  file:
    path: ${user.home}/logs/seata
  extend:
    logstash-appender:
      destination: 127.0.0.1:4560
    kafka-appender:
      bootstrap-servers: 127.0.0.1:9092
      topic: logback_to_logstash

console:
  user:
    username: seata
    password: seata
seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: file
  registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      # nacos ip地址
      application: serverAddr
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      #namespace: "nacos"
      username: "nacos"
      password: "nacos"
      #cluster: default
  store:
    # support: file 、 db 、 redis
    mode: file
#  server:
#    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
  security:
    secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
    tokenValidityInMilliseconds: 1800000
    ignore:
      urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login

seata-server.bat 启动seata服务

8f2b6f9b6a3e414ca4dc07337378ab47.png

微服务application.ymal加入以下seata配置


# Seata 配置项,对应 SeataProperties 类
seata:
  application-id: ${spring.application.name} # Seata 应用编号,默认为 ${spring.application.name}
  tx-service-group: ${spring.application.name}-group # Seata 事务组编号,用于 TC 集群名
  # Seata 服务配置项,对应 ServiceProperties 类
  service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      account-service-group: default
  # Seata 注册中心配置项,对应 RegistryProperties 类
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace:
      #可选
      username: nacos
      #可选
      password: nacos
      #可选
      application: serverAddr
      #默认值和 config 的 SEATA_GROUP 不一样
      group: SEATA_GROUP
      # 可选  默认
      cluster: default

启动后日志会打印注册到seata的信息

434c621599d24904b0c8684abcc5e257.png

示例代码
以下是示例代码,通过创建订单接口分别访问产品服务和账户服务进行分配产品和扣减余额后进行下单。
通过@GlobalTransactional可以使用seata的全局事务



    @Override
    @GlobalTransactional
    public Integer createOrder(Long userId, Long productId, Integer price) throws Exception {
        Integer amount = 1; // 购买数量,暂时设置为 1。

        logger.info("[createOrder] 当前 XID: {}", RootContext.getXID());

        // 扣减库存
        this.reduceStock(productId, amount);

        // 扣减余额
        this.reduceBalance(userId, price);

        // 保存订单
        OrderDO order = new OrderDO().setUserId(userId).setProductId(productId).setPayAmount(amount * price);
        orderDao.saveOrder(order);
        logger.info("[createOrder] 保存订单: {}", order.getId());

        // 返回订单编号
        return order.getId();
    }

 


当扣减库存和扣减余额分别请求处理成功后会立即更新数据库,如果在保存订单时失败,则seata的TC服务会通知产品和账户RM,进行扣减库存和扣减余额事务的回滚操作

Seata 事务传播(openfeign申明式调用xid流转)

微服务中事务的传播其实就是xid的传播,原理就是在com.alibaba.cloud.seata.feign.SeataFeignClient#getModifyRequest方法 ,该方法会在feign发起的请求头中放置seaa全局事务的xid

发起请求放置xid

package com.alibaba.cloud.seata.feign;
 
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import feign.Client;
import feign.Request;
import feign.Response;
import io.seata.core.context.RootContext;
 
import org.springframework.beans.factory.BeanFactory;
import org.springframework.util.StringUtils;
 
/**
 * 实现通过实现feign.Client接口,完成Feign调用传递XID
 * @author xiaojing
 */
public class SeataFeignClient implements Client {
 
	private final Client delegate;
 
	private final BeanFactory beanFactory;
 
	private static final int MAP_SIZE = 16;
 
	SeataFeignClient(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
		this.delegate = new Client.Default(null, null);
	}
 
	SeataFeignClient(BeanFactory beanFactory, Client delegate) {
		this.delegate = delegate;
		this.beanFactory = beanFactory;
	}
 
	@Override
	public Response execute(Request request, Request.Options options) throws IOException {
 
		Request modifiedRequest = getModifyRequest(request);
		return this.delegate.execute(modifiedRequest, options);
	}
 
	private Request getModifyRequest(Request request) {
		// 获取当前进程中的XID
		String xid = RootContext.getXID();
		
		if (StringUtils.isEmpty(xid)) {
			return request;
		}
 
		Map<String, Collection<String>> headers = new HashMap<>(MAP_SIZE);
		headers.putAll(request.headers());
		
		// 将本地的XID添加到调用请求参数中
		List<String> seataXid = new ArrayList<>();
		seataXid.add(xid);
		headers.put(RootContext.KEY_XID, seataXid);
 
		return Request.create(request.method(), request.url(), headers, request.body(),
				request.charset());
	}
 
}

处理请求中xid

package io.seata.integration.http;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import io.seata.common.util.StringUtils;
import io.seata.core.context.RootContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
 
 
/**
 * 处理HTTP调用中的XID传递
 *
 * @author wangxb
 */
public class TransactionPropagationInterceptor extends HandlerInterceptorAdapter {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionPropagationInterceptor.class);
 
    /**
     * 前置处理
     * @param request
     * @param response
     * @param handler
     * @return
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 当前进程绑定的XID
        String xid = RootContext.getXID();
        // 远程调用中HTTP Header中的XID
        String rpcXid = request.getHeader(RootContext.KEY_XID);
 
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("xid in RootContext[{}] xid in HttpContext[{}]", xid, rpcXid);
        }
        if (StringUtils.isBlank(xid) && StringUtils.isNotBlank(rpcXid)) {
            // 本地线程没有绑定XID,且上游调用方传递了XID,则将本地线程与XID绑定
            RootContext.bind(rpcXid);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("bind[{}] to RootContext", rpcXid);
            }
        }
 
        return true;
    }
 
    /**
     * 后置处理
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 远程调用结束前,判断是否启用的全局事务,判断依据是 当前线程是否绑定了XID
        // CONTEXT_HOLDER.get(KEY_XID) != null
        if (RootContext.inGlobalTransaction()) {
            // 如果绑定了XID,将此XID和当前线程接触绑定
            XidResource.cleanXid(request.getHeader(RootContext.KEY_XID));
        }
    }
 
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HELLO XF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值