seata
Seata关键字
XID:一个全局事务的唯一标识,由ip:port:sequence组成
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager ™: 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
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)
修改 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服务
微服务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的信息
示例代码
以下是示例代码,通过创建订单接口分别访问产品服务和账户服务进行分配产品和扣减余额后进行下单。
通过@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));
}
}
}