Mybatis Select...for update用法

Mybatis Select…for update用法

最近有需求批量處理大量數據,由於數據量很大,如果加分佈式鎖讓一個線程跑需要太長時間,所以考慮集羣中二十幾臺機器並行執行,每次取1000條數據處理。

選擇了使用select…for update悲觀鎖,每次把取出來的1000條數據加鎖之後更改狀態字段再commit,從而保證所有線程不重複取數據。

很容易想到的用法就是把select for upate和之後的更新語句放在一個事務中:


SqlSession sqlSession = sqlSessionFactory.openSession(false);
        try{
            List<TestObject> records=sqlSession.selectList("testForUpdate"); //testForUpdate的sql語句爲: select * from test_table where status='0' and rownum<1000 order by create_date desc for update
            if(records!=null && records.size>0){
                 Map<String,Object> updateParam=new HashMap<>();
                updateParam.put("records", records);
                updateParam.put("status", "01");
                sqlSession.update("batchUpdate", updateParam);
            }
        }finally{
            sqlSession.commit(true);
            sqlSession.close();
        }
  • 首先開一個session,參數傳false表示autocommit=false,不自動提交事務
  • 執行select for update
  • 更新取出來的數據
  • commit並且close session

commit的參數必須爲true,這樣在沒有數據更新時也可以commit。否則commit時會判斷一個isDirty的參數,這個參數只有在更新或者插入是會爲true,如果不爲true就不會commit,

sqlSession.commit(true)

具體的源碼如下,我們傳入的true就是這個force參數。

private boolean isCommitOrRollbackRequired(boolean force) {
        return !this.autoCommit && this.dirty || force;
    }

這種做法看起來沒有什麼問題,但是當select for update查不到數據時,也會對錶加行鎖,在我這個情況下就是給status=0的數據加行鎖,即使這些數據並不存在,這個鎖會一直存在,如果此時插入status=0的數據則會報錯。這個時候問題就暴露出來了,當查出的數據爲空時,就不會執行下面的update語句,然後就發現這個事務不會被提交,行鎖一直不會被釋放。

按理說finally語句塊裏的commit是無論如何都會執行的,爲什麼最後事務沒有提交呢?

Debug進去才發現了事情並不是我們一開始想象的這樣。

太長不看版本:

由於我們使用的數據源是阿里的DruidPooledDataSource,這個數據源的autoCommit配置默認爲true,導致我們開啓session時設置的false並不起作用,實際上我們的事務都是自動提交的,最後finally塊中的commit會判斷如果自動提交已經開啓它就不會執行。所以當select for update的數據爲空時,由於不會執行update語句, 所以沒有被自動commt,當我們想要手動commit時,由於已經開啓了自動commit,所以手動commit也乜嘢執行,最紅導致事務不能提交。

詳解

以DefaultSqlSession爲例,Session實例中包含的屬性有:

private Configuration configuration; //配置信息
private Executor executor; //實際執行select的對象
private boolean autoCommit; //openSession時傳入的參數,select之前爲false
private boolean dirty; //表示是否有髒數據,執行update或者insert時會爲true

當sqlSession執行selectList方法時:

public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        List var5;
        try {
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }

        return var5;
    }

可以看到實際執行query方法進行查詢的是Executor對象。

以BaseExecutor爲例,屬性有

protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<BaseExecutor.DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration; 
protected int queryStack = 0;
private boolean closed;

可以看到transaction實際是在Executor對象中,再看transaction的屬性,SpringManagedTransaction爲例:

protected Connection connection;
protected DataSource dataSource;
protected TransactionIsolationLevel level;
protected boolean autoCommmit;

可以看到connection實際是在transaction對象中獲取的,這裏也有一個autoCommit屬性,這個屬性的值在打開session時和我們傳入的參數值相等,是false。
再看connection對象,以我們用的DruidPooledConnection爲例

    public static final int MAX_RECORD_SQL_COUNT = 10;
    protected Connection conn;
    protected volatile DruidConnectionHolder holder;
    protected TransactionInfo transactionInfo;
    private final boolean dupCloseLogEnable;
    private volatile boolean traceEnable = false;
    private boolean disable = false;
    private boolean closed = false;
    private final Thread ownerThread;
    private long connectedTimeNano;
    private volatile boolean running = false;
    private volatile boolean abandoned = false;
    private StackTraceElement[] connectStackTrace;
    private Throwable disableError = null;

其中有get和set autoCommit的方法,然而這個類裏面並沒有autoCommit屬性,可以看到這裏還有一個conn屬性,這個get和set的autoCommit的值都是從這個conn裏來的。

現在把主要的類大致屢清楚了,那麼再看一下selectList的執行過程,以及我們openSession時配置的autoCommit是怎麼被覆蓋的。

前面說到了執行sqlSession的select方法實際執行的是Executor的query方法。執行時會先試圖從緩存中找,如果沒有對應的key則會執行queryFromDatabase方法,從DB中查詢。
在從DB查詢之前會執行prepareStatement方法生成一個Statement,在這個方法裏

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Connection connection = this.getConnection(statementLog);
        Statement stmt = handler.prepare(connection);
        handler.parameterize(stmt);
        return stmt;
    }

可以看到首先是獲取一個連接,最終調用的是Transaction的getConnection方法:

    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            this.openConnection();
        }

        return this.connection;
    }

如果當前沒有連接則open一個,注意,這個時候transaction對象的autoCommit還是我們設置的false。
接下來再看openConnection方法

private void openConnection() throws SQLException {
        this.connection = DataSourceUtils.getConnection(this.dataSource);
        this.autoCommit = this.connection.getAutoCommit();
        this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("JDBC Connection [" + this.connection + "] will" + (this.isConnectionTransactional ? " " : " not ") + "be managed by Spring");
        }

    }

首先由datasource獲取connection,大致過程是dataSource從自己的連接池裏面取一個連接返回過來,這裏的連接都是根據dataSource的配置提前生成的,也就是說由於我們dataSource默認配置的autoCommit爲true,所以這裏拿到的所有connection的autoCommit屬性都是true。

拿到connection後執行了this.autoCommit = this.connection.getAutoCommit();這樣transaction的autoCommit屬性就被設置成了true。

最後sqlSession執行commit時,實際執行commit的是Executor的commit,實際又是執行的transaction對象的commit:

public void commit() throws SQLException {
        if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
            }

            this.connection.commit();
        }

    }

可以看到執行commit的條件是connection不爲空,並且這個連接是事務性的,並且autoCommit=false,由於在獲取連接時transaction的autoCommit屬性已經被替換成了true,所以這裏的commit不會被執行。

終於解釋清楚了爲什麼commit沒有執行,事務沒有被提交。

實際上如果在openSession(false)之後執行一個update方法,在session.commit()之前這個事務就已經被自動提交了。由於select for update並不會觸發事務自動提交所以它的鎖不會被釋放。

解決方法

這裏整個過程中都是用的connection中的autoCommit屬性,而不是sqlSession的這個屬性,因此解決方法有:

  • 用sqlSession.getConnection().setAutoCommit(false);來設置autoCommit屬性爲false
  • 或者提交時用直接調用connection的commit方法:sqlSession.getConnection().commit();

啊!好長啊,終於寫完了。可能有理解不對的地方,還請多多指教哈!

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