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();
啊!好長啊,終於寫完了。可能有理解不對的地方,還請多多指教哈!