最近github上阿里的分布式框架seata十分的红热,这框框架其实去年我也听说过,稍微碰了一下(稍微接触了一下皮毛的皮毛),也没有很搞懂。最近由于这个框架又开始慢慢的成熟了,打算再重新接触一下这个框架,不过目前这个框架还在完善中,文档有是有但不是很全的那种,不过比去年是好点了。
如果不了解seata的同学,文章末尾附上一些链接,可以先学习一下。我们接下来一起来看看大概怎么实现的。如果有人去实现过seata的分布式事务功能的都应该知道。实现分布式事务功能的最重要的两步:1,开启全局事务扫描器 2,配置seata代理数据源。我也从这两步去了解seata at模式的代码
1,开启全局事务扫描器
打开GlobalTransactionScanner类,因为实现类ApplicationContextAwre接口,所以先看afterPropertiesSet方法
@Override
public void afterPropertiesSet() {
if (disableGlobalTransaction) {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Global transaction is disabled.");
}
return;
}
initClient();
}
然后看一下initClient方法
private void initClient() {
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Initializing Global Transaction Clients ... ");
}
if (StringUtils.isNullOrEmpty(applicationId) || StringUtils.isNullOrEmpty(txServiceGroup)) {
throw new IllegalArgumentException(
"applicationId: " + applicationId + ", txServiceGroup: " + txServiceGroup);
}
//init TM
TMClient.init(applicationId, txServiceGroup);
if (LOGGER.isInfoEnabled()) {
LOGGER.info(
"Transaction Manager Client is initialized. applicationId[" + applicationId + "] txServiceGroup["
+ txServiceGroup + "]");
}
//init RM
RMClient.init(applicationId, txServiceGroup);
if (LOGGER.isInfoEnabled()) {
LOGGER.info(
"Resource Manager is initialized. applicationId[" + applicationId + "] txServiceGroup[" + txServiceGroup
+ "]");
}
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Global Transaction Clients are initialized. ");
}
registerSpringShutdownHook();
}
这个方法主要就是初始化rm,tm(at模式中的角色)。对于一个服务既可以是TM角色也可以是RM角色,至于什么时候是 TM 或者 RM 则要看在一次全局事务中 @GlobalTransactional
注解标注在哪。 Client 创建的结果是与 TC 的一个 Netty 连接,rm,tm都会通过netty发送消息给tc。
GlobalTransactionScanner类还有一个方法比较重要,那就是
wrapIfNecessary方法,这个方法是spring aop的一个核心方法,具体可以去了解一些spring aop,
@Override
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (disableGlobalTransaction) {
return bean;
}
try {
synchronized (PROXYED_SET) {
if (PROXYED_SET.contains(beanName)) {
return bean;
}
interceptor = null;
//check TCC proxy
if (TCCBeanParserUtils.isTccAutoProxy(bean, beanName, applicationContext)) {
//TCC interceptor, proxy bean of sofa:reference/dubbo:reference, and LocalTCC
interceptor = new TccActionInterceptor(TCCBeanParserUtils.getRemotingDesc(beanName));
} else {
Class> serviceInterface = SpringProxyUtils.findTargetClass(bean);
Class>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);
if (!existsAnnotation(new Class[] {serviceInterface})
&& !existsAnnotation(interfacesIfJdk)) {
return bean;
}
if (interceptor == null) {
interceptor = new GlobalTransactionalInterceptor(failureHandlerHook);
}
}
LOGGER.info(
"Bean[" + bean.getClass().getName() + "] with name [" + beanName + "] would use interceptor ["
+ interceptor.getClass().getName() + "]");
if (!AopUtils.isAopProxy(bean)) {
bean = super.wrapIfNecessary(bean, beanName, cacheKey);
} else {
AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);
Advisor[] advisor = buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null, null, null));
for (Advisor avr : advisor) {
advised.addAdvisor(0, avr);
}
}
PROXYED_SET.add(beanName);
return bean;
}
} catch (Exception exx) {
throw new RuntimeException(exx);
}
}
这个方法主要的目的:
1,如果是tcc模式,就增加tcc相应的方法拦截器,否则就加入全局事务的方法拦截器
2,执行父类的wrapIfNecessary方法,获取所有的增强器,根据接口名称,以及pointcut匹配规则生成代理类,替换目标类
接下来看一下GlobalTransactionalInterceptor的invoke方法
@Override
public Object invoke(final MethodInvocation methodInvocation) throws Throwable {
// 1
Class> targetClass = (methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null);
Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
final Method method = BridgeMethodResolver.findBridgedMethod(specificMethod);
// 2
final GlobalTransactional globalTransactionalAnnotation = getAnnotation(method, GlobalTransactional.class);
final GlobalLock globalLockAnnotation = getAnnotation(method, GlobalLock.class);
if (globalTransactionalAnnotation != null) {
return handleGlobalTransaction(methodInvocation, globalTransactionalAnnotation);
} else if (globalLockAnnotation != null) {
return handleGlobalLock(methodInvocation);
} else {
return methodInvocation.proceed();
}
}
方法主要操作:
1,找到原目标具体方法
2,获取目标方法上的注解,进入相对应的逻辑处理方法
handleGlobalTransaction方法主要使用了transactionalTemplate调用了其execute方法,这里我们看一下该方法
public Object execute(TransactionalExecutor business) throws Throwable {
// 1. get or create a transaction
GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
// 1.1 get transactionInfo
TransactionInfo txInfo = business.getTransactionInfo();
if (txInfo == null) {
throw new ShouldNeverHappenException("transactionInfo does not exist");
}
try {
// 2. begin transaction
beginTransaction(txInfo, tx);
Object rs = null;
try {
// Do Your Business
rs = business.execute();
} catch (Throwable ex) {
// 3.the needed business exception to rollback.
completeTransactionAfterThrowing(txInfo,tx,ex);
throw ex;
}
// 4. everything is fine, commit.
commitTransaction(tx);
return rs;
} finally {
//5. clear
triggerAfterCompletion();
cleanUp();
}
}
这个方法好像是整个at模式 全局分布式事务的流程。
1,开始分布式事务
这里最终会调用DefaultGlobalTransaction的begin方法
public void begin(int timeout, String name) throws TransactionException {
// a
if (role != GlobalTransactionRole.Launcher) {
check();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Ignore Begin(): just involved in global transaction [" + xid + "]");
}
return;
}
if (xid != null) {
throw new IllegalStateException();
}
if (RootContext.getXID() != null) {
throw new IllegalStateException();
}
// b 利用TmRpcClient 之前建立好的channel给tc发送请求,获取全局事务id
xid = transactionManager.begin(null, null, name, timeout);
status = GlobalStatus.Begin;
// c
RootContext.bind(xid);
if (LOGGER.isInfoEnabled()) {
LOGGER.info("Begin new global transaction [" + xid + "]");
}
}
a,判断是不是分布式事务的发起者,GlobalTransactionRole.Launcher就是事务发起者角色,如果不是就直接return
b,这个方法主要利用TmRpcClient 之前建立好的channel给tc发送请求,获取全局事务id
c,将获取到到全局事务id放到seata上下文中
2,处理方法本身的业务逻辑
3,处理业务逻辑的时候报错了,则进行事务回滚,并抛出异常
跟开启事务一样最终会调到DefaultGlobalTransaction,只是方法这回变成了rollback
@Override
public void rollback() throws TransactionException {
if (role == GlobalTransactionRole.Participant) {
// Participant has no responsibility of committing
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Ignore Rollback(): just involved in global transaction [" + xid + "]");
}
return;
}
if (xid == null) {
throw new IllegalStateException();
}
status = transactionManager.rollback(xid);
if (RootContext.getXID() != null) {
if (xid.equals(RootContext.getXID())) {
RootContext.unbind();
}
}
if (LOGGER.isInfoEnabled()) {
LOGGER.info("[" + xid + "] rollback status:" + status);
}
}
如果角色是参与者就直接return,参与者没有责任去决定整体事务的状态;如果是发起者,则发送消息去tc回滚。tm的大致逻辑就是这样了样了
2,配置seata代理数据源
要实现seata at模式的分布式事务的功能还有一件事情就是要配置如下代码
@Configuration
public class SeataDataSourceProxyConfig {
@Bean(value= "druidDataSource",initMethod = "init",destroyMethod = "close")
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
@Primary
@Bean("jdbcTemplate")
public JdbcTemplate dataSource(DataSourceProxy dataSourceProxy) {
return new JdbcTemplate(dataSourceProxy);
}
}
上面的代码就是将我们原先的数据源替换成seata自带的代理数据源DataSourceProxy。使用代理数据源的原因是seata rm要在原先的数据操作上增加自己的一些业务处理,以此来达到分布式事务的功能。
DataSourceProxy主要是注册相应的数据库配置,生成代理连接ConnectionProxy,代理处理类。代理之后。seata才能控制任何数据库操作类。
先看看AbstractDMLBaseExecutor类
@Override
public T doExecute(Object... args) throws Throwable {
AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
if (connectionProxy.getAutoCommit()) {
return executeAutoCommitTrue(args);
} else {
return executeAutoCommitFalse(args);
}
}
根据connection是否自动提交,进入相应的方法,这里我们先看一下executeAutoCommitFalse
protected T executeAutoCommitFalse(Object[] args) throws Throwable {
TableRecords beforeImage = beforeImage();
T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
TableRecords afterImage = afterImage(beforeImage);
prepareUndoLog(beforeImage, afterImage);
return result;
}
这个方法主要是对执行sql操作之前和之后生成对应对数据快照,防止到时候rollback时可以数据回滚,这是比较关键的点
执行sql最终回到ExecuteTemplate,有点类似与上面tm中的transactionalTemplate,我们来看一下其
execute方法
public static T execute(SQLRecognizer sqlRecognizer,
StatementProxy statementProxy,
StatementCallback statementCallback,
Object... args) throws SQLException {
// 如果不是分布式事务操作就进行原有的数据库操作
if (!RootContext.inGlobalTransaction() && !RootContext.requireGlobalLock()) {
// Just work as original statement
return statementCallback.execute(statementProxy.getTargetStatement(), args);
}
if (sqlRecognizer == null) {
sqlRecognizer = SQLVisitorFactory.get(
statementProxy.getTargetSQL(),
statementProxy.getConnectionProxy().getDbType());
}
Executor executor = null;
//针对不同的数据库操作,进入相应的Executor
if (sqlRecognizer == null) {
executor = new PlainExecutor(statementProxy, statementCallback);
} else {
switch (sqlRecognizer.getSQLType()) {
case INSERT:
executor = new InsertExecutor(statementProxy, statementCallback, sqlRecognizer);
break;
case UPDATE:
executor = new UpdateExecutor(statementProxy, statementCallback, sqlRecognizer);
break;
case DELETE:
executor = new DeleteExecutor(statementProxy, statementCallback, sqlRecognizer);
break;
case SELECT_FOR_UPDATE:
executor = new SelectForUpdateExecutor(statementProxy, statementCallback, sqlRecognizer);
break;
default:
executor = new PlainExecutor(statementProxy, statementCallback);
break;
}
}
T rs = null;
try {
rs = executor.execute(args);
} catch (Throwable ex) {
if (!(ex instanceof SQLException)) {
// Turn other exception into SQLException
ex = new SQLException(ex);
}
throw (SQLException)ex;
}
return rs;
}
每个executor类都有相应的beforeImage,afterImage方法,最终都是执行execute方法,用代理的数据库连接去执行数据库操作。我们这里看到使用代理数据库,我们这边只要做的核心步骤是在数据库操作前后进行了数据快照保存,以及生成了undo-log表数据。
到此seata at模式的client大概就是做了这么一些事情
自蚂蚁金服技术专家、分布式事务 Seata 发起者之一张森(花名:绍辉)在 GIAC 全球互联网架构大会的分享(这文章很好)https://www.toutiao.com/i6724525682224792077/
seata-sample github项目地址(目前这个项目还是有点乱的,jar混乱严重)
https://github.com/seata/seata-samples
seata github地址:
https://github.com/seata/seata
本人对一些分布式事务实现的实践项目:
https://github.com/zfh1038030621/distributed-transaction-solutio