Fescar源碼學習--資源管理者RM(服務提供方)

 之前我們已經在博客《分佈式事務--Fescar》中瞭解學習到Fescar相關的架構,接下來我們分別用幾篇博客分別來介紹一下Fescar的 TM、RM 和 TC之間的交互流程。

TM、RM和TC之間的交互流程圖:

Architecture

簡單角色理解:

TC: Fesacr-server應用

TM:dubbo服務調用方

RM:dubbo服務提供方

一、RM 簡介

1、簡介

       在上一篇博客《Fescar源碼學習--事物管理者TM(服務調用方)》中我們已經介紹了TM角色相關的處理操作,這篇博客我們通過示例和源碼來分析一下在分佈式事務架構中RM角色所承擔的責任。

RM在啓動到處理TM的RPC調用主要做了以下操作:

(1)啓動並向TC(Fescar Server)註冊RM服務

(2)接收TM方的 Dubbo RPC 調用

(3)代理數據庫生成undo日誌,在undo_log表中記錄相關原始數據

(4)返回RPC調用結果

(5)TM根據RM返回的結果向TC發送commit或rollback操作,TC根據XId事物id將commit或rollback通知對應的RM,RM接收TC(Fescar Server)的commit和rollback指令,其中commit是異步清理undo_log表中的數據,rollback操作會根據undo_log表中的數據恢復數據(讀已提交,可能會產生髒數據)。

2、示例

dubbo服務提供者:

public class StorageServiceImpl implements StorageService {

    private static final Logger LOGGER = LoggerFactory.getLogger(StorageService.class);

    private JdbcTemplate jdbcTemplate;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void deduct(String commodityCode, int count) {
        LOGGER.info("Storage Service Begin ... xid: " + RootContext.getXID());
        jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?", new Object[] {count, commodityCode});
        LOGGER.info("Storage Service End ... ");

    }

    public static void main(String[] args) throws Throwable {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"dubbo-storage-service.xml"});
        context.getBean("service");
        JdbcTemplate jdbcTemplate = (JdbcTemplate) context.getBean("jdbcTemplate");
        jdbcTemplate.update("delete from storage_tbl where commodity_code = 'C00321'");
        jdbcTemplate.update("insert into storage_tbl(commodity_code, count) values ('C00321', 100)");
        new ApplicationKeeper(context).keep();
    }
}

xml配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <bean name="storageDataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <property name="url" value="jdbc:mysql://localhost:3306/fescar3"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="initialSize" value="0" />
        <property name="maxActive" value="180" />
        <property name="minIdle" value="0" />
        <property name="maxWait" value="60000" />
        <property name="validationQuery" value="Select 'x' from DUAL" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="testWhileIdle" value="true" />
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <property name="minEvictableIdleTimeMillis" value="25200000" />
        <property name="removeAbandoned" value="true" />
        <property name="removeAbandonedTimeout" value="1800" />
        <property name="logAbandoned" value="true" />
        <property name="filters" value="mergeStat" />
    </bean>

    <bean id="storageDataSourceProxy" class="com.alibaba.fescar.rm.datasource.DataSourceProxy">
        <constructor-arg ref="storageDataSource" />
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="storageDataSourceProxy" />
    </bean>

    <dubbo:application name="dubbo-demo-storage-service"  />
    <dubbo:registry address="multicast://224.5.6.7:1234?unicast=false" />
    <dubbo:protocol name="dubbo" port="20882" />
    <dubbo:service interface="com.alibaba.fescar.tm.dubbo.StorageService" ref="service" timeout="10000"/>

    <bean id="service" class="com.alibaba.fescar.tm.dubbo.impl.StorageServiceImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate"/>
    </bean>

    <bean class="com.alibaba.fescar.spring.annotation.GlobalTransactionScanner">
        <constructor-arg value="dubbo-demo-storage-service"/>
        <constructor-arg value="my_test_tx_group"/>
    </bean>
</beans>

二、執行流程

1、初始化

在工程啓動時會初始化GlobalTransactionScanner類,在initClient方法中會初始化RM相關的操作。

