元始:在回答刚刚的问题之前,老大,问你一个问题,客户端和服务端之间如果通过长连接进行通信的话,你会怎么做?
傅青阳:其他人一般用Netty这个框架来实现基于TCP的长连接吧。但我一向是自己手写Socket来实现。
元始:没错,老大功力深厚,可以自己直接基于底层的Socket实现连接、IO传输等。但是Seata这个还是普遍使用了Netty框架来实现的,所以Seata框架内RM、TM与Server端的通信就是基于Netty实现的。
傅青阳:嗯,继续吧。
元始:好的,那我从上一次的全局事务初始化讲起。
全局事务初始化
我接着上一篇提到的开启全局事务的方法beginTransaction
继续跟进代码,看看如何申请事务xid。
@Override
public void begin(int timeout, String name) throws TransactionException {
// 必须是主干事务才能发起获取事务xid的请求
if (role != GlobalTransactionRole.Launcher) {
//......
return;
}
//......
// 真正获取xid
xid = transactionManager.begin(null, null, name, timeout);
status = GlobalStatus.Begin;
RootContext.bind(xid);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Begin new global transaction [{}]", xid);
}
}
这里的代码主要是判断一下是否满足可以获取全局事务xid的条件:主干事务发起、原本没有获取过xid,获取xid成功后会缓存在当前线程缓存RootContext
中。然后继续跟进transactionManager.begin
方法。
/**
* 启动全局事务,注册到server端
*/
@Override
public String begin(String applicationId, String transactionServiceGroup, String name, int timeout)
throws TransactionException {
GlobalBeginRequest request = new GlobalBeginRequest();
request.setTransactionName(name);
request.setTimeout(timeout);
GlobalBeginResponse response = (GlobalBeginResponse) syncCall(request);
if (response.getResultCode() == ResultCode.Failed) {
throw new TmTransactionException(TransactionExceptionCode.BeginFailed, response.getMsg());
}
// 获取到分配的xid
return response.getXid();
}
其实就是发送开启全局事务的请求GlobalBeginRequest
给Seata Server,由Server端统一分配全局事务xid。
关于如何启动Netty客户端,并且连接到Server端的逻辑待会再讲,我们接着看看获取到xid之后,如何跨服务传递?
如何传递事务xid到其他服务?
由于我的那个Demo的服务间调用是基于Dubbo
实现的,所以在这里还需要引入一个Seata提交的Dubbo Filter:AlibabaDubboTransactionPropagationFilter
。我们看下这个Filter
的实现:
@Activate(group = {DubboConstants.PROVIDER, DubboConstants.CONSUMER}, order = 100)
public class AlibabaDubboTransactionPropagationFilter implements Filter {
//......
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
if (!DubboConstants.ALIBABADUBBO) {
return invoker.invoke(invocation);
}
// 传递事务id
String xid = RootContext.getXID();
BranchType branchType = RootContext.getBranchType();
String rpcXid = getRpcXid();
String rpcBranchType = RpcContext.getContext().getAttachment(RootContext.KEY_BRANCH_TYPE);
//......
boolean bind = false;
if (xid != null) {
// 设置隐式参数
RpcContext.getContext().setAttachment(RootContext.KEY_XID, xid);
RpcContext.getContext().setAttachment(RootContext.KEY_BRANCH_TYPE, branchType.name());
} else {
if (rpcXid != null) {
RootContext.bind(rpcXid);
if (StringUtils.equals(BranchType.TCC.name(), rpcBranchType)) {
RootContext.bindBranchType(BranchType.TCC);
}
bind = true;
//......
}
}
try {
return invoker.invoke(invocation);
} finally {
//......
}
}
/**
* get rpc xid
* 从隐式参数获取事务xid
*/
private String getRpcXid() {
String rpcXid = RpcContext.getContext().getAttachment(RootContext.KEY_XID);
if (rpcXid == null) {
rpcXid = RpcContext.getContext().getAttachment(RootContext.KEY_XID.toLowerCase());
}
return rpcXid;
}
}
这里的代码其实都是一目了然,直接通过Dubbo提供的隐式参数实现的,调用前将从当前线程缓存取出事务xid写入隐式参数,被调用者就可以从隐式参数获得事务xid,然后设置到当前线程缓存RootContext
中。
如何连接到Seata server?
傅青阳:嗯,那前面提到获取事务xid是需要连接到Server端,由Server端分配的,那么如何业务服务如何连接到Seata Server端呢?
元始:这里我们就需要回去看看之前提到的事务扫描组件GlobalTransactionScanner
,看看里面做了什么。
public class GlobalTransactionScanner extends AbstractAutoProxyCreator
implements ConfigurationChangeListener, InitializingBean, ApplicationContextAware, DisposableBean {
由于GlobalTransactionScanner
实现了InitializingBean
这个接口,所以在Spring容器初始化这个bean的时候就会先执行afterPropertiesSet
这个方法,我们跟进看看:
/**
* 启动时执行的入口
*/
@Override
public void afterPropertiesSet() {
//......
if (initialized.compareAndSet(false, true)) {
// 初始化客户端
initClient();
}
}
private void initClient() {
//......
//init TM 初始化TM事务管理器
TMClient.init(applicationId, txServiceGroup, accessKey, secretKey);
//......
//init RM 初始化RM资源管理器
RMClient.init(applicationId, txServiceGroup);
//......
registerSpringShutdownHook();
}
上面的代码可以清楚看到,就是在这里初始化了事务管理器TM和资源管理器RM。
我们先简单看看RM是如何初始化的,因为TM涉及到了对象池的应用,我们后面再看。
public class RMClient {
public static void init(String applicationId, String transactionServiceGroup) {
RmNettyRemotingClient rmNettyRemotingClient = RmNettyRemotingClient.getInstance(applicationId, transactionServiceGroup);
rmNettyRemotingClient.setResourceManager(DefaultResourceManager.get());
rmNettyRemotingClient.setTransactionMessageHandler(DefaultRMHandler.get());
rmNettyRemotingClient.init();
}
}
@Override
public void init() {
// registry processor
registerProcessor();
if (initialized.compareAndSet(false, true)) {
// 继续调用父类初始化
super.init();
if (resourceManager != null
&& !resourceManager.getManagedResources().isEmpty()
&& StringUtils.isNotBlank(transactionServiceGroup)) {
getClientChannelManager().reconnect(transactionServiceGroup);
}
}
}
@Override
public void init() {
//......
super.init();
// 初始化连接客户端
clientBootstrap.start();
}
这里的RM初始化就是层层调用了init方法,最后的重点就是在clientBootstrap
这里。
/**
* 初始化netty client
*/
@Override
public void start() {
//......
this.bootstrap.group(this.eventLoopGroupWorker).channel(
nettyClientConfig.getClientChannelClazz())
// 不启用开启Nagle算法,Nagle算法会收集网络小包再一次性发送,不启用则是即时发送
.option(ChannelOption.TCP_NODELAY, true)
// 连接保活
.option(ChannelOption.SO_KEEPALIVE, true)
// 客户端连接超时时间
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis())
// 发送数据缓冲区大小
.option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize())
// 接收数据缓冲区大小
.option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize());
//......
bootstrap.handler(
new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(
// 基于心跳保持会话状态
new IdleStateHandler(nettyClientConfig.getChannelMaxReadIdleSeconds(),
nettyClientConfig.getChannelMaxWriteIdleSeconds(),
nettyClientConfig.getChannelMaxAllIdleSeconds()))
// 解码器
.addLast(new ProtocolV1Decoder())
// 编码器
.addLast(new ProtocolV1Encoder());
if (channelHandlers != null) {
addChannelPipelineLast(ch, channelHandlers);
}
}
});
}
看到这里就明白了,RM其实就是一个Netty Client,通过这个连接到Seata Server(也即是Netty Server)端,使用TCP长连接进行网络通信。
傅青阳:那么关于Netty的客户端和服务端的参数调优、RM和TM的初始化过程,知道吗?
元始:我掐指一算就知道你要问这个问题,早有准备了。