声明式事务
- spring支持编程式事务管理和声明式事务管理两种方式。
- 编程式事务以代码的方式管理事务,换句话说,事务将由开发者通过自己的代码来实现。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
- ①获取数据库连接Connection对象。
- ②取消事务的自动提交。
- ③执行操作。
- ④正常完成操作时手动提交事务。
- ⑤执行失败时回滚事务。
- ⑥关闭相关资源。
- 声明式事务是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
- 编程式事务以代码的方式管理事务,换句话说,事务将由开发者通过自己的代码来实现。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
- 声明式事务管理有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。本文将用@Transactional注解举例声明式事务。
- 创建一个maven项目,导入spring和支持aop、mysql的相关jar包:
1 | <dependency> |
- 配置类如下:
1 |
|
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。上面配置类中的DataSourceTransactionManager(用于在应用程序中只需要处理一个数据源,而且通过JDBC存取的情况)即为其中一种事务管理器,其抽象层是PlatformTransactionManager,有关继承树如图:
PlatformTransactionManager接口还有其它不同的实现类:
- 测试连接数据库和操作数据库的jdbcTemple对象:
1 | public class TxTest { |
如果打印如下结果则说明成功:
1 | com.mysql.cj.jdbc.ConnectionImpl@55f616cf |
在mysql数据库新建三个表,初始数据分别如下:
book表
book_stock表
account表
创建相关dao层和service模拟业务处理:
1 |
|
- 创建测试类,执行用户买书的方法。
1 | public class TxTest { |
执行结果可想而知,由于没有在buyBooks方法中开启事务支持,而且此方法中有异常导致数据库减库存成功而用户的余额没更新成功。而当在buyBooks方法加上注解@Transactional,即开始事务,这时执行完方法后数据库中的库存表和用户账户表的数据均没有修改,即回滚成功了。
@Transactional注解的各个属性:
- ①int timeout() default -1:事务超出指定时长后自动终止并回滚。下面的例子由于在业务处理中超过了两秒,导致事务自动回滚,抛出异常TransactionTimedOutException: Transaction timed out: deadline。如下例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BookService {
BookDao bookDao;
public void buyBooks(String username,String isbn){
//减库存
bookDao.reduceStock(isbn);
//获取书的价格
int price = bookDao.getPrice(isbn);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//更新用户余额
bookDao.reduceBalance(username,price);
}
}- ②boolean readOnly() default false:设置事务为只读事务,即可以进行事务优化,readOnly=true可以加快查询速度。而如果在标了@Transactional(readOnly = true)注解的业务方法中有对数据库数据进行改动则会报错TransientDataAccessResourceException。如下例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BookService {
BookDao bookDao;
public void buyBooks(String username,String isbn){
//减库存
bookDao.reduceStock(isbn);
//获取书的价格
int price = bookDao.getPrice(isbn);
int i=10/0;
//更新用户余额
bookDao.reduceBalance(username,price);
}
}③String[] noRollbackForClassName() default {}:可以让原来回滚的异常不回滚(String全类名)
④Class<? extends Throwable>[] noRollbackFor() default {}:可以让原来回滚的异常不回滚(类名)
- 运行时异常(非检查异常):默认都回滚。
- 编译时异常(检查异常):要么try–catch,要么在方法上声明throws,且默认不回滚。
下面的例子设置了默认回滚的数学算术异常不回滚,则运行完后库存表的数据发生改变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class BookService {
BookDao bookDao;
public void buyBooks(String username,String isbn){
//减库存
bookDao.reduceStock(isbn);
//获取书的价格
int price = bookDao.getPrice(isbn);
int i=10/0;
//更新用户余额
bookDao.reduceBalance(username,price);
}
}- ⑤String[] rollbackForClassName() default {}:可以让原来不回滚的异常回滚(String全类名)
- ⑥Class<? extends Throwable>[] rollbackFor() default {}:可以让原来不回滚的异常回滚(类名)
- 下面的例子设置了默认不回滚的io异常回滚,则运行完后库存表的数据不发生改变,注意异常是以throws方式处理异常的,如果是写在try catch块里则还是不回滚。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BookService {
BookDao bookDao;
public void buyBooks(String username,String isbn) throws FileNotFoundException {
//减库存
bookDao.reduceStock(isbn);
//获取书的价格
int price = bookDao.getPrice(isbn);
new FileInputStream("/usr/a.txt");
//更新用户余额
bookDao.reduceBalance(username,price);
}
}⑦Isolation isolation() default Isolation.DEFAULT:事务的隔离级别。
+
数据库的并发问题:对于同时运行的多个事务, 当这些事务访问数据库中相同的数据时, 如果没有采取必要的隔离机制, 就会导致各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度, 隔离级别越高, 数据一致性就越好, 但并发性越弱。脏读:对于两个事务 T1, T2, T1 读取了已经被 T2 更新但还没有被提交的字段。之后, 若 T2 回滚, T1读取的内容就是临时且无效的。
不可重复读:对于两个事务T1, T2, T1 读取了一个字段, 然后 T2 更新了该字段。之后, T1再次读取同一个字段, 值就不同了。
幻读: 对于两个事务T1, T2, T1 从一个表中读取了一个字段, 然后 T2 在该表中插入了一些新的行。之后, 如果 T1 再次读取同一个表, 就会多出几行。
数据库提供的4种事务隔离级别:
测试读未提交:
①打开第一个终端,开启一个mysql会话并修改当前会话的隔离级别为“读未提交”,接着开启一个事务查询到图书表中ISBN-001图书的初始价格为100。
②打开第二个终端,开启一个mysql会话并开启一个事务将图书表中ISBN-001图书的价格更改为90,此时当前事务并未提交。
③此时在第一个终端还没提交的事务中继续查询此书的价格,发现能读取到第二个终端开启的未提交的事务更改的数据的值。
测试读已提交:
①打开第一个终端,开启一个mysql会话并修改当前会话的隔离级别为“读已提交”,接着开启一个事务查询到图书表中ISBN-001图书的初始价格为100。
②打开第二个终端,开启一个mysql会话并开启一个事务将图书表中ISBN-001图书的价格更改为90,此时当前事务并未提交。
③此时在第一个终端还没提交的事务中继续查询此书的价格,发现读取到的还是原来的值,原因是第二个终端中的事务还没提交。
④使第二个终端的事务提交。
⑤回到第一个终端发现此时才能查询到最新的价格。
测试可重复读:
①打开第一个终端,开启一个mysql会话,此时默认的隔离级别为可重复读,接着开启一个事务查询到图书表中ISBN-001图书的初始价格为100。
②打开第二个终端,开启一个mysql会话并开启一个事务将图书表中ISBN-001图书的价格更改为90,此时当前事务并未提交。
③此时第一个终端查询发现还是原来的价格。
④使第二个终端中的事务提交。
⑤回到第一个终端继续查询发现还是原来的价格。即当隔离级别为可重复读时在同一个事务中查询到的数据不会别其它事务所影响。
⑥当第一个终端中的事务提交后才能查到最新的价格。
测试并发修改同一个数据:
①打开第一个终端,开启一个mysql会话,接着开启一个事务查询到图书表中ISBN-001图书的初始价格为100,紧接着修改价格为90。
②打开第二个终端,开启一个mysql会话并将图书表中ISBN-001图书的价格更改为80,此时由于第一个终端中修改同一条数据的事务并未提交,所以排队进行等待。
③当第一个终端的事务提交后,第二个终端的事务才更改成功。
Oracle 支持的 2 种事务隔离级别:READ COMMITED, SERIALIZABLE。 Oracle 默认的事务隔离级别为: READ COMMITED 。Mysql 支持 4 种事务隔离级别。Mysql 默认的事务隔离级别为: REPEATABLE READ。
⑧Propagation propagation() default Propagation.REQUIRED:事务的传播行为,即当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring定义了7种类传播行为:
测试Propagation.REQUIRED
①BookService中开启两个事务方法,并在其中一个事务方法中模拟异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BookService {
BookDao bookDao;
public void buyBooks(String username, String isbn) {
//减库存
bookDao.reduceStock(isbn);
//获取书的价格
int price = bookDao.getPrice(isbn);
//更新用户余额
bookDao.reduceBalance(username, price);
}
public void updatePrice(String isbn,int price){
bookDao.updatePrice(isbn,price);
int i = 10 / 0;
}
}②MainService中新建一个事务方法并注入bookService组件并调用其两个事务方法。
1
2
3
4
5
6
7
8
9
10
11
public class MainService {
BookService bookService;
public void mainService() {
bookService.buyBooks("Tom", "ISBN-001");
bookService.updatePrice("ISBN-002", 80);
}
}③测试:
1
2
3
4
5
6
7public class TxTest {
public static void main(String[] args) throws SQLException, FileNotFoundException {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(TxConfig.class);
MainService bean = applicationContext.getBean(MainService.class);
bean.mainService();
}
}测试结果发现三个事务方法对数据库的操作全部回滚了,原因是bookService中的两个事务方法都设置成Propagation.REQUIRED,而它们都被MainService中的方法所调用(此方法也开启了事务),导致这三个方法公用一个事务,一旦一个地方执行出错则全部回滚。(注意:由于两个小事务和大事务公用一个事务,则两个小事务设置的其它属性不生效,其共有的属性应该在大事务中设置)
测试Propagation.REQUIRES_NEW
①在上面测试Propagation.REQUIRED的基础上将BookServive中的buyBooks方法设置成Propagation.REQUIRES_NEW。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BookService {
BookDao bookDao;
public void buyBooks(String username, String isbn) {
//减库存
bookDao.reduceStock(isbn);
//获取书的价格
int price = bookDao.getPrice(isbn);
//更新用户余额
bookDao.reduceBalance(username, price);
}
public void updatePrice(String isbn,int price){
bookDao.updatePrice(isbn,price);
int i = 10 / 0;
}
}②通过测试发现只有含算术异常的updatePrice方法中对数据库的操作被回滚了,而不影响无任何异常的buyBooks方法,原因是buyBooks方法被设置成Propagation.REQUIRES_NEW,即在任何情况下都会独立开启一个新事务而不会受到外边事务的影响。(注意:如果这里将buyBooks方法设置成Propagation.REQUIRED而updatePrice方法设置成Propagation.REQUIRES_NEW,其它地方不变,则会发现所有方法都回滚了,是因为拥有异常的updatePrice方法即使设置新开一个事务,但其一旦运行出错会被外界感知到,导致全部回滚)
注意:如果是本类中的事务方法相互调用,则它们同为一个事务。如下测试案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class BookService {
BookDao bookDao;
public void buyBooks(String username, String isbn) throws InterruptedException {
//减库存
bookDao.reduceStock(isbn);
//获取书的价格
int price = bookDao.getPrice(isbn);
//更新用户余额
bookDao.reduceBalance(username, price);
}
public void updatePrice(String isbn, int price) {
bookDao.updatePrice(isbn, price);
}
public void mainService() throws InterruptedException {
buyBooks("Tom", "ISBN-001");
updatePrice("ISBN-002", 80);
int i = 10 / 0;
}
}
public class TxTest {
public static void main(String[] args) throws SQLException, FileNotFoundException, InterruptedException {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(TxConfig.class);
BookService bean = applicationContext.getBean(BookService.class);
System.out.println(bean.getClass());//class com.example.tx.service.BookService$$EnhancerBySpringCGLIB$$5d08faed
bean.mainService();
}
}测试结果发现两个新开的小事务方法都回滚了。