<!--markdown-->## 概念定义
事务(Transaction)是指一个对数据库进行一系列操作集合的统称,它是数据库执行的逻辑单元。在一个逻辑单元中可以有多个CRUD执行操作。
事务特性
事务具有四种特性,一般简称ACID特性
- 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
- 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行
- 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库
事务作用
为什么要使用事务?由于我们的业务逻辑都是由很多数据组成,每组数据的正确性依赖于其他数据的正确性,完整性,如A向B转账,如果A扣款成功,B一定要收到钱,反之B收到钱,A必须扣款,如果扣款失败,A既不扣款,B也不收款,如扣款成功,A必须扣款,B必须收款。这种业务逻辑的正确执行才能满足我们的实际需求,但是如何去保证这一逻辑的正确执行呢? 这时候就需要用到数据库事务,使用事务可以保证我们的业务逻辑正常执行,数据得到正确性,完整性,安全性。如果没有事物的话数据的完整性一致性等无法保障,我们就无法编写正确的业务逻辑。
事务原理
事务使用
- 编程式事务:就是直接在代码里手动开启事务,手动提交,手动回滚。优点就是可以灵活控制,缺点就是太麻烦了,太多重复的代码了。
- 声明式事务:就是使用SpringAop配置事务,这种方式大大的简化了编码。需要注意的是切入点表达式一定要写正确。
- 注解事务:直接在Service层的方法上面加上@Transactional注解
事务回滚
在多个crud执行中,由于数据的不正确或者异常的导致,我们对数据库提交的数据需要做撤回操作,可以利用数据库事务回滚机制来解决脏数据插入到数据库。使用事务回滚,数据库会将数据恢复到原来之前的样子,确保事务的一致性,原子性。
Spring默认情况下会对运行期例外(RunTimeException)进行事务回滚。这个例外是unchecked
如果遇到checked意外就不回滚。
如何改变默认规则:
- 让checked例外也回滚:在整个方法前加上 @Transactional(rollbackFor=Exception.class)
- 让unchecked例外不回滚: @Transactional(notRollbackFor=RunTimeException.class)
- 不需要事务管理的(只查询的)方法:@Transactional(readOnly=true) 或者不加注解
- 如果不添加rollbackFor等属性,Spring碰到Unchecked Exceptions都会回滚,不仅是RuntimeException,也包括Error
注意:
如果异常被try{}catch{}了,事务就不回滚了,如果想让事务回滚必须再往外抛
try{}catch{throw Exception}
为什么事务回滚默认指是针对未检查异常?
Checked Exception异常,这也是在编程中使用最多的Exception,所有继承自Exception并且不是RuntimeException的异常都是checked Exception,下图中的IOException和ClassNotFoundException。JAVA 语言规定必须对checked Exception作处理,编译器会对此作检查,要么在方法体中声明抛出checked Exception,要么使用catch语句捕获checked Exception进行处理,不然不能通过编译。Spring鼓励你手动去处理处理这种异常,确保不会因此出现问题。
事务回滚总结:
- 要想事务起作用,必须是主方法名上有@Transactional注解,方法体内不能用try catch;如果用try catch,则catch中必须用throw new Exception();
- @Transactional注解应该只被应用到public方法上,不要用在protected、private等方法上,即使用了也将被忽略,不起作用。这是由Spring AOP决定的
- 只有来自外部的方法调用才会被AOP代理捕捉,类内部方法调用类内部的其他方法,子方法并会不引起事务行为,即使被调用的方法上使用有@Transactional注解。
关于第三点:
在一个Service内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务。其背后原因是因为事务的实现机制是spring通过代理实现的,spring采用动态代理机制来实现事务控制,而动态代理最终都是要调用原始对象的,而原始对象在去调用方法时,是不会再触发代理了。
当然解决办法有两种:
- 把这两个方法分开到不同的类中;
使用代理对象调用当前类的方法
ServiceA proxy =(ServiceA)AopContext.currentProxy();
proxy.b();
事务隔离级别
四种事务级别:
- 读未提交(read uncommitted ):别人改数据的事务尚未提交,我在我的事务中也能读到。
- 读已提交(read committed):别人改数据的事务已经提交,我在我的事务中才能读到。
- 可重复读(repeatable read):别人改数据的事务已经提交,我在我的事务中也不去读。
- 串行提交(serializable:):事务串行化执行,后面的事务等待前面的事务执行完才能去接着执行。
mysql默认的事务隔离级别是:可重复读(repeatable read)
总结:这 4 种隔离级别,并行性能依次降低,安全性依次提高。
修改事务级别
查询当前数据库的事务隔离级别:SELECT @@tx_isolation
mysql内置四种事务:
read uncommitted
read committed
repeatable read
serializable
设置当前会话
set session transaction isolation LEVEL read uncommitted
设置全局
set global transaction isolation LEVEL read uncommitted
事务传播机制
在当前含有事务方法内部调用其他的方法(无论该方法是否含有事务),这就属于Spring事务传播机制的知识点范畴了。Spring事务基于Spring AOP
Spring的@Transactional默认是PROPAGATION_REQUIRED。
事务嵌套
事务与锁
共享锁(S锁)
用于只读操作(SELECT),锁定共享的资源。共享锁不会阻止其他用户读,但是阻止其他的用户写和修改。
更新锁(U锁)
用于可更新的资源中。防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁。
独占锁(X锁,也叫排他锁)
一次只能有一个独占锁用在一个资源上,并且阻止其他所有的锁包括共享缩。写是独占锁,可以有效的防止“脏读”。
Read Uncommited 如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
Read Committed 读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。可以通过“瞬间共享读锁”和“排他写锁”实现。
Repeatable Read 读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。可以通过“共享读锁”和“排他写锁”实现。
Serializable 读加共享锁,写加排他锁,读写互斥。