1 前言
事務是一組由於邏輯上緊密關聯而合併成一個整體的多個數據操作,這些操作要麼都執行,要麼都不執行。事務有以下4個特性(ACID):
- 原子性(Atomicity):操作這些指令時,要麼全部執行成功,要麼全部不執行。只要其中一個指令執行失敗,所有的指令都執行失敗,數據進行回滾,回到執行指令前的數據狀態。
- 一致性(Consistency):事務的執行使數據從一個狀態轉換爲另一個狀態,但是對於整個數據的完整性保持穩定。
- 隔離性(Isolation):隔離性是當多個用戶併發訪問數據庫時,比如操作同一張表時,數據庫爲每一個用戶開啓的事務,不能被其他事務的操作所幹擾,多個併發事務之間要相互隔離。
- 持久性(Durability):當事務正確完成後,它對於數據的改變是永久性的。
需要導入的 jar 包如下,其中最後三個包是 JdbcTemplate 所需的 jar 包。
2 案例
首先在 MySQL 中創建數據庫:taobao,再在此數據庫中創建表:users(uid int, balance int),books(bid varchar, price int, num int),users 和 books 表中數據如下:
Dao.java
package com.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class Dao {
@Autowired
private JdbcTemplate jdbcTemplate;
public Integer queryPrice(String bid) { //查詢書的價格
Integer price=jdbcTemplate.queryForObject("select price from books where bid=?", new Object[] {bid},Integer.class);
return price;
}
public Integer queryNum(String bid) { //查詢書的庫存量
Integer num=jdbcTemplate.queryForObject("select num from books where bid=?", new Object[] {bid},Integer.class);
return num;
}
public Integer queryBalance(Integer uid) { //查詢用戶的餘額
Integer balance=jdbcTemplate.queryForObject("select balance from users where uid=?", new Object[] {uid},Integer.class);
return balance;
}
public void updateBook(String bid,Integer buy_num) { //更新書的數量
Integer num=queryNum(bid);
if(num<buy_num) {
throw new RuntimeException();
}else {
jdbcTemplate.update("update books set num=num-? where bid=?", buy_num,bid);
}
}
public void updateUser(Integer uid,Integer price,Integer buy_num) { //更新用戶的餘額
Integer balance=queryBalance(uid);
if(balance<price*buy_num) {
throw new RuntimeException();
}else {
jdbcTemplate.update("update users set balance=balance-? where uid=?", price*buy_num,uid);
}
}
}
其中,@Repository 用於將 Dao 標註爲持久層,並由 IOC 容器生成名爲 dao 的 bean;@Autowired 用於自動注入屬性。@Repository 和 @Autowired 的具體用法見通過註解配置bean。
BookService.java
package com.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
@Autowired
private Dao dao;
@Transactional
public void buyBook(Integer uid,String bid,Integer buy_num) {
Integer price=dao.queryPrice(bid);
dao.updateUser(uid, price, buy_num); //用戶扣錢
dao.updateBook(bid, buy_num); //書店減書
}
}
其中, @Service 用於將 BookService 標註爲服務層,並由 IOC 容器生成名爲 bookService 的 bean;@Transactional 用於標註 buyBook() 方法爲一個事務,使其內部操作爲一個整體,要麼全部執行,要麼全不執行(因爲可能存在用戶扣了錢,書店缺書的情況)。另外,@Transactional 可以加在方法上,也可以加在類上(相當於在類中所有方法上加了 @Transactional)。
transaction.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- 引入屬性文件 -->
<context:property-placeholder location="db.properties"/>
<!-- 創建數據源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 通過數據源配置JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 掃描組件 -->
<context:component-scan base-package="com.transaction"></context:component-scan>
<!-- 配置事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 開啓註解驅動,即對事務相關的註解進行掃描,解析含義並執行功能 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
Test.java
package com.transaction;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ac=new ClassPathXmlApplicationContext("transaction.xml");
BookService service=ac.getBean("bookService",BookService.class);
service.buyBook(1002, "b2", 1);
}
}
由於用戶 1002 有錢(100元),但書店缺書 b2(0本),因此事務沒有執行,即用戶沒有扣錢。如果 BookService.java 中沒有註解 @Transactional,即使書店缺書,用戶也會扣錢。
3 @Transactional 的屬性
@Transactional 註解後可以添加 propagation、isolation、timeout、readOnly、rollbackFor 等屬性。
3.1 propagation(事務的傳播級別)
事務傳播級別是針對嵌套事務而言的,比如:事務 A 中,嵌套了 B、C、D 3個事務。事務傳播級別定義了嵌套事務的傳播行爲,有以下7個傳播級別:
傳播級別 | 描述 |
REQUIRED | 如果上下文中已經存在事務,那麼就加入到事務中執行,如果當前上下文中不存在事務,則新建事務執行。 |
SUPPORTS | 如果上下文存在事務,則支持加入事務,如果沒有事務,則使用非事務的方式執行。 |
MANDATORY | 上下文中必須要存在事務,否則就會拋出異常。配置該方式的傳播級別是有效的控制上下文調用代碼遺漏添加事務控制的保證手段。 |
REQUIRED_NEW | 每次都會新建一個事務,並且同時將上下文中的事務掛起,執行當前新建事務完成以後,上下文事務恢復再執行。 |
NOT_SUPPORTE | 上下文中存在事務,則掛起事務,執行當前邏輯,結束後恢復上下文的事務。 |
NEVER | 上下文中不能存在事務,一旦有事務,就拋出 Runtime 異常,強制停止執行。 |
NESTED | 如果上下文中存在事務,則嵌套事務執行,如果不存在事務,則新建事務。 |
傳播級別屬性的使用方式如下:
@Transactional(propagation=Propagation.REQUIRED)
3.2 isolation(事務的隔離級別)
在訪問數據時,通常存在髒讀、不可重複讀、幻讀等問題,其釋義如下:
- 髒讀:A事務讀取B事務尚未提交的更改數據。如:B事務中有兩次取錢操作,在兩次取錢之間,A事務讀取了餘額,那麼A事務就讀取了B事務未提交的數據。
- 不可重複讀:在一個事務範圍內,多次查詢某個數據,卻得到不同的結果。如:A事務中有兩次查詢餘額,中間B事務修改了餘額並提交,那麼A事務兩次讀取的餘額數就不一樣。
- 幻讀:在一個事務範圍內,多次查詢表,得到的記錄數不一樣。如:A事務中有兩次查詢用戶表中用戶數,中間B事務插入了一條用戶記錄,那麼A事務兩次查詢的記錄數就不一樣。
針對上述問題,定義了4個事務隔離級別,如下:
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 | 鎖 |
讀未提交 READ_UNCOMMITTED |
無鎖 | |||
讀已提交 READ_COMMITTED |
鎖正在讀取的行 | |||
可重複讀 REPEATABLE_READ |
鎖所有讀取的行 | |||
串行化 SERIALIZABLE |
鎖表 |
注意:還有默認隔離級別(DEFAULT),即使用數據庫的默認值,MySQL(可重複讀)、Oracle(讀已提交)、SQL Server(讀已提交)。
隔離級別屬性的使用方式如下:
@Transactional(isolation=Isolation.READ_COMMITTED)
3.3 timeout
timeout 指定了事務在強制回滾前最多可執行(等待)的時間,使用方法如下:
@Transactional(timeout=5) //最多執行5秒
3.4 readOnly
readOnly 用於指定事務中的一系列操作是否爲只讀,若設置爲只讀,不管事務中是否有寫操作,MySQL 都會在請求訪問數據庫的時候不加鎖,以提高性能,但是,當事務中有寫操作時,一定不要設置爲只讀,否則髒讀、不可重複讀、幻讀等問題都有可能會發生。readOnly 的使用方式如下:
@Transactional(readOnly=true)
3.5 rollbackFor | rollbackForClassName | noRollbackFor | noRollbackForClassName
rollbackFor | rollbackForClassName | noRollbackFor | noRollbackForClassName 用於指定事務在遇到哪些異常時會回滾(或不會滾),使用方法如下:
//事務執行過程中,若出現空指針異常或算術異常,就會回滾,其他異常不回滾
@Transactional(rollbackFor={NullPointerException.class,ArithmeticException.class})