大學裏面數據庫課考試,事務和鎖的相關知識絕對是要劃的重點。數據庫的事務要遵循ACID(原子性、一致性、隔離性、持久性)四要素,鎖又有悲觀鎖和樂觀鎖的劃分方式。那麼今天咱們講講,如何基於SpringBoot+Mybatis的框架,進行有關事務和鎖的代碼開發。java
在實際應用中,兩者密不可分。在業務系統開發過程當中,每每有一系列對數據庫的操做是須要綁定在一個事務裏的,要麼一塊兒提交,要麼一塊兒回滾。例如:A給B轉100塊錢,同時要執行 下面兩個方法。mysql
(1)update account set money=money-100 where user='A'; (2)update account set money=money+100 where user='B' ;
這兩個方法必須做爲同一個事務提交,事務提交的結果,要麼轉帳成功,要麼轉帳失敗。是絕對不可以存在A扣錢成功,B帳號沒加錢;或A沒扣錢,B的帳號卻多了100塊錢。sql
爲了遵循事務的ACID原則,咱們會引用了鎖的概念,若是是單純基於某個數據庫的事務,咱們可使用接下來要講的悲觀鎖和樂觀鎖。固然有些特殊狀況,咱們還須要考慮分佈式事務鎖的方案,那就說來話長了,本文就不作介紹了。數據庫
在使用事務以前,請先保證數據是手動提交事務的。oracle默認是手動提交事務的,可是mysql數據庫一般默認都是自動提交事務的,下面是如何關閉mysql自動提交事務的設置。後端
--查看是否自動提交 show variables like '%autocommit%'; --0爲關閉自動提交;1爲開啓自動提交 set global autocommit= 0
實際開發過程當中,咱們絕大部分的事務都是有併發狀況。當多個事務併發運行,常常會操做相同的數據來完成各自的任務。在這種狀況下可能會致使如下的問題:併發
不可重複讀的和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。解決不可重複讀的問題只需鎖住知足條件的行,解決幻讀須要鎖表。oracle
SpringBoot爲事務管理提供了不少功能支持,目前最經常使用的就是經過聲明式事務管理,基於@Transactional註解的方式來實現。實現原理是基於AOP,對方法先後進行攔截,而後在目標方法開始以前建立或者加入一個事務,在執行完目標方法以後根據執行狀況提交或者回滾事務。app
@Transactional註解可做用於類、接口和方法上。框架
2.接口:在使用基於該接口的代理時,事務屬性纔會生效。
3.方法:做爲事務管理的最細粒度。值得注意的有,aop的本質決定該註解只能做用在public方法上,不然會被忽略,但不會報錯。分佈式
默認狀況下,只有來自外部的方法調用纔會被AOP代理捕獲,也就是,類內部方法調用本類內部的其餘方法並不會引發事務行爲,即便被調用方法使用@Transactional註解進行修飾。
開啓基於@Transactional事務的方式很簡單,先在啓動類經過 @EnableTransactionManagement 註解開啓事務管理。隨後在對應的類、接口、方法加上 @Transactional 就能夠了。
在某個示例Controller中的一個方法
@RequestMapping(path = "/updateNameNow",method = RequestMethod.GET) @Transactional public Response updateNameNow(@RequestParam("name")String name,@RequestParam("username")String username) throws Exception{ //根據username,更新用戶name userMapper.updateName(name,username); throw new RuntimeException("發生了一個錯誤"); }
該方法加了註解@Transactional,原方法做用是更新用戶的姓名,可是在執行dao層的update操做後,拋出了一個運行時異常。最終的結果是update事務回滾了,數據庫中沒有更新成功。
值得注意的是咱們拋出的異常是RuntimeException運行時異常,@Transactional默認支持回滾的異常就是運行時異常。非運行時異常(JAVA編譯器強制要求咱們必需對進行catch並處理的異常)並不會觸發事務回滾,不過咱們能夠在主鍵的屬性中申明支持回滾的粒度,如:
@RequestMapping(path = "/updateNameNow",method = RequestMethod.GET) @Transactional(rollbackFor =Exception.class ) public Response updateNameNow(@RequestParam("name")String name,@RequestParam("username")String username) throws Exception{ //根據username,更新用戶name userMapper.updateName(name,username); throw new Exception("發生了一個錯誤"); }
剛剛咱們見識過@Transactional中的rollbackFor 屬性,這裏列一下經常使用的幾種屬性。
propagation: propagation用於指定事務的傳播行爲,就是若是@Transactional的方法調用了另一個@Transactional的方法,事務該如何傳播。propagation有七種類型,默認值爲 REQUIRED。
屬性 | 含義 |
---|---|
REQUIRED | 若是當前沒有事務,就新建一個事務,若是已經存在一個事務,則加入到這個事務中。這是最多見的選擇。 |
SUPPORTS | 支持當前事務,若是當前沒有事務,就以非事務方式執行。 |
|MANDATORY|表示該方法必須在事務中運行,若是當前事務不存在,則會拋出一個異常。|
|REQUIRES_NEW|表示當前方法必須運行在它本身的事務中。一個新的事務將被啓動。若是存在當前事務,在該方法執行期間,當前事務會被掛起。|
|NOT_SUPPORTED|表示該方法不該該運行在事務中。若是當前存在事務,就把當前事務掛起。|
|NEVER|表示當前方法不該該運行在事務上下文中。若是當前正有一個事務在運行,則會拋出異常。|
|NESTED|若是當前存在事務,則在嵌套事務內執行。若是當前沒有事務,則執行與PROPAGATION_REQUIRED相似的操做。|
咱們這裏回顧一下數據庫中的兩種鎖,悲觀鎖和樂觀鎖。
悲觀鎖,顧名思義,就是對數據的衝突採起一種悲觀的態度,也就是說假設數據確定會衝突,因此在數據開始讀取的時候就把數據鎖定住。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。樂觀鎖,就是認爲數據通常狀況下不會形成衝突,因此在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測。若是發現衝突了,則讓用戶返回錯誤的信息,讓用戶決定如何去作。Java中有CAS就是樂觀鎖的實現方式。
加鎖實際上會增長數據庫資源的消耗,至於咱們該如何合理的選用鎖,則取決於實際應用場景中事務衝突發生的頻率。若是衝突的頻率較高,建議選擇悲觀鎖;若是衝突的頻率較低,樂觀鎖顯然更合適。
oracle和mysql數據庫都支持行級鎖,行級鎖中又分共享鎖(讀鎖)和排他鎖(寫鎖)。而悲觀鎖明顯是排他鎖,須要阻塞其餘的寫鎖和讀鎖。
對應於數據庫的經常使用操做中,共享鎖對應的語言是DQL(select),排他鎖對應的語言是DML(update,delete,insert)。咱們若是要保證DQL也遵循悲觀鎖的控制,能夠經過 (select ... for update)來實現。咱們來看一個例子。
UserMapper.java
/** * 根據 username 查詢 name * @param username * @return */ @Select("select name from user where username=#{username} for update") String getNameByUsername(@Param("username") String username); /** * 更新 name * @param name * @param username * @return */ @Update("update user set name=#{name} where username=#{username}") int updateName(@Param("name")String name,@Param("username")String username);
UserController.java
/** * 查詢 name ,停10秒返回結果 * @param username * @return */ @RequestMapping(path = "/getNameByUsername", method = RequestMethod.GET) @Transactional public Response getNameByUsername(@RequestParam("username") String username) { String name = userMapper.getNameByUsername(username); try { Thread.sleep(10000); } catch (Exception e) { e.printStackTrace(); } return Response.ok().data(name); } /** * 更新 name,馬上返回 * * @param name * @param username * @return * @throws Exception */ @RequestMapping(path = "/updateNameNow", method = RequestMethod.GET) @Transactional public Response updateNameNow(@RequestParam("name") String name, @RequestParam("username") String username) throws Exception { int ret = userMapper.updateName(name, username); return Response.ok().data(ret); }
咱們經過這兩個接口測試,UserMapper.getNameByUsername方法的查詢sql有 "for update" ,說明使用了排他鎖,而另外一個接口 UserMapper.updateName 明顯也是排他鎖。
樂觀鎖的控制權通常不在數據庫層面,而在業務層面。並無任何排他鎖的操做,而是在最後提交的時候,按照咱們自定義的規則比對一下數據,若是按照咱們的規則發現數據衝突了,則本身解決衝突。那麼重點就在於這個自定義的規則。
我在咱們公司,早期是基於Oracle的ADF框架作開發的。建表後要在ADF中建Entity Object 作字段的映射,Entity Object 有5個基礎字段:
前面4個字段咱們很好理解,最後一個version number 版本號,我以前一直以爲不少餘。實際上它是ADF中實現樂觀鎖的關鍵字段,包括Hibernate等 orm框架都是利用它來作數據比較。咱們看下面的例子:
UserMapper.java
/** * 根據 username 查詢,返回 User對象 * @param username * @return */ @Select("select * from user where username=#{username}") User getUserByUsername(@Param("username") String username); /** * 根據版本號,更新User * @param user * @return */ @Update("update user set name=#{user.name},object_version_number=object_version_number+1 " + "where username=#{user.username} and object_version_number=#{user.objectVersionNumber}") int updateUser(@Param("user") User user);
UserController.java
/** * 更新 User * @param user * @return * @throws Exception */ @RequestMapping(path = "/updateUser", method = RequestMethod.POST) @Transactional(rollbackFor = Exception.class) public Response updateUser(@RequestBody User user) throws Exception { int ret = userMapper.updateUser(user); if (ret < 1) { throw new Exception("樂觀鎖致使保存失敗"); } return Response.ok(); }
必需要保證全部的對錶數據的更新操做,都要將版本號加1。在作DML操做時,須要帶上當前拿到的版本號信息,放在DML語言的where條件中。
這個例子中,咱們經過對錶中的版本號字段的比較,就完成了樂觀鎖的實現,實現方式明顯看起來要不悲觀鎖「友善」的多。咱們平時業務開發時,若是沒有遇到事務衝突很是嚴重的場景,使用樂觀鎖基本就能達到目的。