private void initClient() {
		
		//RM服務與TC建立連接
        TMClient.init(applicationId, txServiceGroup);
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info(
                "Transaction Manager Client is initialized. applicationId[" + applicationId + "] txServiceGroup["
                    + txServiceGroup + "]");
        }
        if ((AT_MODE & mode) > 0) {
			//初始化與TC連接,初始化線程AsyncWorker
            RMClientAT.init(applicationId, txServiceGroup);
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info(
                    "Resource Manager for AT Client is initialized. applicationId[" + applicationId
                        + "] txServiceGroup["
                        + txServiceGroup + "]");
            }
        }
        
    }

2、RPC調用

TM(服務消費者)在調用RM(服務提供者)使用dubbo的rpc調用機制,會將全局事務xid傳遞到RM服務中,通過TX_XID鍵獲取值

@Activate(group = { Constants.PROVIDER, Constants.CONSUMER }, order = 100)
public class TransactionPropagationFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionPropagationFilter.class);

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String xid = RootContext.getXID();
        String rpcXid = RpcContext.getContext().getAttachment(RootContext.KEY_XID);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("xid in RootContext[" + xid + "] xid in RpcContext[" + rpcXid + "]");
        }
        boolean bind = false;
        if (xid != null) {
            RpcContext.getContext().setAttachment(RootContext.KEY_XID, xid);
        } else {
            if (rpcXid != null) {
                RootContext.bind(rpcXid);
                bind = true;
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("bind[" + rpcXid + "] to RootContext");
                }
            }
        }
        try {
            return invoker.invoke(invocation);

        } finally {
            if (bind) {
                String unbindXid = RootContext.unbind();
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("unbind[" + unbindXid + "] from RootContext");
                }
                if (!rpcXid.equalsIgnoreCase(unbindXid)) {
                    LOGGER.warn("xid in change during RPC from " + rpcXid + " to " + unbindXid);
                    if (unbindXid != null) {
                        RootContext.bind(unbindXid);
                        LOGGER.warn("bind [" + unbindXid + "] back to RootContext");
                    }
                }
            }
        }
    }
}

3、方法調用

在經過第二步dubbo的Filter攔截獲取TX_XID之後,設置爲本地線程變量,接下來就是執行本地函數。

    @Override
    public void deduct(String commodityCode, int count) {
        LOGGER.info("Storage Service Begin ... xid: " + RootContext.getXID());
        jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?", new Object[] {count, commodityCode});
        LOGGER.info("Storage Service End ... ");

    }
jdbcTemplate在執行SQL操作時最終是交由ConnectionProxy的commit方法處理,在將事務提交到數據庫之前做了一些處理操作。

4、分片事務提交

在ConnectionProxy的commit方法中做了以下操作:

(1)向TC(事務協調器,Fescar Server)註冊分片事務,並獲取分片事務id

(2)根據分片事務id在undo_log表中生成恢復日誌

(3)真正提交數據庫事務

(4)事務如果提交失敗則向TC報告失敗

(5)事務如果提交成功則向TC報告成功

    @Override
    public void commit() throws SQLException {
        if (context.inGlobalTransaction()) {
            try {
                //向TC註冊並獲取分片事務id
                register();
            } catch (TransactionException e) {
                recognizeLockKeyConflictException(e);
            }

            try {
                //生成恢復日誌到undo_log表中
                if (context.hasUndoLog()) {
                    UndoLogManager.flushUndoLogs(this);
                }
                //真正提交數據庫事事務
                targetConnection.commit();
            } catch (Throwable ex) {
                //向TC提交事務執行失敗
                report(false);
                if (ex instanceof SQLException) {
                    throw (SQLException) ex;
                } else {
                    throw new SQLException(ex);
                }

            }
            //向TC提交事務執行成功
            report(true);
            context.reset();

        } else {
            targetConnection.commit();
        }
    }

在執行完commit操作之後,分片執行成功或失敗在TC中都存在記錄,並且數據庫中原有的數據也會生成恢復日誌保存到undo_log中,以便全局事務回滾時將數據還原。

5、事務commit或rollback

在上一篇博客《Fescar源碼學習--事物管理者TM(服務調用方)》中我們已經學習到全局事務的提交或者回滾由TM(服務調用方)發送指令到TC,TC會根據全局事務XID將分片事務commit或rollback的操作通知到每個RM。

RM分片事務操作:

(1)commit:RM直接刪除本地undo_log的記錄即可

(2)rollback:RM根據本地undo_log表中的記錄還原數據,可能會產生髒數據(undo_log中記錄的是某個時間點的原始數據,可能和當前數據已經不一致了)。

