select for update不交由spring事務管理的正確姿式

明確一下概念java

  1. set autocommit = 0

關閉當前會話中事務的自動提交,須要手動 commit 或者 rollback,至關於開啓一個全局的事務。在 mysql 的
事務中,默認autocommit = 1,每一次 sql 操做都被認爲是一個單次的事務,被隱式提交。而oracle默認是
autocommit=0mysql

  1. start transaction

掛起 autocommit 的狀態,開啓一個事務上下文。首先數據庫會隱式提交以前的還未被提交的操做,同時開啓一個新事務。掛起autocommit 的意思是保存 autocommit 的當前狀態,而後 start transaction,直到 commit or
rollback 結束本次事務,再恢復以前掛起的 autocommit 的狀態。
若是 start transaction 前 autocommit = 1,則完成本次事務後 autocommit 仍是 1
若是 start transaction 前 autocommit = 0,則完成本次事務後 autocommit 仍是0,接下來的操做你仍需手動
commit 才能夠提交。spring

SqlSession sqlSession = null;
        try {
            InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(in);
            // 不開啓自動提交事務
            sqlSession = sqlSessionFactory.openSession(false);
            List<User> list = sqlSession.selectList("com.shuang.test.findAllUsers");
            if (list.size() > 0) {
                sqlSession.update("xxx");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 強制提交事務,若是不設置的話,只有在insert或update纔會提交事務,若是selectForUpdate結果爲空,沒法進行update操做是沒法提交事務的
            sqlSession.commit(true);
            sqlSession.close();
        }

上面的代碼是在oracle數據庫中進行的,數據庫鏈接池使用druid,代碼看起來無任何問題。而實際是當查詢爲空時,不會執行下面的update語句,而事務仍是沒有提交,致使相應行被鎖住了。也就是sqlSessionFactory.openSession(false)中的設置的autoCommit不起做用(注意程序中的autoCommit的值與mysql中的autocommit沒有任何關係,它只是一個屬性,用來輔助代碼最後的是否進行commit,它不是設置mysql的autocommit的值)。debug源碼,跟蹤executor.query中的方法 sql

image.png
這裏的queryFromDatabase就是查詢數據庫了
image.png
debug裏面的doQuery,感受距離真相愈來愈近了
image.png
這裏的prepareStatement是關鍵
image.png
getConnection獲取鏈接就能進行數據庫操做了
image.png
這裏的Connection中的setAudoCommit纔是決定是否自動提交事務的關鍵,而sqlSessionFactory.openSession(false)設置autoCommit最終沒有正確賦值給Connection纔會致使事務沒有正確提交,而咱們使用Druid,對應的DruidPooledConnection的autoCommit默認是true,即自動提交事務
image.png
既然自動提交事務,那麼sqlSession.commit(true)就沒法強制提交事務了
image.png
最終是調用transaction.commit
image.png
由於它是自動commit,因此就沒有執行connection.commit(),最終致使在查詢結果爲空沒有執行下面的update語句時,selectForUpdate會沒有提交事務鎖住相應行。儘管查詢結果爲空,但它仍能夠鎖住行,好比這個例子中的sql是select * from user where age=18,儘管查詢結果爲空,但對於age=18的行鎖仍然存在,當其它的sql插入age=18的數據時會被阻塞
image.png數據庫

解決辦法有三種:mybatis

  1. 用sqlSession.getConnection().setAutoCommit(false);來設置autoCommit屬性爲false
  2. 提交時用直接調用connection的commit方法:sqlSession.getConnection().commit();
  3. 使用spring來開啓事務

須要注意的是,上面的例子,在oracle數據庫會阻塞,但mysql不會。緣由就是mysql數據庫是autocommit=1,會隱式commit,因此不會阻塞,而oracle是autocommit=0,但commit沒有執行就阻塞了。 併發

image.png

最後給出4個思考題梳理一下:oracle

  • 開啓事務,事務不提交,for update悲觀鎖不會釋放。(start transaction,必需要commit,這個沒什麼好說的)
public void forupdateByTransaction() throws Exception {
  // 主線程獲取獨佔鎖
  reentrantLock.lock();

  new Thread(() -> transactionTemplate.execute(transactionStatus -> {
    // select * from forupdate where name = #{name} for update
    this.forupdateMapper.findByName("testforupdate");
    System.out.println("==========for update==========");
    countDownLatch.countDown();
    // 阻塞不讓提交事務
    reentrantLock.lock();
    return null;
  })).start();

  countDownLatch.await();

  System.out.println("==========for update has countdown==========");
  this.forupdateMapper.updateByName("testforupdate");
  System.out.println("==========update success==========");

  reentrantLock.unlock();
}
  • 不加spring事務併發執行for update,oracle會發生阻塞,而mysql不會阻塞。(druid的屬性autocommit默認是true,mybatis認爲咱們已經提交事務了,不會執行commit,mysql的autocommit默認1,沒影響,但oracle的autocommit默認是0,須要顯式commit,因此阻塞了)
public void forupdateByConcurrent() {
  AtomicInteger atomicInteger = new AtomicInteger();

  for (int i = 0; i < 100; i++) {
    new Thread(() -> {
      // select * from forupdate where name = #{name} for update
      this.forupdateMapper.findByName("testforupdate");
      System.out.println("========ok:" + atomicInteger.getAndIncrement());
    }).start();
  }

}
  • 不加spring事務併發for update操做,而且druid鏈接池autocommit=false,不會發生阻塞(因爲druid的autocommit是false,因此mybatis認爲事務沒有提交,幫咱們commit了,因此不會阻塞)
  • 加spring事務併發for update操做,不會發生阻塞(由spring負責start transaction,執行sql,而後commit,完美,不會阻塞)
private void forupdateByConcurrentAndTransaction() {
  AtomicInteger atomicInteger = new AtomicInteger();

  for (int i = 0; i < 100; i++) {
    new Thread(() -> transactionTemplate.execute(transactionStatus -> {
      // select * from forupdate where name = #{name} for update
      this.forupdateMapper.findByName("testforupdate");
      System.out.println("========ok:" + atomicInteger.getAndIncrement());
      return null;
    })).start();
  }
}
相關文章
相關標籤/搜索