劍指架構師系列-InnoDB存儲引擎、Spring事務與緩存
事務與鎖是不同的。事務具有ACID屬性:
原子性:持久性:由redo log重做日誌來保證事務的原子性和持久性,
一致性:undo log用來保證事務的一致性
隔離性:一個事務在操作過程中看到了其他事務的結果,如幻讀。鎖是用於解決隔離性的一種機制。事務的隔離級別通過鎖的機制來實現。
數據庫的事務隔離級別有(多個事務並發的情況下):
1、read uncommitted
#首先,修改隔離級別 set tx_isolation=‘READ-UNCOMMITTED‘; select @@tx_isolation; +------------------+ | @@tx_isolation | +------------------+ | READ-UNCOMMITTED | +------------------+ #事務A:啟動一個事務 start transaction; select * from tx; +------+------+ | id | num | +------+------+ | 1 | 1 | | 2 | 2 | | 3 | 3 | +------+------+ #事務B:也啟動一個事務(那麽兩個事務交叉了) 在事務B中執行更新語句,且不提交 start transaction; update tx set num=10 where id=1; select * from tx; +------+------+ | id | num | +------+------+ | 1 | 10 | | 2 | 2 | | 3 | 3 | +------+------+ #事務A:那麽這時候事務A能看到這個更新了的數據嗎? select * from tx; +------+------+ | id | num | +------+------+ | 1 | 10 | --->可以看到!說明我們讀到了事務B還沒有提交的數據 | 2 | 2 | | 3 | 3 | +------+------+ #事務B:事務B回滾,仍然未提交 rollback; select * from tx; +------+------+ | id | num | +------+------+ | 1 | 1 | | 2 | 2 | | 3 | 3 | +------+------+ #事務A:在事務A裏面看到的也是B沒有提交的數據 select * from tx; +------+------+ | id | num | +------+------+ | 1 | 1 | --->臟讀意味著我在這個事務中(A中),事務B雖然沒有提交,但它任何一條數據變化,我都可以看到! | 2 | 2 | | 3 | 3 | +------+------+
2、read committed
#首先修改隔離級別 set tx_isolation=‘read-committed‘; select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | READ-COMMITTED | +----------------+ #事務A:啟動一個事務 start transaction; select * from tx; +------+------+ | id | num | +------+------+ | 1 | 1 | | 2 | 2 | | 3 | 3 | +------+------+ #事務B:也啟動一個事務(那麽兩個事務交叉了)在這事務中更新數據,且未提交 start transaction; update tx set num=10 where id=1; select * from tx; +------+------+ | id | num | +------+------+ | 1 | 10 | | 2 | 2 | | 3 | 3 | +------+------+ #事務A:這個時候我們在事務A中能看到數據的變化嗎? select * from tx; -------------> +------+------+ | | id | num | | +------+------+ | | 1 | 1 |--->並不能看到! | | 2 | 2 | | | 3 | 3 | | +------+------+ |——>相同的select語句,結果卻不一樣 | #事務B:如果提交了事務B呢? | commit; | | #事務A: | select * from tx; -------------> +------+------+ | id | num | +------+------+ | 1 | 10 |--->因為事務B已經提交了,所以在A中我們看到了數據變化 | 2 | 2 | | 3 | 3 | +------+------+
3、repeatable read
#首先,更改隔離級別 set tx_isolation=‘repeatable-read‘; select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ #事務A:啟動一個事務 start transaction; select * from tx; +------+------+ | id | num | +------+------+ | 1 | 1 | | 2 | 2 | | 3 | 3 | +------+------+ #事務B:開啟一個新事務(那麽這兩個事務交叉了) 在事務B中更新數據,並提交 start transaction; update tx set num=10 where id=1; select * from tx; +------+------+ | id | num | +------+------+ | 1 | 10 | | 2 | 2 | | 3 | 3 | +------+------+ commit; #事務A:這時候即使事務B已經提交了,但A能不能看到數據變化? select * from tx; +------+------+ | id | num | +------+------+ | 1 | 1 | --->還是看不到的!(這個級別2不一樣,也說明級別3解決了不可重復讀問題) | 2 | 2 | | 3 | 3 | +------+------+ #事務A:只有當事務A也提交了,它才能夠看到數據變化 commit; select * from tx; +------+------+ | id | num | +------+------+ | 1 | 10 | | 2 | 2 | | 3 | 3 | +------+------+
4、serializable
#首先修改隔離界別 set tx_isolation=‘serializable‘; select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | SERIALIZABLE | +----------------+ #事務A:開啟一個新事務 start transaction; #事務B:在A沒有commit之前,這個交叉事務是不能更改數據的 start transaction; insert tx values(‘4‘,‘4‘); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction update tx set num=10 where id=1; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
總結一下:
√: 可能出現 ×: 不會出現
事務的隔離級別 | 臟讀 事務1更新了記錄,但沒有提交,事務2讀取了更新後的行,然後事務T1回滾,現在T2讀取無效。違反隔離性導致的問題,添加行鎖實現 | 不可重復讀 事務1讀取記錄時,事務2更新了記錄並提交,事務1再次讀取時可以看到事務2修改後的記錄(修改批更新或者刪除)需要添加行鎖進行實現 |
幻讀 事務1讀取記錄時事務2增加了記錄並提交,事務1再次讀取時可以看到事務2新增的記錄。需要添加表鎖進行實現。InnoDB存儲引擎通過多版本並發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題 |
Read uncommitted | √ | √ | √ |
Read committed | × | √ | √ |
Repeatable read | × | × | √ |
Serializable | × | × | × |
註意點: (1)要分清不可重復讀和幻讀的區別 一個是更新記錄,另外一個是讀取了新增的記錄 (2)不同的數據庫存儲引擎其實並沒有嚴格按照標準來執行,如innodb默認的repeatable read隔離級別下就可以做到避免幻讀的問題(采用了Next-Key-Lock鎖的算法)。InnoDB和Falcon存儲引擎通過多版本並發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題。
對應著Spring中的5個事務隔離級別(通過lsolation的屬性值指定)
1、default 默認的事務隔離級別。使用的是數據庫默認的事務隔離級別
2、read_uncommitted 讀未提交,一個事務可以操作另外一個未提交的事務,不能避免臟讀,不可重復讀,幻讀,隔離級別最低,並發性能最高
3、read_committed(臟讀) 大多數數據庫默認的事務隔離級別。讀已提交,一個事務不可以操作另外一個未提交的事務, 能防止臟讀,不能避免不可重復讀,幻讀
4、repeatable_read(不可重復讀) innodb默認的事務隔離級別。能夠避免臟讀,不可重復讀,不能避免幻讀
5、serializable(幻讀) innodb存儲引擎在這個級別才能有分布式XA事務的支持。隔離級別最高,消耗資源最低,代價最高,能夠防止臟讀, 不可重復讀,幻讀
Spring中的事務完全基於數據庫的事務,如果數據庫引擎使用MyISAM引擎,那Spring的事務其實是不起作用的。另外,Spring為開發者提供的與事務相關的特性就是事務的傳播行為,如下:
事務傳播行為類型 |
說明 |
propagation_required |
如果當前沒有事務,就新建一個事務,如果已經存在一個事務中,加入到這個事務中。這是最常見的選擇(Spring默認的事務傳播行為) |
propagation_supports |
支持當前事務,如果當前沒有事務,就以非事務方式執行 |
propagation_mandatory(托管) |
使用當前的事務,如果當前沒有事務,就拋出異常 |
propagation_requireds_new |
新建事務,如果當前存在事務,把當前事務掛起 |
propagation_not_supported |
以非事務方式執行操作,如果當前存在事務,就把當前事務掛起 |
propagation_never |
以非事務方式執行,如果當前存在事務,則拋出異常 |
propagation_nested |
如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行與propagation_required類似的操作,也就是新建一個事務
|
Spring通過事務傳播行為控制當前的事務如何傳播到被嵌套調用的目標服務接口方法中。
Spring可以配置事務的屬性,但是隔離級別、讀寫事務屬性、超時時間與回滾設置等都交給了JDBC,真正自己實現的只有事務的傳播行為。那麽什麽時候發生事務的傳播行為呢?
public class ForumService { private UserService userService; @Transactional(propagation = Propagation.REQUIRED) public void addTopic() { // add Topic this.updateTopic(); userService.addCredits(); } @Transactional(propagation = Propagation.REQUIRED) public void updateTopic() { // add Topic } public void setUserService(UserService userService) { this.userService = userService; } }
看一下userService中的addCredits()方法,如下:
public class UserService { @Transactional(propagation = Propagation.REQUIRES_NEW) public void addCredits() { } }
然後測試下:
forumService.addTopic();
開啟了Spring4日誌的DEBUG模式後,輸出如下:
- Returning cached instance of singleton bean ‘txManager‘ - Creating new transaction with name [com.baobaotao.service.ForumService.addTopic]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT; ‘‘ - Acquired Connection [[email protected]] for JDBC transaction - Switching JDBC Connection [[email protected]] to manual commit - Suspending current transaction, creating new transaction with name [com.baobaotao.service.UserService.addCredits] - Acquired Connection [[email protected]] for JDBC transaction - Switching JDBC Connection [[email protected]] to manual commit - Initiating transaction commit - Committing JDBC transaction on Connection [[email protected]] - Releasing JDBC Connection [[email protected]] after transaction - Returning JDBC Connection to DataSource - Resuming suspended transaction after completion of inner transaction - Initiating transaction commit - Committing JDBC transaction on Connection [[email protected]] - Releasing JDBC Connection [[email protected]] after transaction - Returning JDBC Connection to DataSource
清楚的看到調用addCredis()方法時創建了一個新的事務,而在這個方法中調用addCredits()方法時,由於這個方法的事務傳播行為為progation_required_new,所以掛起了當前的線程,又創建了一個新的線程。但是對於this.updateTopic()方法調用時,由於這個
方法的事務仍然為propagation_required,所以在當前線程事務中執行即可。
在使用事務中我們需要做到盡量避免死鎖、盡量減少阻塞,根據不同的數據庫設計和性能要求進行所需要的隔離級別,才是最恰當的。具體以下方面需要特別註意:
A、 事務操作過程要盡量小,能拆分的事務要拆分開來
B、 事務操作過程不應該有交互(系統交互,接口調用),因為交互等待的時候,事務並未結束,可能鎖定了很多資源
C、 事務操作過程要按同一順序訪問對象。(避免死鎖的情況產生)
D、 提高事務中每個語句的效率,利用索引和其他方法提高每個語句的效率可以有效地減少整個事務的執行時間。
E、 查詢時可以用較低的隔離級別,特別是報表查詢的時候,可以選擇最低的隔離級別(未提交讀)。
劍指架構師系列-InnoDB存儲引擎、Spring事務與緩存