Spring 事务传播特性和隔离级别
Spring 事务传播特性和隔离级别
事务是处理逻辑原子性的保证,作为单个逻辑单元执行一系列操作,要么执行完成要么全部不执行。事务遵循ACID四个特性。
事务的两个重要特性是,事务的传播特性和事务的隔离级别特性。传播级别决定了事务的控制范围,事务隔离级别决定了事务在数据库读写方面的控制范围。
- 原子性:事务作为一个原子整体,要么执行要么完全不执行
- 一致性:事务保证数据库状态从一个一致性变为另一个一致性
- 隔离性:一个事务的执行不影响其他事务的执行
- 持久性:已经提交事务对数据的修改,应该永久保存在数据库
事务的传播级别
- PROPAGATION_REQUIRED:Spring的默认传播级别,如果上下文中存在事务则加入当前事务,如果不存在事务则新建事务执行。
- PROPAGATION_SUPPORTS:如果上下文中存在事务则加入当前事务,如果没有事务则以非事务方式执行。
- PROPAGATION_MANDATORY:该传播级别要求上下文中必须存在事务,否则抛出异常。
- PROPAGATION_REQUIRES_NEW:该传播级别每次执行都会创建新事务,并同时将上下文中的事务挂起,执行完当前线程后再恢复上下文中事务。(子事务的执行结果不影响父事务的执行和回滚)
- PROPAGATION_NOT_SUPPORTED:当上下文中有事务则挂起当前事务,执行完当前逻辑后再恢复上下文事务。(降低事务大小,将非核心的执行逻辑包裹执行。)
- PROPAGATION_NEVER:该传播级别要求上下文中不能存在事务,否则抛出异常。
- PROPAGATION_NESTED:嵌套事务,如果上下文中存在事务则嵌套执行,如果不存在则新建事务。(save point概念)
事务隔离级别
- 脏读:读取到了别的事务回滚前的数据,例如B事务修改数据库X,在未提交前A事务读取了X的值,而B事务发生了回滚。
- 不可重复读:一个事务在两次读取同一个数据的值不一致。例如A事务读取X,在中间过程中B事务修改了X的值,事务A再次读取X时值发生了改变。
- 幻读:查询得到的数据条数发生了改变,例如A事务搜索数据时有10条数据,在这时B事务插入了一条数据,A事务再搜索时发现数据有11条了。
数据隔离级别
- read-uncommitted:未提交读(脏读、不可重复读、幻读)
- read-committed:已提交读(不可重复读、幻读),大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。
- repeatable-read:可重复读(幻读),保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。
- serializable:串行化最严格的级别,事务串行执行,资源消耗最大
Spring事务传播和隔离级别配置
@Transactional(propagation=Propagation.REQUIRED,rollbackFor=Exception.class,timeout=1,isolation=Isolation.DEFAULT)
- 事务的传播性:@Transactional(propagation=Propagation.REQUIRED) 如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
- 事务的超时性:@Transactional(timeout=30) //默认是30秒
- 事务的隔离级别:@Transactional(isolation = Isolation.READ_UNCOMMITTED)
- 回滚:指定异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
- 只读:@Transactional(readOnly=true)该属性用于设置当前事务是否为只读事务,设置为true表示只读
深入理解事务隔离
未提交读(Read uncommitted)
未提交读(READ UNCOMMITTED)是最低的隔离级别。通过名字我们就可以知道,在这种事务隔离级别下,一个事务可以读到另外一个事务未提交的数据。
未提交读的数据库锁情况(实现原理)
事务在读数据的时候并未对数据加锁。
务在修改数据的时候只对数据增加[行级共享锁]。
现象:
事务1 读取某行记录时,事务2也能对这行记录进行读取、更新(因为事务一并未对数据增加任何锁)
事务2 对该记录进行更新时,事务1再次读取该记录,能读到事务2对该记录的修改版本(因为事务二只增加了共享读锁,事务一可以再增加共享读锁读取数据),即使该修改尚未被提交。
事务1 更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。(因为事务一对数据增加了共享读锁,事务二不能增加[排他写锁]进行数据的修改)
举例
下面还是借用我在[数据库的读现象浅析]一文中举的例子来说明在未提交读的隔离级别中两个事务之间的隔离情况。 image.png事务一共查询了两次,在两次查询的过程中,事务二对数据进行了修改,并未提交(commit)。但是事务一的第二次查询查到了事务二的修改结果。在数据库的读现象浅析中我们介绍过,这种现象我们称之为[脏读]。
所以,未提交读会导致[脏读]
提交读(Read committed)
提交读(READ COMMITTED)也可以翻译成读已提交,通过名字也可以分析出,在一个事务修改数据过程中,如果事务还没提交,其他事务不能读该数据。
提交读的数据库锁情况
事务对当前被读取的数据加 行级共享锁(当读到时才加锁),一旦读完该行,立即释放该行级共享锁;
事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。
现象:
事务1在读取某行记录的整个过程中,事务2都可以对该行记录进行读取(因为事务一对该行记录增加行级共享锁的情况下,事务二同样可以对该数据增加共享锁来读数据)。
事务1读取某行的一瞬间,事务2不能修改该行数据,但是,只要事务1读取完改行数据,事务2就可以对该行数据进行修改。(事务一在读取的一瞬间会对数据增加共享锁,任何其他事务都不能对该行数据增加排他锁。但是事务一只要读完该行数据,就会释放行级共享锁,一旦锁释放,事务二就可以对数据增加排他锁并修改数据)
事务1更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。(事务一在更新数据的时候,会对该行数据增加排他锁,知道事务结束才会释放锁,所以,在事务二没有提交之前,事务一都能不对数据增加共享锁进行数据的读取。所以,提交读可以解决
脏读
的现象)
举例
image.png在提交读隔离级别中,在事务二提交之前,事务一不能读取数据。只有在事务二提交之后,事务一才能读数据。
但是从上面的例子中我们也看到,事务一两次读取的结果并不一致,所以提交读不能解决。
简而言之,提交读这种隔离级别保证了读到的任何数据都是提交的数据,避免了脏读(dirty reads)。但是不保证事务重新读的时候能读到相同的数据,因为在每次数据读完之后其他事务可以修改刚才读到的数据。
可重复读(Repeatable reads)
可重复读(REPEATABLE READS),由于提交读隔离级别会产生不可重复读的读现象。所以,比提交读更高一个级别的隔离级别就可以解决不可重复读的问题。这种隔离级别就叫可重复读(这名字起的是不是很任性!)
可重复读的数据库锁情况
事务在读取某数据的瞬间(就是开始读取的瞬间),必须先对其加 行级共享锁,直到事务结束才释放;
事务在更新某数据的瞬间(就是发生更新的瞬间),必须先对其加 行级排他锁,直到事务结束才释放。
现象
事务1在读取某行记录的整个过程中,事务2都可以对该行记录进行读取(因为事务一对该行记录增加行级共享锁的情况下,事务二同样可以对该数据增加共享锁来读数据)。
事务1在读取某行记录的整个过程中,事务2都不能修改该行数据(事务一在读取的整个过程会对数据增加共享锁,直到事务提交才会释放锁,所以整个过程中,任何其他事务都不能对该行数据增加排他锁。所以,可重复读能够解决
不可重复读
的读现象)事务1更新某行记录时,事务2不能对这行记录做更新,直到事务1结束。(事务一在更新数据的时候,会对该行数据增加排他锁,知道事务结束才会释放锁,所以,在事务二没有提交之前,事务一都能不对数据增加共享锁进行数据的读取。所以,提交读可以解决
脏读
的现象)
举例
image.png在上面的例子中,只有在事务一提交之后,事务二才能更改该行数据。所以,只要在事务一从开始到结束的这段时间内,无论他读取该行数据多少次,结果都是一样的。
从上面的例子中我们可以得到结论:可重复读隔离级别可以解决不可重复读的读现象。但是可重复读这种隔离级别中,还有另外一种读现象他解决不了,那就是[幻读]。看下面的例子:
上面的两个事务执行情况及现象如下:
-
事务一的第一次查询条件是
age BETWEEN 10 AND 30;
如果这是有十条记录符合条件。这时,他会给符合条件的这十条记录增加行级共享锁。任何其他事务无法更改这十条记录。 -
事务二执行一条sql语句,语句的内容是向表中插入一条数据。因为此时没有任何事务对表增加[表级锁],所以,该操作可以顺利执行。
-
事务一再次执行
SELECT * FROM users WHERE age BETWEEN 10 AND 30;
时,结果返回的记录变成了十一条,比刚刚增加了一条,增加的这条正是事务二刚刚插入的那条。
所以,事务一的两次范围查询结果并不相同。这也就是我们提到的幻读。
可序列化(Serializable)是最高的隔离级别,前面提到的所有的隔离级别都无法解决的幻读,在可序列化的隔离级别中可以解决。
我们说过,产生幻读的原因是事务一在进行范围查询的时候没有增加范围锁(range-locks:给SELECT 的查询中使用一个“WHERE”子句描述范围加锁),所以导致幻读。
可序列化的数据库锁情况
事务在读取数据时,必须先对其加 表级共享锁 ,直到事务结束才释放;
事务在更新数据时,必须先对其加 表级排他锁 ,直到事务结束才释放。
现象:
事务1正在读取A表中的记录时,则事务2也能读取A表,但不能对A表做更新、新增、删除,直到事务1结束。(因为事务一对表增加了表级共享锁,其他事务只能增加共享锁读取数据,不能进行其他任何操作)
事务1正在更新A表中的记录时,则事务2不能读取A表的任意记录,更不可能对A表做更新、新增、删除,直到事务1结束。(事务一对表增加了表级排他锁,其他事务不能对表增加共享锁或排他锁,也就无法进行任何操作)
虽然可序列化解决了脏读、不可重复读、幻读等读现象。但是序列化事务会产生以下效果:
- 无法读取其它事务已修改但未提交的记录。
- 在当前事务完成之前,其它事务不能修改目前事务已读取的记录。
- 在当前事务完成之前,其它事务所插入的新记录,其索引键值不能在当前事务的任何语句所读取的索引键范围中。
四种事务隔离级别从隔离程度上越来越高,但同时在并发性上也就越来越低。之所以有这么几种隔离级别,就是为了方便开发人员在开发过程中根据业务需要选择最合适的隔离级别。