RM提供了RmMessageListener用於建立TC發送過來的commit或rollback操作指令

    @Override
    public void onMessage(long msgId, String serverAddress, Object msg, ClientMessageSender sender) {
        if (LOGGER.isInfoEnabled()) {
            LOGGER.info("onMessage:" + msg);
        }
        //commit 操作
        if (msg instanceof BranchCommitRequest) {
            handleBranchCommit(msgId, serverAddress, (BranchCommitRequest)msg, sender);
        //rollback 操作
        } else if (msg instanceof BranchRollbackRequest) {
            handleBranchRollback(msgId, serverAddress, (BranchRollbackRequest)msg, sender);
        }
    }

6、commit操作

對於commit的操作RM的處理是簡單的,RM只需要保證能將undo_log表中相關的記錄刪除即可,不需要過多的處理操作,因此commit請求最終會提交給AsyncWorker,由線程定時異步刪除記錄即可。

(1)xid最終會記錄到map中

(2)在AsyncWorker初始化時建立定時任務每秒執行一次doBranchCommits函數,刪除undo_log表中的記錄

(3)在doBranchCommits中根據全局事務xid和分片事務branchId調用UndoLogManager.deleteUndoLog刪除記錄。

DataSourceManager中提交到 AsyncWorker異步操作:

    @Override
    public BranchStatus branchCommit(String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
        return asyncWorker.branchCommit(xid, branchId, resourceId, applicationData);
    }

 在AsyncWorker中異常刪除記錄操作:

    @Override
    public BranchStatus branchCommit(String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
        //提交事務ID到map中記錄即可
        if (ASYNC_COMMIT_BUFFER.size() < ASYNC_COMMIT_BUFFER_LIMIT) {
            ASYNC_COMMIT_BUFFER.add(new Phase2Context(xid, branchId, resourceId, applicationData));
        } else {
            LOGGER.warn("Async commit buffer is FULL. Rejected branch [" + branchId + "/" + xid + "] will be handled by housekeeping later.");
        }
        return BranchStatus.PhaseTwo_Committed;
    }

    public synchronized void init() {
        LOGGER.info("Async Commit Buffer Limit: " + ASYNC_COMMIT_BUFFER_LIMIT);
        timerExecutor = new ScheduledThreadPoolExecutor(1,
            new NamedThreadFactory("AsyncWorker", 1, true));
        //定時任務,定時處理commit操作
        timerExecutor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                try {

                    doBranchCommits();


                } catch (Throwable e) {
                    LOGGER.info("Failed at async committing ... " + e.getMessage());

                }
            }
        }, 10, 1000 * 1, TimeUnit.MILLISECONDS);
    }

    //調用 UndoLogManager.deleteUndoLog 刪除記錄即可
    private void doBranchCommits() {
        if (ASYNC_COMMIT_BUFFER.size() == 0) {
            return;
        }
        Map<String, List<Phase2Context>> mappedContexts = new HashMap<>();
        Iterator<Phase2Context> iterator = ASYNC_COMMIT_BUFFER.iterator();
        while (iterator.hasNext()) {
            Phase2Context commitContext = iterator.next();
            List<Phase2Context> contextsGroupedByResourceId = mappedContexts.get(commitContext.resourceId);
            if (contextsGroupedByResourceId == null) {
                contextsGroupedByResourceId = new ArrayList<>();
                mappedContexts.put(commitContext.resourceId, contextsGroupedByResourceId);
            }
            contextsGroupedByResourceId.add(commitContext);

            iterator.remove();

        }

        for (String resourceId : mappedContexts.keySet()) {
            Connection conn = null;
            try {
                try {
                    DataSourceProxy dataSourceProxy = DataSourceManager.get().get(resourceId);
                    conn = dataSourceProxy.getPlainConnection();
                } catch (SQLException sqle) {
                    LOGGER.warn("Failed to get connection for async committing on " + resourceId, sqle);
                    continue;
                }

                List<Phase2Context> contextsGroupedByResourceId = mappedContexts.get(resourceId);
                for (Phase2Context commitContext : contextsGroupedByResourceId) {
                    try {
                        UndoLogManager.deleteUndoLog(commitContext.xid, commitContext.branchId, conn);
                    } catch (Exception ex) {
                        LOGGER.warn("Failed to delete undo log [" + commitContext.branchId + "/" + commitContext.xid + "]", ex);
                    }
                }

            } finally {
                if (conn != null) {
                    try {
                        conn.close();
                    } catch (SQLException closeEx) {
                        LOGGER.warn("Failed to close JDBC resource while deleting undo_log ", closeEx);
                    }
                }
            }


        }


    }

