主要以結果為導向解釋Spring 事務原理,連接池的消耗,以及事務內開啟事務線程要注意的問題.
Spring 事務原理這里不多說,網上一搜一大堆,也就是基於AOP配合ThreadLocal實現.
這里強調一下Spring Aop 以及Spring 注解式注入在非Spring容器管理的類中是無效的.
因為Spring Aop是在運行時實現字節碼增強,字節碼增強有多種實現方法,請自行了解,原生AspectJ是編譯時織入,但是需要特定的編譯器.語法並沒有Spring Aop好理解.
先看下Spring的 事務傳播行為類型
事務傳播行為類型 |
說明 |
PROPAGATION_REQUIRED |
如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是 最常見的選擇。 |
PROPAGATION_SUPPORTS |
支持當前事務,如果當前沒有事務,就以非事務方式執行。 |
PROPAGATION_MANDATORY |
使用當前的事務,如果當前沒有事務,就拋出異常。 |
PROPAGATION_REQUIRES_NEW |
新建事務,如果當前存在事務,把當前事務掛起。 |
PROPAGATION_NOT_SUPPORTED |
以非事務方式執行操作,如果當前存在事務,就把當前事務掛起。 |
PROPAGATION_NEVER |
以非事務方式執行,如果當前存在事務,則拋出異常。 |
PROPAGATION_NESTED |
如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與 PROPAGATION_REQUIRED類似的操作。 |
打開日記debug模式,留意控制台輸出
以下測試為了可讀性以及更容易理解全是基於Spring注解式事務,而沒有配置聲明式事務.
測試1:
可以看見只創建了一個SqlSession以及一個事務,在方法內所有操作都使用同一個連接,同一個事務
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) @Transactional(propagation = Propagation.REQUIRED) public void testThreadTx(){ //此方法沒有事務(當前方法是 Propagation.REQUIRED) Quotation quotation = quotationService.findEntityById(new String("1")); //此方法沒有事務(當前方法是 Propagation.REQUIRED) quotationService.updateEntity(quotation); }
//查看控制台輸出(紅字關鍵部分,第三個查詢是更新方法內部需要先查詢一次再更新,可以無視)
Creating a new SqlSession Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] ansaction(line/:54) -JDBC Connection [1068277098(com.mysql.jdbc.JDBC4Connection@5d92bace)] will be managed by Spring otationMapper.findEntityById(line/:54) -==> Preparing: SELECT * FROM table WHERE id = 1 otationMapper.findEntityById(line/:54) -==> Parameters: otationMapper.findEntityById(line/:54) -<== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] from current transaction Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] from current transaction otationMapper.updateEntity(line/:54) -==> Preparing: update ….. where id = 1 otationMapper.updateEntity(line/:54) -==> Parameters: otationMapper.updateEntity(line/:54) -<== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] .impl.UserOperationLogServiceImpl(line/:41) -請求所用時間:207 .impl.UserOperationLogServiceImpl(line/:42) -請求結束******************************************************************************* Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4] Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@230376b4]
測試2:不使用事務
可以看出在非事務操作數據庫,會使用多個連接,非常不環保,這里給稍微多線程插入埋下一個陷阱
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) // @Transactional(propagation = Propagation.REQUIRED) public void testThreadTx(){ Quotation quotation = quotationService.findEntityById(new String("1")); quotationService.updateEntity(quotation); }
//查看控制台輸出(紅字關鍵部分,第三個查詢是更新方法內部需要先查詢一次再更新,可以無視) Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f7b94f] was not registered for synchronization because synchronization ansaction(line/:54) -JDBC Connection [352410768(com.mysql.jdbc.JDBC4Connection@c63fcb6)] will not be managed by Spring otationMapper.findEntityById(line/:54) -==> Preparing: SELECT * FROM table WHERE id = 1 otationMapper.findEntityById(line/:54) -==> Parameters: otationMapper.findEntityById(line/:54) -<== Total: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7f7b94f] Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7a41785a] was not registered for synchronization because synchronization ansaction(line/:54) -JDBC Connection [1615108970(com.mysql.jdbc.JDBC4Connection@38377d86)] will not be managed by Spring otationMapper.findEntityById(line/:54) -==> Preparing: SELECT * FROM table WHERE id = 1 otationMapper.findEntityById(line/:54) -==> Parameters: otationMapper.findEntityById(line/:54) -<== Total: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7a41785a] Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@181e5a22] was not registered for synchronization because synchronization ansaction(line/:54) -JDBC Connection [2096339748(com.mysql.jdbc.JDBC4Connection@5d4e9892)] will not be managed by Spring otationMapper.updateEntity(line/:54) -==> Preparing: update …. where id = 1 otationMapper.updateEntity(line/:54) -==> Parameters: otationMapper.updateEntity(line/:54) -<== Updates: 1 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@181e5a22] .impl.UserOperationLogServiceImpl(line/:41) -請求所用時間:614 .impl.UserOperationLogServiceImpl(line/:42) -請求結束*******************************************************************************
測試3:
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) @Transactional(propagation = Propagation.REQUIRED) public void testThreadTx(){ final ExecutorService executorService = Executors.newFixedThreadPool(3); Quotation quotation = quotationService.findEntityById(new String("1")); quotationService.updateEntity(quotation); List<Future<Integer>> futures = new ArrayList<Future<Integer>>(3); for(int i=0;i<3;i++){ Callable<Integer> task = new Callable<Integer>() { @Override public Integer call() throws Exception { Quotation quotation = quotationService.findEntityById(new String("1")); quotationService.updateEntity(quotation); return null; } }; futures.add(executorService.submit(task)); } executorService.shutdown(); }
//查看控制台輸出(紅字關鍵部分,第三個查詢是更新方法內部需要先查詢一次再更新,可以無視)
為了節篇幅,這里不貼出控制台數據
大概就是輸出了10個Creating a new SqlSession(大概有些同學使用了多線程,把線程池耗完了也沒弄明白原因)
外層方法啟動一個,內部3個線程,每個線程3個.一共是使用了10個連接.
為什么?這涉及到ThreadLocal以及線程私有棧的概念.如果Spring 事務使用InhertableThreadLocal就可以把連接傳到子線程,但是為什么Spring不那么干呢?因為這樣毫無意義,如果把同一個連接傳到子線程,那就是SQL操作會串行執行,那何必還多線程呢?
有關於ThreadLocal,InhertableThreadLocal配合線程池的一些陷阱
請看我另一篇文章:
ThreadLoacl,InheritableThreadLocal,原理,以及配合線程池使用的一些坑
測試4:
既然使用同一個事務,不能實現並發操作,那么只能折中,在每一個線程開啟一個事務,減少創建更多的連接,執行完畢以后可以返回操作成功失敗結果,反饋給用戶
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) // @Transactional(propagation = Propagation.REQUIRED) public void testThreadTx(){ ExecutorService executorService = Executors.newFixedThreadPool(3); List<Future<Integer>> futures = new ArrayList<Future<Integer>>(3); for(int i=0;i<3;i++){ Callable<Integer> task = new Callable<Integer>() { @Override public Integer call() throws Exception { quotationService.doSomeThing(); return null; } }; futures.add(executorService.submit(task)); } executorService.shutdown(); } //封裝一下 @Override @Transactional(propagation =Propagation.REQUIRED) public void doSomeThing(){ Quotation quotation = this.findEntityById(new String("1")); this.updateEntity(quotation); }
//查看控制台輸出,只會創建3個連接,為節省篇幅,這里不貼出控制台所有數據 Creating a new SqlSession Creating a new SqlSession Creating a new SqlSession
最后小技巧PROPAGATION_NOT_SUPPORTED(僅僅為了讓Spring能獲取ThreadLocal的connection),如果不使用事務,但是同一個方法多個對數據庫操作,那么使用這個傳播行為可以減少消耗數據庫連接
@RequestMapping(value="/testThreadTx",method = RequestMethod.GET) @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testThreadTx(){ Quotation quotation = quotationService.findEntityById(new String("1")); quotation.setStatus(ClassDataManager.STATE_N); quotationService.updateEntity(quotation); } //這樣只會創建一個SqlSession