spring 事务隔离级别和传播行为_Java工程师面试1000题146-Spring数据库事务传播属性和隔离级别...
146、簡介一下Spring支持的數據庫事務傳播屬性和隔離級別
介紹Spring所支持的事務和傳播屬性之前,我們先了解一下SpringBean的作用域,與此題無關,僅做一下簡單記錄。
在Spring中,可以在元素的scope屬性中設置bean的作用域,來決定這個bean是單實例的還是多實例的。默認情況下,Spring只為每個在IOC容器里聲明的bean創建唯一的實例,整個IOC容器范圍內都可以共享該實例;所有后續的getBean()調用和bean引用都將返回這個唯一的bean實例,該作用域被稱為singleton,他是所有bean的默認作用域。
介紹完Spring Bean的作用域之后,下面開始進入正題——Spring支持的數據庫事務傳播屬性和隔離級別
1、事務的傳播屬性
首先我們先了解一下什么是事務的傳播屬性(傳播行為):當一個事務方法被被另一個事務方法調用時,必須指定事務應該如何傳播。例如:方法可能繼續在現有事務中運行,也可能開啟一個新事務,并在自己的事務中運行。
事務的傳播行為是由傳播屬性來指定的。
propagation:用來設置事務的傳播行為:一個方法運行在了一個開啟了事務的方法中時,當前方法是使用原來的事務,還是開啟一個新的事務,這就是事務的傳播行為。
比如:Propagation.REQUIRED:默認值,代表繼續使用原來的事務;Propagation.REQUIRES_NEW:將原來的事務掛起,開啟一個新的事務。最常用的事務傳播屬性就是REQUIRED和REQUIRES_NEW,下面就通過編程來進行測試。
首先,在數據庫里面新建三張表:
CREATE DATABASE /*!32312 IF NOT EXISTS*/`location` /*!40100 DEFAULT CHARACTER SET utf8 */;USE `location`;DROP TABLE IF EXISTS `account`;CREATE TABLE `account` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(30) DEFAULT NULL, `balance` float unsigned DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;insert into `account`(`id`,`username`,`balance`) values (1,'HanZong',100);DROP TABLE IF EXISTS `book`;CREATE TABLE `book` ( `isbn` varchar(20) DEFAULT NULL, `name` varchar(20) DEFAULT NULL, `price` float DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into `book`(`isbn`,`name`,`price`) values ('1001','Spring',60),('1002','SpringMVC',50);DROP TABLE IF EXISTS `book_stock`;CREATE TABLE `book_stock` ( `isbn` varchar(20) DEFAULT NULL, `stock` int(11) DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;insert into `book_stock`(`isbn`,`stock`) values ('1001',100),('1002',100);然后,搭建Spring的開發環境,具體配置在這里不再講解了,不是本知識點的重點。然后新建三個接口,三個實現類。
Cashier接口:
import java.util.List;public interface Cashier { //去結賬的方法 void checkout(int userId, List isbns);}實現類:import com.spring.transaction.BookShopService;import com.spring.transaction.Cashier;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.List;@Service("cashier")public class CashierImpl implements Cashier { @Autowired private BookShopService bookShopService; @Transactional @Override public void checkout(int userId, List isbns) { for (String isbn : isbns){ //調用BookShopService中的買東西方法 bookShopService.purchase(userId,isbn); } }}BookShopService接口:public interface BookShopService { //定義一個買東西方法 void purchase(int userId,String isbn);}實現類:
import com.spring.transaction.BookShopDao;import com.spring.transaction.BookShopService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;@Servicepublic class BookShopServiceImpl implements BookShopService { @Autowired private BookShopDao bookShopDao; @Transactional @Override public void purchase(int userId, String isbn) { //1.獲取要買的圖書的價格 double bookPrice = bookShopDao.getBookPriceByIsbn(isbn);System.out.println(bookPrice); //2.更新圖書的庫存 bookShopDao.updateBookStock(isbn); //3.更新用戶的余額 bookShopDao.updateAccountBalance(userId, bookPrice);double bookPriceByIsbn = bookShopDao.getBookPriceByIsbn(isbn);System.out.println(bookPriceByIsbn); }}操作數據庫的接口:
public interface BookShopDao {//根據書號查詢圖書的價格double getBookPriceByIsbn(String isbn);//根據書號更新圖書的庫存,每次只買一本圖書void updateBookStock(String isbn);//根據用戶的id和圖書的價格更新用戶的賬戶余額void updateAccountBalance(int userId, double bookPrice);}實現類:
import com.spring.transaction.BookShopDao;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.jdbc.core.JdbcTemplate;import org.springframework.stereotype.Repository;@Repository("bookShopDao")public class BookShopDaoImpl implements BookShopDao {@Autowiredprivate JdbcTemplate jdbcTemplate;@Overridepublic double getBookPriceByIsbn(String isbn) {// 寫sql語句String sql = "select price from book where isbn = ?";// 調用JdbcTemplate中的queryForObject方法Double bookPrice = jdbcTemplate.queryForObject(sql, Double.class, isbn);return bookPrice;}@Overridepublic void updateBookStock(String isbn) {// 寫sql語句String sql = "update book_stock set stock = stock - 1 where isbn = ?";// 調用JdbcTemplate中的update方法jdbcTemplate.update(sql, isbn);}@Overridepublic void updateAccountBalance(int userId, double bookPrice) {// 寫sql語句String sql = "update account set balance = balance - ? where id = ?";// 調用JdbcTemplate中的update方法jdbcTemplate.update(sql, bookPrice, userId);}}介紹一下上面的接口和實現類,BookShopService接口里面有一個買東西的方法purchase(),Cashier里面有一個checkout()方法,結賬的方法,checkout()方法要調用purchase()方法來實現功能,checkout()方法上面添加了聲明式事務注解@Transactional,purchase()方法上面也添加了聲明式事務注解@Transactional。checkout()方法調用了purchase()方法,兩個方法都使用了事務,這時候在運行的時候,purchase()到底是使用自己的事務呢,還是使用checkout()的事務呢?這個就屬于事務的傳播行為!
事務的傳播行為可以使用@Transactional注解里面的一個propagation屬性來設置。propagation可以設置以下7種屬性值。
我們來看一下啊,purchase()方法運行在checkout()方法里面,按照Spring默認的事務傳播屬性為REQUIRED,那么purchase()方法就應該使用checkout()方法的事務,checkout()方法里面有一個for循環,可能會調用多次purchase方法,根據事務的原子性,多次執行purchase()方法要么全部成功,要么全部失敗。我們寫一個測試方法:
public class TestTX { //創建IOC容器對象 ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); @Test public void testCashier(){ Cashier cashier = (Cashier) ioc.getBean("cashier"); //創建List List isbns = new ArrayList<>(); isbns.add("1001"); isbns.add("1002"); //去結賬 cashier.checkout(1,isbns); }}測試程序中,我們創建一個ArrayList,里面添加兩個圖書id,一個是1001,一個是1002,代表我們將要購買的圖書,圖書的價格保存在數據庫中book表中,1001的價格是60,1002的價格是50,另一張表book_store里面存放的是圖書庫存,1001庫存100本,1002庫存100本,最后一張表是用戶表account,里面就只有一個用戶,用戶余額100元,這個余額在建表的時候必須要設置為unsigned的,不能成為負數,否則就沒法測試了。
現在是賬戶余額只有100元,要同時買兩本1001和1002,明顯差10元。現在我們來測試一下,到底是一本也買不成功,還是可以買成功一本。根據事務傳播行為,沒有設置就代表默認值,默認值就是REQUIRED:如果有事務在運行,當前的方法就在這個事務內運行,否則就開啟一個新的事務,并在自己的事務內運行。也就是說,現在買1001和買1002在同一個事務里面,根據事務的原子性,要么都完成,要么都不完成,現在我的余額是100,可以買成功1001,不能賣成功1002,到底最終的結果是什么呢?讓我們運行測試程序。報了一個異常:
Caused by: com.mysql.jdbc.MysqlDataTruncation: Data truncation: Out of range value for column 'balance' at row 1這句報異常就是由于在建表的時候把balance設置為unsigned的,使之不能成為負數。這不是我們關心的,我們關心的是數據庫中庫存和賬戶余額是否發生變化。我們刷新數據庫表,發現余額沒有改變,兩本書都沒有買成功。為什么會這樣呢?我們再來分析一下。如果事務的傳播行為是默認值的話,即我們沒有在@Transactional注解里面設置,默認值就是REQUIRED,也就說是會使用checkout()方法原來的事務,雖然我們在purchase()上面也添加了事務,但是由于事務的傳播行為是默認值,所以他會使用checkout()方法的事務,如果使用checkout()方法的事務,我們發現,在ArrayList里面有兩本圖書,買兩本書調用的都是同一個purchase()方法,兩次調用是在同一個事務里面,但是買完1001之后,再去買1002,失敗了,根據事務的原子性,要么都完成,要么都不完成,所以,它要回滾事務,最終才造成了上面的結果。
那么,我們能不能讓它買成功一本呢?可以,只需要把purchase()方法的事務傳播行為改為REQUIRES_NEW。
@Transactional(propagation = Propagation.REQUIRES_NEW)同樣運行測試程序,還是報“Data truncation: Out of range value for column 'balance' at row 1”異常,不管他,我們刷新數據庫,觀察賬戶余額,發現變為了40,再看一下庫存,1001的庫存變為了99,1002的庫存沒有變還是100。這就說明我們買成功了一本。由于我們把purchase()方法的事務傳播行為改為REQUIRES_NEW,就是每次調用都要開啟一個新事物,雖然checkout()也設置了事務,但是我不用你的,每次都用我自己的,這就是事務之間的隔離性,互相之間沒有影響,所以我們買1001和買1002的時候用到的就不是同一個事務了,購買1002失敗不會導致購買1001也失敗。所以最終的結果就是1001買成功了,1002沒有買成功。
小總結:
- REQUIRED傳播行為:當bookService的purchase()方法被另外一個事務方法checkout()調用時,它會默認在現有的事務內運行。因此在checkout()方法的開始和結束內只有一個事務,這個事務只會在checkout()方法調用結束時被提交,那就導致用戶一本都買不了。
- REQUIRES_NEW傳播行為:表示該方法必須啟動一個新的事務,并在自己的事務內運行,如果已經有在運行,就先把他掛起。
?
2、事務的隔離級別
在講事務的隔離級別之前,我們先來看一下數據庫事務并發問題:
假設現在有兩個事務:Transaction01和Transaction02并發執行。
①臟讀:當前事務讀到了其他事務更新但是還沒有提交的值(其他事務不回滾還好,其他事務回滾你讀到的就是一個無效值)。
②不可重復讀:
③幻讀:
事務的隔離級別:數據庫系統必須具有隔離并發運行各個事務的能力,使它們不會相互影響,避免各種并發問題。一個事務與其他事務隔離的程度成為事務的隔離級別。SQL標準中規定了多種事務隔離級別,不同隔離級別對應不同的干擾程度,隔離級別越高,數據一致性就越好,但是并發性就越弱。
1、讀未提交:READ UNCOMMITTED,允許Transaction01讀取Transaction02未提交的修改。(臟讀、不可重復讀、幻讀都有可能出現)
2、讀已提交:READ COMMITTED,要求Transaction01只能讀取Transaction02已經提交的修改。(臟讀就可以避免了)
3、可重復讀:REPEATABLE READ,確保Transaction01可以多次從一個字段讀取到相同的值,即Transaction01執行期間禁止其他事務對這個字段進行更新。(臟讀、不可重復讀都不會出現了)
4、串行化:SERIALIZABLE,確保Transaction01可以多次從一個表中讀取到相同的行,在Transaction01執行期間,禁止其他事務對這個表進行添加、更新、刪除操作。可以避免所有并發問題,但是性能最低。(臟讀、不可重復讀、幻讀都不可能出現)
各數據庫產品對事務隔離級別的支持程度:
總結
以上是生活随笔為你收集整理的spring 事务隔离级别和传播行为_Java工程师面试1000题146-Spring数据库事务传播属性和隔离级别...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: etc卡能拔出来吗
- 下一篇: 信用卡积分兑换要谨慎 并不是实打实的优惠