7、rollback操作

在DataSourceManager中進行事務回滾操作

    @Override
    public BranchStatus branchRollback(String xid, long branchId, String resourceId, String applicationData) throws TransactionException {
        DataSourceProxy dataSourceProxy = get(resourceId);
        if (dataSourceProxy == null) {
            throw new ShouldNeverHappenException();
        }
        try {
            //根據表undo_log中的快照信息回滾數據
            UndoLogManager.undo(dataSourceProxy, xid, branchId);
        } catch (TransactionException te) {
            if (te.getCode() == TransactionExceptionCode.BranchRollbackFailed_Unretriable) {
                return BranchStatus.PhaseTwo_RollbackFailed_Unretriable;
            } else {
                return BranchStatus.PhaseTwo_RollbackFailed_Retriable;
            }
        }
        //返回回滾結果信息
        return BranchStatus.PhaseTwo_Rollbacked;

    }

在UndoLogManager中首先根據全局事務XID和分片事務branchId獲取結果信息,其中在rollback_info中以二進制方式保存了快照信息。

分片事務回滾: 

public static void undo(DataSourceProxy dataSourceProxy, String xid, long branchId) throws TransactionException {
        assertDbSupport(dataSourceProxy.getTargetDataSource().getDbType());

        Connection conn = null;
        ResultSet rs = null;
        PreparedStatement selectPST = null;
        try {
            conn = dataSourceProxy.getPlainConnection();

            // The entire undo process should run in a local transaction.
            conn.setAutoCommit(false);

            // Find UNDO LOG
            //根據xid和branchId查找數據
            selectPST = conn.prepareStatement(SELECT_UNDO_LOG_SQL);
            selectPST.setLong(1, branchId);
            selectPST.setString(2, xid);
            rs = selectPST.executeQuery();
            
            while (rs.next()) {
                Blob b = rs.getBlob("rollback_info");
                //獲取rollback_info字段數據
                String rollbackInfo = StringUtils.blob2string(b);
                //生成回滾sql
                BranchUndoLog branchUndoLog = UndoLogParserFactory.getInstance().decode(rollbackInfo);

                //執行回滾操作
                for (SQLUndoLog sqlUndoLog : branchUndoLog.getSqlUndoLogs()) {
                    TableMeta tableMeta = TableMetaCache.getTableMeta(dataSourceProxy, sqlUndoLog.getTableName());
                    sqlUndoLog.setTableMeta(tableMeta);
                    AbstractUndoExecutor undoExecutor = UndoExecutorFactory.getUndoExecutor(dataSourceProxy.getDbType(), sqlUndoLog);
                    undoExecutor.executeOn(conn);
                }

            }
            //刪除記錄
            deleteUndoLog(xid, branchId, conn);
            //提交事務
            conn.commit();

        } catch (Throwable e) {
            if (conn != null) {
                try {
                    conn.rollback();
                } catch (SQLException rollbackEx) {
                    LOGGER.warn("Failed to close JDBC resource while undo ... ", rollbackEx);
                }
            }
            throw new TransactionException(BranchRollbackFailed_Retriable, String.format("%s/%s", branchId, xid), e);

        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (selectPST != null) {
                    selectPST.close();
                }
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException closeEx) {
                LOGGER.warn("Failed to close JDBC resource while undo ... ", closeEx);
            }
        }

    }

總結:

(1)Fescar通過dubbo遠程調用傳遞全局事務id

(2)RM在執行本地數據庫操作時首先會向TC申請分片事務

(3)根據分片事務id在undo_log生成回滾日誌

(4)執行本地數據庫操作,成功或失敗都會向TC進行報告

(5)TM進行事務commit或rollback操作,將操作提交到TC,TC將操作發送到RM

(6)RM接收到TC的通知進行commit或rollback操作

(7)如果是commit操作則異步通過線程AsyncWorker進行刪除本地undo_log即可

(8)如果是rollback操作則根據全局事務xid和分片事務branchId找到記錄,根據字段rollback_info中的快照信息進行回滾數據操作。

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