Spring MVC系列-(6) 聲明式事務

Spring.png

6 聲明式事務

6.1 Spring中事務的使用

在進行數據操作事,通常會將多條SQL語句作爲整體進行操作,這一條或者多條SQL語句就稱爲數據庫事務。數據庫事務可以確保該事務範圍內的所有操作都可以全部成功或者全部失敗。如果事務失敗,那麼效果就和沒有執行這些SQL一樣,不會對數據庫數據有任何改動。

事務是恢復和併發控制的基本單位。

事務應該具有4個屬性:原子性、一致性、隔離性、持久性。這四個屬性通常稱爲ACID特性。

  • 原子性(atomicity)。一個事務是一個不可分割的工作單位,事務中包括的操作要麼都做,要麼都不做。
  • 一致性(consistency)。事務必須是使數據庫從一個一致性狀態變到另一個一致性狀態。一致性與原子性是密切相關的。
  • 隔離性(isolation)。一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的數據對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾。
  • 持久性(durability)。持久性也稱永久性(permanence),指一個事務一旦提交,它對數據庫中數據的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響。

Spring中可以通過@Transactional註解,實現了對事務的支持。

首先定義配置類,配置類中創建了數據源,封裝了jdbcTemplate和事務管理器。

@Configuration
@ComponentScan("com.enjoy.cap11")
@EnableTransactionManagement  //開啓事務管理功能,對@Transactional起作用
public class Cap11MainConfig {
	//創建數據源
	@Bean
	public DataSource dataSource() throws PropertyVetoException{
		//這個c3p0封裝了JDBC, dataSource接口的實現
		ComboPooledDataSource dataSource = new ComboPooledDataSource();
		dataSource.setUser("root");
		dataSource.setPassword("xxxxx");
		dataSource.setDriverClass("com.mysql.jdbc.Driver");
		dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/Spring?useSSL=false");
		return dataSource;
	}
	
	//jdbcTemplate能簡化增刪改查的操作
	@Bean
	public JdbcTemplate jdbcTemplate() throws PropertyVetoException{
		return new JdbcTemplate(dataSource());
	}
	//註冊事務管理器
	@Bean
	public PlatformTransactionManager platformTransactionManager() throws PropertyVetoException{
		return new DataSourceTransactionManager(dataSource());
	}
}

新建Order測試表:

