Apache ShardingSphere整合Atomikos源碼解析

Shardingsphere整合Atomikos對XA分佈式事務的支持

Apache ShardingSphere 是一套開源的分佈式數據庫中間件解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規劃中)這 3 款相互獨立,卻又能夠混合部署配合使用的產品組成。 它們均提供標準化的數據分片、分佈式事務和數據庫治理功能,可適用於如 Java 同構、異構語言、雲原生等各種多樣化的應用場景。

ShardingSphere 已於2020年4月16日成爲 Apache 軟件基金會的頂級項目。

咋們話不多,接上篇,我們直接進入正題。

Atomikos簡單介紹

Atomikos(https://www.atomikos.com/),其實是一家公司的名字,提供了基於JTA規範的XA分佈式事務TM的實現。其旗下最著名的產品就是事務管理器。產品分兩個版本:

  • TransactionEssentials:開源的免費產品;

  • ExtremeTransactions:上商業版,需要收費。

這兩個產品的關係如下圖所示:

ExtremeTransactions在TransactionEssentials的基礎上額外提供了以下功能(重要的):

  • 支持TCC:這是一種柔性事務

  • 支持通過RMI、IIOP、SOAP這些遠程過程調用技術,進行事務傳播。

  • 事務日誌雲存儲,雲端對事務進行恢復,並且提供了完善的管理後臺。

org.apache.shardingsphere.transaction.xa.XAShardingTransactionManager詳解

我們簡單的來回顧下org.apache.shardingsphere.transaction.spi.ShardingTransactionManager

public interface ShardingTransactionManager extends AutoCloseable {

    /**
     * Initialize sharding transaction manager.
     *
     * @param databaseType database type
     * @param resourceDataSources resource data sources
     */
    void init(DatabaseType databaseType, Collection<ResourceDataSource> resourceDataSources);

    /**
     * Get transaction type.
     *
     * @return transaction type
     */
    TransactionType getTransactionType();

    /**
     * Judge is in transaction or not.
     *
     * @return in transaction or not
     */
    boolean isInTransaction();

    /**
     * Get transactional connection.
     *
     * @param dataSourceName data source name
     * @return connection
     * @throws SQLException SQL exception
     */
    Connection getConnection(String dataSourceName) throws SQLException;

    /**
     * Begin transaction.
     */
    void begin();

    /**
     * Commit transaction.
     */
    void commit();

    /**
     * Rollback transaction.
     */
    void rollback();
}

我們重點縣關注init方法,從它的命名,你就應該能夠看出來,這是整個框架的初始化方法,讓我們來看看它是如何進行初始化的。


 private final Map<String, XATransactionDataSource> cachedDataSources = new HashMap<>();

 private final XATransactionManager xaTransactionManager = XATransactionManagerLoader.getInstance().getTransactionManager();

    @Override
    public void init(final DatabaseType databaseType, final Collection<ResourceDataSource> resourceDataSources) {
        for (ResourceDataSource each : resourceDataSources) {
            cachedDataSources.put(each.getOriginalName(), new XATransactionDataSource(databaseType, each.getUniqueResourceName(), each.getDataSource(), xaTransactionManager));
        }
        xaTransactionManager.init();
    }
  • 首先SPI的方式加載XATransactionManager的具體實現類,這裏返回的就是org.apache.shardingsphere.transaction.xa.atomikos.manager.AtomikosTransactionManager

  • 我們在關注下 new XATransactionDataSource() , 進入 org.apache.shardingsphere.transaction.xa.jta.datasource。XATransactionDataSource類的構造方法。

public XATransactionDataSource(final DatabaseType databaseType, final String resourceName, final DataSource dataSource, final XATransactionManager xaTransactionManager) {
        this.databaseType = databaseType;
        this.resourceName = resourceName;
        this.dataSource = dataSource;
        if (!CONTAINER_DATASOURCE_NAMES.contains(dataSource.getClass().getSimpleName())) {
            // 重點關注 1 ,返回了xaDatasource
            xaDataSource = XADataSourceFactory.build(databaseType, dataSource);
            this.xaTransactionManager = xaTransactionManager;
            // 重點關注2 註冊資源
            xaTransactionManager.registerRecoveryResource(resourceName, xaDataSource);
        }
    }
  • 我們重點來關注 XADataSourceFactory.build(databaseType, dataSource),從名字我們就可以看出,這應該是返回JTA規範裏面的XADataSource,在ShardingSphere裏面很多的功能,可以從代碼風格的命名上就能猜出來,這就是優雅代碼(吹一波)。不多逼逼,我們進入該方法。
public final class XADataSourceFactory {

    public static XADataSource build(final DatabaseType databaseType, final DataSource dataSource) {
        return new DataSourceSwapper(XADataSourceDefinitionFactory.getXADataSourceDefinition(databaseType)).swap(dataSource);
    }
}
  • 首先又是一個SPI定義的 XADataSourceDefinitionFactory,它根據不同的數據庫類型,來加載不同的方言。然後我們進入 swap方法。
 public XADataSource swap(final DataSource dataSource) {
        XADataSource result = createXADataSource();
        setProperties(result, getDatabaseAccessConfiguration(dataSource));
        return result;
    }
  • 很簡明,第一步創建,XADataSource,第二步給它設置屬性(包含數據的連接,用戶名密碼等),然後返回。

  • 返回 XATransactionDataSource 類,關注 xaTransactionManager.registerRecoveryResource(resourceName, xaDataSource); 從名字可以看出,這是註冊事務恢復資源。這個我們在事務恢復的時候詳解。

  • 返回 XAShardingTransactionManager.init() ,我們重點來關注: xaTransactionManager.init();,最後進入AtomikosTransactionManager.init()。流程圖如下:

代碼:

public final class AtomikosTransactionManager implements XATransactionManager {

    private final UserTransactionManager transactionManager = new UserTransactionManager();

    private final UserTransactionService userTransactionService = new UserTransactionServiceImp();

    @Override
    public void init() {
        userTransactionService.init();
    }

}
  • 進入UserTransactionServiceImp.init()
private void initialize() {
       //添加恢復資源 不用關心
		for (RecoverableResource resource : resources_) {
			Configuration.addResource ( resource );
		}
		for (LogAdministrator logAdministrator : logAdministrators_) {
			Configuration.addLogAdministrator ( logAdministrator );
		}
         //註冊插件 不用關心
        for (TransactionServicePlugin nxt : tsListeners_) {
        	Configuration.registerTransactionServicePlugin ( nxt );
		}
        //獲取配置屬性 重點關心
        ConfigProperties configProps = Configuration.getConfigProperties();
        configProps.applyUserSpecificProperties(properties_);
        //進行初始化
        Configuration.init();
	}
  • 我們重點關注,獲取配置屬性。最後進入com.atomikos.icatch.provider.imp.AssemblerImp.initializeProperties()方法。
	@Override
	public ConfigProperties initializeProperties() {
		 //讀取classpath下的默認配置transactions-defaults.properties
        Properties defaults = new Properties();
        loadPropertiesFromClasspath(defaults, DEFAULT_PROPERTIES_FILE_NAME);
        //讀取classpath下,transactions.properties配置,覆蓋transactions-defaults.properties中相同key的值
        Properties transactionsProperties = new Properties(defaults);
        loadPropertiesFromClasspath(transactionsProperties, TRANSACTIONS_PROPERTIES_FILE_NAME);
        //讀取classpath下,jta.properties,覆蓋transactions-defaults.properties、transactions.properties中相同key的值
        Properties jtaProperties = new Properties(transactionsProperties);
        loadPropertiesFromClasspath(jtaProperties, JTA_PROPERTIES_FILE_NAME);

        //讀取通過java -Dcom.atomikos.icatch.file方式指定的自定義配置文件路徑,覆蓋之前的同名配置
        Properties customProperties = new Properties(jtaProperties);
        loadPropertiesFromCustomFilePath(customProperties);
        //最終構造一個ConfigProperties對象,來表示實際要使用的配置
        Properties finalProperties = new Properties(customProperties);
        return new ConfigProperties(finalProperties);
	}
  • 接下來重點關注, Configuration.init(), 進行初始化。
ublic static synchronized boolean init() {
		boolean startupInitiated = false;
		if (service_ == null) {
			startupInitiated = true;
           //SPI方式加載插件註冊,無需過多關心
			addAllTransactionServicePluginServicesFromClasspath();
			ConfigProperties configProperties = getConfigProperties();
          //調用插件的beforeInit方法進行初始化話,無需過多關心
			notifyBeforeInit(configProperties);
          //進行事務日誌恢復的初始化,很重要,接下來詳解
			assembleSystemComponents(configProperties);
         //進入系統註解的初始化,一般重要
			initializeSystemComponents(configProperties);
			notifyAfterInit();
			if (configProperties.getForceShutdownOnVmExit()) {
				addShutdownHook(new ForceShutdownHook());
			}
		}
		return startupInitiated;
	}
  • 我們先來關注 assembleSystemComponents(configProperties); 進入它,進入com.atomikos.icatch.provider.imp.AssemblerImp.assembleTransactionService()方法:
@Override
	public TransactionServiceProvider assembleTransactionService(
			ConfigProperties configProperties) {
		RecoveryLog recoveryLog =null;
       //打印日誌
		logProperties(configProperties.getCompletedProperties());
       //生成唯一名字
		String tmUniqueName = configProperties.getTmUniqueName();

		long maxTimeout = configProperties.getMaxTimeout();
		int maxActives = configProperties.getMaxActives();
		boolean threaded2pc = configProperties.getThreaded2pc();
      //SPI方式加載OltpLog ,這是最重要的擴展地方,如果用戶沒有SPI的方式去擴展那麼就爲null
		OltpLog oltpLog = createOltpLogFromClasspath();
		if (oltpLog == null) {
			LOGGER.logInfo("Using default (local) logging and recovery...");
                        //創建事務日誌存儲資源
			Repository repository = createRepository(configProperties);
			oltpLog = createOltpLog(repository);
			//??? Assemble recoveryLog
			recoveryLog = createRecoveryLog(repository);
		}
		StateRecoveryManagerImp	recoveryManager = new StateRecoveryManagerImp();
		recoveryManager.setOltpLog(oltpLog);
           //生成唯一id生成器,以後生成XID會用的到
		UniqueIdMgr idMgr = new UniqueIdMgr ( tmUniqueName );
		int overflow = idMgr.getMaxIdLengthInBytes() - MAX_TID_LENGTH;
		if ( overflow > 0 ) {
			// see case 73086
			String msg = "Value too long : " + tmUniqueName;
			LOGGER.logFatal ( msg );
			throw new SysException(msg);
		}
		return new TransactionServiceImp(tmUniqueName, recoveryManager, idMgr, maxTimeout, maxActives, !threaded2pc, recoveryLog);
	}
  • 我們重點來分析createOltpLogFromClasspath(), 採用SPI的加載方式來獲取,默認這裏會返回 null, 什麼意思呢? 就是當沒有擴展的時候,atomikos,會創建框架自定義的資源,來存儲事務日誌。
private OltpLog createOltpLogFromClasspath() {
		OltpLog ret = null;
		ServiceLoader<OltpLogFactory> loader = ServiceLoader.load(OltpLogFactory.class,Configuration.class.getClassLoader());
		int i = 0;
        for (OltpLogFactory l : loader ) {
			ret = l.createOltpLog();
			i++;
		}
        if (i > 1) {
			String msg = "More than one OltpLogFactory found in classpath - error in configuration!";
			LOGGER.logFatal(msg);
			throw new SysException(msg);
        }
        return ret;
	}
  • 我們跟着進入 Repository repository = createRepository(configProperties);
	private CachedRepository createCoordinatorLogEntryRepository(
			ConfigProperties configProperties) throws LogException {
        //創建內存資源存儲
		InMemoryRepository inMemoryCoordinatorLogEntryRepository = new InMemoryRepository();
       //進行初始化
		inMemoryCoordinatorLogEntryRepository.init();
       //創建使用文件存儲資源作爲backup
		FileSystemRepository backupCoordinatorLogEntryRepository = new FileSystemRepository();
       //進行初始化
		backupCoordinatorLogEntryRepository.init();
      //內存與file資源進行合併
		CachedRepository repository = new CachedRepository(inMemoryCoordinatorLogEntryRepository, backupCoordinatorLogEntryRepository);
		repository.init();
		return repository;
	}
  • 這裏就會創建出 CachedRepository,裏面包含了 InMemoryRepositoryFileSystemRepository

  • 回到主線 com.atomikos.icatch.config.Configuration.init(), 最後來分析下notifyAfterInit();

	private static void notifyAfterInit() {
         //進行插件的初始化
		for (TransactionServicePlugin p : tsListenersList_) {
			p.afterInit();
		}
		for (LogAdministrator a : logAdministrators_) {
			a.registerLogControl(service_.getLogControl());
		}
         //設置事務恢復服務,進行事務的恢復
		for (RecoverableResource r : resourceList_ ) {
			r.setRecoveryService(recoveryService_);
		}

	}
  • 插件的初始化會進入com.atomikos.icatch.jta.JtaTransactionServicePlugin.afterInit()
	public void afterInit() {
		TransactionManagerImp.installTransactionManager(Configuration.getCompositeTransactionManager(), autoRegisterResources);
          //如果我們自定義擴展了 OltpLog ,這裏就會返回null,如果是null,那麼XaResourceRecoveryManager就是null
		RecoveryLog recoveryLog = Configuration.getRecoveryLog();
		long maxTimeout = Configuration.getConfigProperties().getMaxTimeout();
		if (recoveryLog != null) {
			XaResourceRecoveryManager.installXaResourceRecoveryManager(new DefaultXaRecoveryLog(recoveryLog, maxTimeout),Configuration.getConfigProperties().getTmUniqueName());
		}

	}
  • 重點注意 RecoveryLog recoveryLog = Configuration.getRecoveryLog(); ,如果用戶採用SPI的方式,擴展了com.atomikos.recovery.OltpLog這裏就會返回 null。 如果是null,則不會對 XaResourceRecoveryManager 進行初始化。

  • 回到 notifyAfterInit(), 我們來分析 setRecoveryService

public void setRecoveryService ( RecoveryService recoveryService )
            throws ResourceException
    {

        if ( recoveryService != null ) {
            if ( LOGGER.isTraceEnabled() ) LOGGER.logTrace ( "Installing recovery service on resource "
                    + getName () );
            this.branchIdentifier=recoveryService.getName();
            recover();
        }
    }
  • 我們進入 recover() 方法:
 public void recover() {
    	XaResourceRecoveryManager xaResourceRecoveryManager = XaResourceRecoveryManager.getInstance();
        //null for LogCloud recovery
    	if (xaResourceRecoveryManager != null) {
    		try {
				xaResourceRecoveryManager.recover(getXAResource());
			} catch (Exception e) {
				refreshXAResource(); //cf case 156968
			}

    	}
    }
  • 看到最關鍵的註釋了嗎,如果用戶採用SPI的方式,擴展了com.atomikos.recovery.OltpLog,那麼XaResourceRecoveryManager 爲null,則就會進行雲端恢復,反之則進行事務恢復。 事務恢復很複雜,我們會單獨來講。

到這裏atomikos的基本的初始化已經完成。

atomikos事務begin流程

我們知道,本地的事務,都會有一個 trainsaction.begin, 對應XA分佈式事務來說也不另外,我們再把思路切換回XAShardingTransactionManager.begin(), 會調用com.atomikos.icatch.jta.TransactionManagerImp.begin()。流程圖如下:

代碼:

  public void begin ( int timeout ) throws NotSupportedException,
            SystemException
    {
        CompositeTransaction ct = null;
        ResumePreviousTransactionSubTxAwareParticipant resumeParticipant = null;

        ct = compositeTransactionManager.getCompositeTransaction();
        if ( ct != null && ct.getProperty (  JTA_PROPERTY_NAME ) == null ) {
            LOGGER.logWarning ( "JTA: temporarily suspending incompatible transaction: " + ct.getTid() +
                    " (will be resumed after JTA transaction ends)" );
            ct = compositeTransactionManager.suspend();
            resumeParticipant = new ResumePreviousTransactionSubTxAwareParticipant ( ct );
        }

        try {
      //創建事務補償點
            ct = compositeTransactionManager.createCompositeTransaction ( ( ( long ) timeout ) * 1000 );
            if ( resumeParticipant != null ) ct.addSubTxAwareParticipant ( resumeParticipant );
            if ( ct.isRoot () && getDefaultSerial () )
                ct.setSerial ();
            ct.setProperty ( JTA_PROPERTY_NAME , "true" );
        } catch ( SysException se ) {
        	String msg = "Error in begin()";
        	LOGGER.logError( msg , se );
            throw new ExtendedSystemException ( msg , se );
        }
        recreateCompositeTransactionAsJtaTransaction(ct);
    }
  • 這裏我們主要關注 compositeTransactionManager.createCompositeTransaction(),
public CompositeTransaction createCompositeTransaction ( long timeout ) throws SysException
    {
        CompositeTransaction ct = null , ret = null;

        ct = getCurrentTx ();
        if ( ct == null ) {
            ret = getTransactionService().createCompositeTransaction ( timeout );
            if(LOGGER.isDebugEnabled()){
            	LOGGER.logDebug("createCompositeTransaction ( " + timeout + " ): "
                    + "created new ROOT transaction with id " + ret.getTid ());
            }
        } else {
        	 if(LOGGER.isDebugEnabled()) LOGGER.logDebug("createCompositeTransaction ( " + timeout + " )");
            ret = ct.createSubTransaction ();

        }

        Thread thread = Thread.currentThread ();
        setThreadMappings ( ret, thread );

        return ret;
    }
  • 創建了事務補償點,然後把他放到了用當前線程作爲key的Map當中,這裏思考,爲啥它不用 threadLocal

到這裏atomikos的事務begin流程已經完成。 大家可能有些疑惑,begin好像什麼都沒有做,XA start 也沒調用? 別慌,下一節繼續來講。

XATransactionDataSource getConnection() 流程

我們都知道想要執行SQL語句,必須要獲取到數據庫的connection。讓我們再回到 XAShardingTransactionManager.getConnection() 最後會調用到org.apache.shardingsphere.transaction.xa.jta.datasourceXATransactionDataSource.getConnection()。流程圖如下:

代碼 :

 public Connection getConnection() throws SQLException, SystemException, RollbackException {
      //先檢查是否已經有存在的connection,這一步很關心,也是XA的關鍵,因爲XA事務,必須在同一個connection
        if (CONTAINER_DATASOURCE_NAMES.contains(dataSource.getClass().getSimpleName())) {
            return dataSource.getConnection();
        }
      //獲取數據庫連接
        Connection result = dataSource.getConnection();
      //轉成XAConnection,其實是同一個連接
        XAConnection xaConnection = XAConnectionFactory.createXAConnection(databaseType, xaDataSource, result);
      //獲取JTA事務定義接口
        Transaction transaction = xaTransactionManager.getTransactionManager().getTransaction();
        if (!enlistedTransactions.get().contains(transaction)) {
      //進行資源註冊
            transaction.enlistResource(new SingleXAResource(resourceName, xaConnection.getXAResource()));
            transaction.registerSynchronization(new Synchronization() {
                @Override
                public void beforeCompletion() {
                    enlistedTransactions.get().remove(transaction);
                }

                @Override
                public void afterCompletion(final int status) {
                    enlistedTransactions.get().clear();
                }
            });
            enlistedTransactions.get().add(transaction);
        }
        return result;
    }
  • 首先第一步很關心,尤其是對shardingsphere來說,因爲在一個事務裏面,會有多個SQL語句,打到相同的數據庫,所以對相同的數據庫,必須獲取同一個XAConnection,這樣才能進行XA事務的提交與回滾。

  • 我們接下來關心 transaction.enlistResource(new SingleXAResource(resourceName, xaConnection.getXAResource()));, 會進入com.atomikos.icatch.jta.TransactionImp.enlistResource(), 代碼太長,截取一部分。

try {
				restx = (XAResourceTransaction) res
						.getResourceTransaction(this.compositeTransaction);

				// next, we MUST set the xa resource again,
				// because ONLY the instance we got as argument
				// is available for use now !
				// older instances (set in restx from previous sibling)
				// have connections that may be in reuse already
				// ->old xares not valid except for 2pc operations

				restx.setXAResource(xares);
				restx.resume();
			} catch (ResourceException re) {
				throw new ExtendedSystemException(
						"Unexpected error during enlist", re);
			} catch (RuntimeException e) {
				throw e;
			}

			addXAResourceTransaction(restx, xares);
  • 我們直接看 restx.resume();
public synchronized void resume() throws ResourceException {
		int flag = 0;
		String logFlag = "";
		if (this.state.equals(TxState.LOCALLY_DONE)) {// reused instance
			flag = XAResource.TMJOIN;
			logFlag = "XAResource.TMJOIN";
		} else if (!this.knownInResource) {// new instance
			flag = XAResource.TMNOFLAGS;
			logFlag = "XAResource.TMNOFLAGS";
		} else
			throw new IllegalStateException("Wrong state for resume: "
					+ this.state);

		try {
			if (LOGGER.isDebugEnabled()) {
				LOGGER.logDebug("XAResource.start ( " + this.xidToHexString
						+ " , " + logFlag + " ) on resource "
						+ this.resourcename
						+ " represented by XAResource instance "
						+ this.xaresource);
			}
			this.xaresource.start(this.xid, flag);

		} catch (XAException xaerr) {
			String msg = interpretErrorCode(this.resourcename, "resume",
					this.xid, xaerr.errorCode);
			LOGGER.logWarning(msg, xaerr);
			throw new ResourceException(msg, xaerr);
		}
		setState(TxState.ACTIVE);
		this.knownInResource = true;
	}
  • 哦多尅,看見了嗎,各位,看見了 this.xaresource.start(this.xid, flag); 了嗎????,我們進去,假設我們使用的Mysql數據庫:
 public void start(Xid xid, int flags) throws XAException {
        StringBuilder commandBuf = new StringBuilder(300);
        commandBuf.append("XA START ");
        appendXid(commandBuf, xid);
        switch(flags) {
        case 0:
            break;
        case 2097152:
            commandBuf.append(" JOIN");
            break;
        case 134217728:
            commandBuf.append(" RESUME");
            break;
        default:
            throw new XAException(-5);
        }

        this.dispatchCommand(commandBuf.toString());
        this.underlyingConnection.setInGlobalTx(true);
    }
  • 組裝XA start Xid SQL語句,進行執行。

到這裏,我們總結下,在獲取數據庫連接的時候,我們執行了XA協議接口中的 XA start xid

atomikos事務commit流程

好了,上面我們已經開啓了事務,現在我們來分析下事務commit流程,我們再把視角切換回XAShardingTransactionManager.commit(),最後我們會進入com.atomikos.icatch.imp.CompositeTransactionImp.commit() 方法。流程圖如下:

代碼:

 public void commit () throws HeurRollbackException, HeurMixedException,
            HeurHazardException, SysException, SecurityException,
            RollbackException
    {
       //首先更新下事務日誌的狀態
        doCommit ();
        setSiblingInfoForIncoming1pcRequestFromRemoteClient();

        if ( isRoot () ) {
         //真正的commit操作
          coordinator.terminate ( true );
        }
    }
  • 我們關注 coordinator.terminate ( true );
 protected void terminate ( boolean commit ) throws HeurRollbackException,
            HeurMixedException, SysException, java.lang.SecurityException,
            HeurCommitException, HeurHazardException, RollbackException,
            IllegalStateException

    {
    	synchronized ( fsm_ ) {
    		if ( commit ) {
                     //判斷有幾個參與者,如果只有一個,直接提交
    			if ( participants_.size () <= 1 ) {
    				commit ( true );
    			} else {
                                //否則,走XA 2階段提交流程,先prepare, 再提交
    				int prepareResult = prepare ();
    				// make sure to only do commit if NOT read only
    				if ( prepareResult != Participant.READ_ONLY )
    					commit ( false );
    			}
    		} else {
    			rollback ();
    		}
    	}
    }
  • 首先會判斷參與者的個數,這裏我們可以理解爲MySQL的database數量,如果只有一個,退化成一階段,直接提交。 如果有多個,則走標準的XA二階段提交流程。

  • 我們來看 prepare (); 流程,最後會走到com.atomikos.icatch.imp.PrepareMessage.send() ---> com.atomikos.datasource.xa.XAResourceTransaction.prepare()

int ret = 0;
		terminateInResource();

		if (TxState.ACTIVE == this.state) {
			// tolerate non-delisting apps/servers
			suspend();
		}

		// duplicate prepares can happen for siblings in serial subtxs!!!
		// in that case, the second prepare just returns READONLY
		if (this.state == TxState.IN_DOUBT)
			return Participant.READ_ONLY;
		else if (!(this.state == TxState.LOCALLY_DONE))
			throw new SysException("Wrong state for prepare: " + this.state);
		try {
			// refresh xaresource for MQSeries: seems to close XAResource after
			// suspend???
			testOrRefreshXAResourceFor2PC();
			if (LOGGER.isTraceEnabled()) {
				LOGGER.logTrace("About to call prepare on XAResource instance: "
						+ this.xaresource);
			}
			ret = this.xaresource.prepare(this.xid);

		} catch (XAException xaerr) {
			String msg = interpretErrorCode(this.resourcename, "prepare",
					this.xid, xaerr.errorCode);
			if (XAException.XA_RBBASE <= xaerr.errorCode
					&& xaerr.errorCode <= XAException.XA_RBEND) {
				LOGGER.logWarning(msg, xaerr); // see case 84253
				throw new RollbackException(msg);
			} else {
				LOGGER.logError(msg, xaerr);
				throw new SysException(msg, xaerr);
			}
		}
		setState(TxState.IN_DOUBT);
		if (ret == XAResource.XA_RDONLY) {
			if (LOGGER.isDebugEnabled()) {
				LOGGER.logDebug("XAResource.prepare ( " + this.xidToHexString
						+ " ) returning XAResource.XA_RDONLY " + "on resource "
						+ this.resourcename
						+ " represented by XAResource instance "
						+ this.xaresource);
			}
			return Participant.READ_ONLY;
		} else {
			if (LOGGER.isDebugEnabled()) {
				LOGGER.logDebug("XAResource.prepare ( " + this.xidToHexString
						+ " ) returning OK " + "on resource "
						+ this.resourcename
						+ " represented by XAResource instance "
						+ this.xaresource);
			}
			return Participant.READ_ONLY + 1;
		}
  • 終於,我們看到了這麼一句 ret = this.xaresource.prepare(this.xid); 但是等等,我們之前不是說了,XA start xid 以後要先 XA end xid 嗎? 答案就在 suspend(); 裏面。
public synchronized void suspend() throws ResourceException {

		// BugzID: 20545
		// State may be IN_DOUBT or TERMINATED when a connection is closed AFTER
		// commit!
		// In that case, don't call END again, and also don't generate any
		// error!
		// This is required for some hibernate connection release strategies.
		if (this.state.equals(TxState.ACTIVE)) {
			try {
				if (LOGGER.isDebugEnabled()) {
					LOGGER.logDebug("XAResource.end ( " + this.xidToHexString
							+ " , XAResource.TMSUCCESS ) on resource "
							+ this.resourcename
							+ " represented by XAResource instance "
							+ this.xaresource);
				}
                 //執行了 xa end 語句
				this.xaresource.end(this.xid, XAResource.TMSUCCESS);

			} catch (XAException xaerr) {
				String msg = interpretErrorCode(this.resourcename, "end",
						this.xid, xaerr.errorCode);
				if (LOGGER.isTraceEnabled())
					LOGGER.logTrace(msg, xaerr);
				// don't throw: fix for case 102827
			}
			setState(TxState.LOCALLY_DONE);
		}
	}

到了這裏,我們已經執行了 XA start xid -> XA end xid --> XA prepare xid, 接下來就是最後一步 commit

  • 我們再回到 terminate(false) 方法,來看 commit()流程。其實和 prepare流程一樣,最後會走到 com.atomikos.datasource.xa.XAResourceTransaction.commit()。 commit執行完,數據提交
//繁雜代碼過多,就顯示核心的
this.xaresource.commit(this.xid, onePhase);

思考:這裏的參與者提交是在一個循環裏面,一個一個提交的,如果之前的提交了,後面的參與者提交的時候,掛了,就會造成數據的不一致性。

Atomikos rollback() 流程

上面我們已經分析了commit流程,其實rollback流程和commit流程一樣,我們在把目光切換回 org.apache.shardingsphere.transaction.xa.XAShardingTransactionManager.rollback() ,最後會執行到com.atomikos.icatch.imp.CompositeTransactionImp.rollback()

    public void rollback () throws IllegalStateException, SysException
    {
        //清空資源,更新事務日誌狀態等
    	doRollback ();
        if ( isRoot () ) {
            try {
                coordinator.terminate ( false );
            } catch ( Exception e ) {
                throw new SysException ( "Unexpected error in rollback: " + e.getMessage (), e );
            }
        }
    }
  • 重點關注 coordinator.terminate ( false ); ,這個和 commit流程是一樣的,只不過在 commit流程裏面,參數傳的是true。
 protected void terminate ( boolean commit ) throws HeurRollbackException,
            HeurMixedException, SysException, java.lang.SecurityException,
            HeurCommitException, HeurHazardException, RollbackException,
            IllegalStateException

    {
    	synchronized ( fsm_ ) {
    		if ( commit ) {
    			if ( participants_.size () <= 1 ) {
    				commit ( true );
    			} else {
    				int prepareResult = prepare ();
    				// make sure to only do commit if NOT read only
    				if ( prepareResult != Participant.READ_ONLY )
    					commit ( false );
    			}
    		} else {
                 //如果是false,走的是rollback
    			rollback ();
    		}
    	}
    }
  • 我們重點關注 rollback() ,最後會走到com.atomikos.datasource.xa.XAResourceTransaction.rollback()
public synchronized void rollback()
			throws HeurCommitException, HeurMixedException,
			HeurHazardException, SysException {
		terminateInResource();

		if (rollbackShouldDoNothing()) {
			return;
		}
		if (this.state.equals(TxState.TERMINATED)) {
			return;
		}

		if (this.state.equals(TxState.HEUR_MIXED))
			throw new HeurMixedException();
		if (this.state.equals(TxState.HEUR_COMMITTED))
			throw new HeurCommitException();
		if (this.xaresource == null) {
			throw new HeurHazardException("XAResourceTransaction "
					+ getXid() + ": no XAResource to rollback?");
		}

		try {
			if (this.state.equals(TxState.ACTIVE)) { // first suspend xid
				suspend();
			}

			// refresh xaresource for MQSeries: seems to close XAResource after
			// suspend???
			testOrRefreshXAResourceFor2PC();
			if (LOGGER.isDebugEnabled()) {
				LOGGER.logDebug("XAResource.rollback ( " + this.xidToHexString
						+ " ) " + "on resource " + this.resourcename
						+ " represented by XAResource instance "
						+ this.xaresource);
			}
			this.xaresource.rollback(this.xid);

  • 先在supend()方法裏面執行了 XA end xid 語句, 接下來執行 this.xaresource.rollback(this.xid); 進行數據的回滾。

文章到此,已經寫的很長很多了,我們分析了ShardingSphere對於XA方案,提供了一套SPI解決方案,對Atomikos進行了整合,也分析了Atomikos初始化流程,開始事務流程,獲取連接流程,提交事務流程,回滾事務流程。希望對大家理解XA的原理有所幫助。

作者介紹: 肖宇,Apache ShardingSphere Committer,開源hmily分佈式事務框架作者, 開源soul網關作者,熱愛開源,追求寫優雅代碼。目前就職入京東數科,參與ShardingSphere的開源建設,以及分佈式數據庫的研發工作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章