1.什么是数据库事务,几种隔离等级下的数据库事务介绍
2.什么是事务的传播行为?几种传播行为的特点测试
1.数据库事务介绍,几种隔离等级下的事务特点
一、什么是数据库事务
事务是一系列作为一个逻辑单元来执行的操作集合。它是数据库维护数据一致性的单位,它将数据库从一致状态转变为新的一致状态,指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。举个例子,张三通过网上银行向李四转账1000块钱,但是期间由于网络原因,张三转账的操作成功了,但是李四的网络不好,倒置没有收到这笔钱,那么这个时候我们就不应该让张三账户上少这1000块钱,因为收款这一操作失败了,整个事务(转账和收款)都要失败,恢复到最初的状态。
事务有以下几个特性:
- 原子性:即一个事务当中的多个操作是不可分割的,它们被作为一个整体对待。
- 一致性:一个事务中的操作的状态必须保持一致,一个操作失败了,那么其它的操作都要失败,要么都成功,要么都失败。
- 隔离性:多个事务之间要相互隔离,不应该被其它事务打扰到,数据库提供了几种事务的隔离级别,稍后介绍。
- 持久性:事务一旦提交到数据库中,那么这个操作就是永久的,对数据库的数据的改变就是永久的。
二、数据库的隔离级别
在介绍数据库的隔离级别之前,我们先要了解并发地对数据库进行操作时会有什么问题出现:
- 脏读:即读取到在数据库事务中还没有被提交地数据。
- 不可重复读:第一次读到的数据库数据与第二次读到的数据不一致。请注意,这个问题大部分时候可以允许的,因为不会对我们的操作产生什么影响。
- 幻读:往数据库插入或者删除记录时,前后两次获取的记录数不一致。这个问题在大部分条件下也是允许出现的。
相应的,数据库的隔离级别可以一一对应的解决上述问题:
- 读未提交(READ UNCOMMITTED):在该隔离级别下,将读取到没有被提交的数据。我们可以实验一下:
开启一个数据库会话,将隔离等级设置到read uncommitted,然后开启事务。将一个字段price的值由原来的200改为300。但是注意,我们没有提交事务。
打开另一个数据库会话,设置隔离等级为读未提交。开启事务,再去读price字段的值,发现读到了300,即读到了没有被提交的数据。
此隔离等级下没有解决任何的并发读写问题。
- 读已提交(read committed):在此隔离等级下,只有被提交的事务的数据才能被我们正确的读取到。同样的开启测试验证我们的想法。
设置隔离级别是读已提交,将price修改位300,但是注意,我们没有提交事务!!!
打开另一个数据库会话,跟上述设置一样。同时读取被修改字段的值。
可以发现,我们读到的price=200,没有改变。因为修改price的事务没有提交,接下来我们提交。
再去读取一次该字段的值:
发现这个时候读取到的数据已经改变了。该隔离等级下可以解决脏读问题。
-
可重复读(repeatable read):在该隔离等级下,读取到的数据值与第一次读到的值保持一致。即使被修改了,也不会影响我们读到的字段值。
开启测试,验证是否正确。
由于mysql数据库的默认隔离等级就是可重复读,所以我们不需要再去设置隔离等级。
开启事务,读取到price的值是200。打开另一个会话,开启事务,修改price的值是400并且提交。再读一次。
发现读到的值仍然是200,这就验证了上述的论述。该隔离登记下,可以解决不可重复度的问题。 -
serializable(串行化):在该隔离登记下,将解决上述的所有问题,但是速度非常漫,效率低下。
2.什么是事务的传播行为和几种传播类型的实验。
一、事务的传播行为定义
当多个有事务的方法互相调用时,这些事务如何在这些方法之间传播,这些事务的方法该如何运行。是不是很抽象?那么我们可以通过接下来的实验来帮助我们理解什么是传播行为。
先了解一下传播行为的分类:
- REQUIRED:即当一个事务在另一个事务的方法内运行时,如果子事务有事务,那么就在当前方法运行,如果没有,那么子事务就会新建一个事务在方法内运行。
- REQUIRES_NEW:当前方法必须开启新的事务,并把原来的方法暂时挂起
- SUPPORTS:如果有事务在运行,那么就运行在事务内,否则可以不运行在事务内
- NOT_SUPPORTS:当前方法不应该运行在事务中,如果有事务,那么立即将它挂起
- MANDATORY:当前方法必须运行在事务内部,如果违反了上述原则,那么抛出异常,与3对比
最常用的就是REQUIRED和REQUIRES_NEW。
那么就来进行测试吧!
@Transactional(propagation = Propagation.REQUIRED) //告诉Spring这个方法要添加事务
public void checkout(String username,String isbn){
bookDao.updateStock(isbn);
int price = bookDao.getPrice(isbn);
bookDao.reduceBalance(username, price);
System.out.println("结账成功");
}
@Transactional(propagation = Propagation.REQUIRED)
public void updatePrice(String isbn,int price){
bookDao.updatePrice(isbn,price);
System.out.println(this.getClass());
}
设置两个方法的传播方式都是REQUIRED,在一个新的事务方法中运行。一个用来更新价格,一个用来结账(余额减少,书库存减少)
操作之前的数据库字段值:
@Transactional
public void mulTransaction(){
bookService.updatePrice("ISBN-002", 200);
bookService.checkout("Tom", "ISBN-002");
}
创建测试方法运行mulTransaction()方法,结果如下:
两个事务方法都执行成功。
下面我们在mulTransaction方法内设置异常,查看结果。(这个操作不修改上述修改后的表).
没有发生改变。
原因:
因为我们设置更新价格和结账的事务的传播行为都是REQUIRED,所以它们搭载的都是同一个事务,一个失败,全部都要失败。
接下来设置结账的事务传播行为是REQUIRES_NEW。保留异常,那么继续测试。
可以看到结账操作成功了,但是更新价格的操作成功不了。
原因:因为我们设置结账的操作是REQUIRES_NEW,那么就相当于开启新事务,结账操作的成败已经与整个操作的是否成功无关。
/**
* multx(){
*
* //REQUIRED
* A(){
* //REQUIRES_NEW
* B(){}
* //REQUIRED
* C(){}
* }
* //REQUIRES_NEW
* D(){
* //REQUIRED
* E(){
* //REQUIRES_NEW
* F(){
*
* 10/0; (D,F,G,E,A,C崩);
* }
* }
* //REQUIRES_NEW
* G(){}
* }
* 10/0 (A,C异常,其余成功)
* }
*/
附上一张较为复杂的事务嵌套,帮助我们理解。