CREATE TABLE `order` (
  `orderid` int(11) DEFAULT NULL,
  `ordertime` datetime DEFAULT NULL,
  `ordermoney` decimal(20,0) DEFAULT NULL,
  `orderstatus` char(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8

新建OrderDao操作數據庫,

@Repository
public class OrderDao {
	@Autowired
	private JdbcTemplate jdbcTemplate;
	//操作數據的方法
	public void insert(){
		String sql = "insert into `order` (ordertime, ordermoney, orderstatus) values(?,?,?)";
		jdbcTemplate.update(sql,new Date(),20,0);
	}
}

新建 OrderService類,將orderDao注入進來

@Service
public class OrderService {
    @Autowired
	private OrderDao orderDao;
    @Transactional
    public void addOrder(){
    	orderDao.insert();
    	System.out.println("操作完成.........");
    	
    	//int a = 1/0;
    }
}

在下面的測試用例中,正常的向數據庫中插入一條數據,查詢數據庫可以發現插入正常。

public class Cap11Test {
	@Test
	public void test01(){
		AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(Cap11MainConfig.class);
		
		OrderService bean = app.getBean(OrderService.class);
		bean.addOrder();
		
		app.close();
	}
}

但是接着測試,在addOrder方法中手動設置一個異常,下面的代碼中,在運行時會拋出除數爲0的異常。從運行結果可以看到,這種情況下數據庫的插入操作沒有成功,說明Spring對insert操作進行了回滾,保證了事務的一致性。

@Service
public class OrderService {
    @Autowired
	private OrderDao orderDao;
    @Transactional
    public void addOrder(){
    	orderDao.insert();
    	System.out.println("操作完成.........");
    	
    	int a = 1/0;
    }
}

6.2 Spring事務原理分析

在上面的例子中,爲了使事務能夠生效,需要加上@EnableTransactionManagement註解,整個源碼實現和AOP原理一致,在註冊Bean時對對象進行包裝,生成增強的Bean,返回代理對象。在執行階段,利用事務攔截器來運行有事務註解的代碼,當出現異常時進行回滾。

通過@EnableTransactionManagement引入的class可以看到,默認PROXY模式下,會引入AutoProxyRegistrar.classProxyTransactionManagementConfiguration.class,下面分析這兩個組件的功能。

Screen Shot 2020-02-17 at 10.14.19 PM.png

AutoProxyRegistrar.class

從下面的代碼可以看到,和AOP類似,該組件會往容器中註冊InfrastructureAdvisorAutoProxyCreator,利用後置處理器機制在對象創建以後,包裝對象,返回一個代理對象(增強器),代理對象執行方法利用攔截器鏈進行調用。

Screen Shot 2020-02-17 at 10.25.28 PM.png

ProxyTransactionManagementConfiguration.class

事務增強器要用事務註解的信息,AnnotationTransactionAttributeSource解析事務註解。

Screen Shot 2020-02-17 at 11.27.29 PM.png

攔截執行流程

和AOP類似,在攔截執行的時候,首先會獲取攔截鏈,然後依次執行攔截器的proceed方法。

事務攔截器是TransactionInterceptor,它也是MethodInterceptor的子類,下面是其執行時的主要邏輯,歸納可以分爲如下幾步:

  1. 先獲取事務相關的屬性
  2. 再獲取PlatformTransactionManager,如果事先沒有添加指定任何transactionmanger
    最終會從容器中按照類型獲取一個PlatformTransactionManager;
  3. 執行目標方法
  • 如果異常,獲取到事務管理器,利用事務管理回滾操作;
  • 如果正常,利用事務管理器,提交事務。

Screen Shot 2020-02-17 at 11.25.32 PM.png

6.3 Spring的事務隔離級別與傳播性

隔離級別

隔離性(Isolation)作爲事務特性的一個關鍵特性,它要求每個讀寫事務的對象對其他事務的操作對象能相互分離,即該事務提交前對其他事務都不可見,在數據庫層面都是使用鎖來實現。

在辨析不同的隔離級別之前,引入幾個基本概念:

1. 髒讀 :髒讀就是指當一個事務正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務也訪問這個數據,然後使用了這個數據。

2. 不可重複讀 :是指在一個事務內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務也訪問該同一數據。那麼,在第一個事務中的兩次讀數據之間,由於第二個事務的修改,那麼第一個事務兩次讀到的的數據可能是不一樣的。這樣就發生了在一個事務內兩次讀到的數據是不一樣的,因此稱爲是不可重複讀。簡單來講就是,事務 A 讀取了事務 B 已提交的更改數據。

3. 幻讀 : 是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行。 同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那麼,以後就會發生操作第一個事務的用戶發現表中還有沒有修改的數據行,就好象 發生了幻覺一樣。簡單來講就是,事務 A 讀取了事務 B 已提交的新增數據。

事務的隔離級別從低到高有以下四種:

  • READ UNCOMMITTED(未提交讀):這是最低的隔離級別,其含義是允許一個事務讀取另外一個事務沒有提交的數據。READ UNCOMMITTED是一種危險的隔離級別,在實際開發中基本不會使用,主要是由於它會帶來髒讀問題。

髒讀對於要求數據一致性的應用來說是致命的,目前主流的數據庫的隔離級別都不會設置成READ UNCOMMITTED。不過髒讀雖然看起來毫無用處,但是它主要優點是併發能力高,適合那些對數據一致性沒有要求而追求高併發的場景。

  • READ COMMITTED(讀寫提交): 它是指一個事務只能讀取另外一個事務已經提交的數據,不能讀取未提交的數據。READ COMMITTED會帶來不可重複讀的問題。

一般來說,不可重複讀的問題是可以接受的,因爲其讀到的是已經提交的數據,本身並不會帶來很大的問題。因此,很多數據庫如(ORACLE,SQL SERVER)將其默認隔離級別設置爲READ COMMITTED,允許不可重複讀的現象。

  • REPEATABLE READ (可重複讀):對相同字段的多次讀取的結果是一致的,除非數據被當前事務本身改變。可防止髒讀和不可重複讀,但幻影讀仍可能發生。

  • SERIALIZABLE(串行化):數據庫最高的隔離級別,它要求所有的SQL都會按照順序執行,這樣可以克服上述所有隔離出現的各種問題,能夠完全包住數據的一致性。

Spring中可以配置5種隔離級別:

DEFAULT(-1),  ## 數據庫默認級別
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);

可以使用類似下面的註解,很方便的配置隔離級別:

@Transactional(isolation = Isolation.SERIALIZABLE)
public int insertUser(User user){
    return userDao.insertUser(user);
}

上面的代碼中我們使用了串行化的隔離級別來包住數據的一致性,這使它將阻塞其他的事務進行併發,所以它只能運用在那些低併發而又需要保證數據一致性的場景下。

傳播行爲

在Spring中,當一個方法調用另外一個方法時,可以讓事務採取不同的策略工作,如新建事務或者掛起當前事務等,這便是事務的傳播行爲。

在Spring的事務機制中對數據庫存在7種傳播行爲,通過枚舉類Propagation定義。

public enum Propagation {
    /**
     * 需要事務,默認傳播性行爲。
     * 如果當前存在事務,就沿用當前事務,否則新建一個事務運行子方法
     */
    REQUIRED(0),
    /**
     * 支持事務,如果當前存在事務,就沿用當前事務,
     * 如果不存在,則繼續採用無事務的方式運行子方法
     */
    SUPPORTS(1),
    /**
     * 必須使用事務,如果當前沒有事務,拋出異常
     * 如果存在當前事務,就沿用當前事務
     */
    MANDATORY(2),
    /**
     * 無論當前事務是否存在,都會創建新事務允許方法
     * 這樣新事務就可以擁有新的鎖和隔離級別等特性,與當前事務相互獨立
     */
    REQUIRES_NEW(3),
    /**
     * 不支持事務,當前存在事務時,將掛起事務,運行方法
     */
    NOT_SUPPORTED(4),
    /**
     * 不支持事務,如果當前方法存在事務,將拋出異常,否則繼續使用無事務機制運行
     */
    NEVER(5),
    /**
     * 在當前方法調用子方法時,如果子方法發生異常
     * 只回滾子方法執行過的SQL,而不回滾當前方法的事務
     */
    NESTED(6);
}

日常開發中基本只會使用到REQUIRED(0),REQUIRES_NEW(3),NESTED(6)三種。

NESTEDREQUIRES_NEW是有區別的。NESTED傳播行爲會沿用當前事務的隔離級別和鎖等特性,而REQUIRES_NEW則可以擁有自己獨立的隔離級別和鎖等特性。

NESTED的實現主要依賴於數據庫的保存點(SAVEPOINT)技術,SAVEPOINT記錄了一個保存點,可以通過ROLLBACK TO SAVEPOINT來回滾到某個保存點。如果數據庫支持保存點技術時就啓用保存點技術;如果不支持就會新建一個事務去執行代碼,也就相當於REQUIRES_NEW。

Transactional自調用失效

如果一個類中自身方法的調用,我們稱之爲自調用。如一個訂單業務實現類OrderServiceImpl中有methodA方法調用了自身類的methodB方法就是自調用,如:

@Transactional
public void methodA(){
    for (int i = 0; i < 10; i++) {
        methodB();
    }
}
    
@Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
public int methodB(){
    ......
}

在上面方法中不管methodB如何設置隔離級別和傳播行爲都是不生效的。即自調用失效。

這主要是由於@Transactional的底層實現原理是基於AOP實現,而AOP的原理是動態代理,在自調用的過程中是類自身的調用,而不是代理對象去調用,那麼就不會產生AOP,於是就發生了自調用失敗的現象。

要克服這個問題,有2種方法:

  • 編寫兩個Service,用一個Service的methodA去調用另外一個Service的methodB方法,這樣就是代理對象的調用,不會有問題;
  • 在同一個Service中,methodA不直接調用methodB,而是先從Spring IOC容器中重新獲取代理對象OrderServiceImpl,獲取到後再去調用methodB。說起來有點亂,還是show you the code。
public class OrderServiceImpl implements OrderService,ApplicationContextAware {
    private ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Transactional
    public void methodA(){
        OrderService orderService = applicationContext.getBean(OrderService.class);
        for (int i = 0; i < 10; i++) {
            orderService.methodB();
        }
    }

    @Transactional(isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRES_NEW)
    public int methodB(){
        ......
    }
}

上面代碼中我們先實現了ApplicationContextAware接口,然後通過applicationContext.getBean()獲取了OrderService的接口對象。這個時候獲取到的是一個代理對象,也就能正常使用AOP的動態代理了。


參考:


本文由『後端精進之路』原創,首發於博客 http://teckee.github.io/ , 轉載請註明出處

搜索『後端精進之路』關注公衆號,立刻獲取最新文章和價值2000元的BATJ精品面試課程

後端精進之路.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章