数据库事务原理详解
1.事务的基本概念
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。
特点:事务是恢复和并发控制的基本单位,具有ACID特性:
原子性(Atomicity)
事务是一个不可分割的工作单位,事务执行后只有两个结果,全部成功,全部失败。
一致性(Consistency)
事务必须是数据库从一个一致性状态变为另一个一致性状态,事务执行前后,数据库都必须保持一致性。
比如转账:A用户和B用户的钱合计是800,在使用事务的前提下,A和B互相转账,不管最终转账多少次,A用户和B用户的钱合计只能是800。这就是事务的一致性。
隔离性(Isolation)
事务之间是相互隔离,互相不影响的。
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
持久性(Durability)
一旦事务执行完成,对数据库的数据操作是永久性的。
2.Spring事务的基本原理:
Spring是无法提供事务功能的,Spring事务的本质就是数据库的事务。JDBC中使用事务:
// 1.获取连接
Connection conn = DriverManager.getConection();
// 2.开启事务
conn.setAutoCommit(true/false);
// 3.CRUD
// 4.提交/回滚事务
conn.commit(); / conn.rollback();
// 5.关闭链接
conn.close();
使用Spring的事务管理后,开启事务、提交事务/回滚事务由Spring完成。
Spring以注解为例:可在配置文件中开启注解,在类或者方法使用注解@Transactional
标识。
Spring 在启动的时候会解析生成相关的bean, 这个时候会查看拥有相关注解的类和方法,并为这些类和方法生成代理,并根据@Transactional
的相关参数进行相关配置注入,在代理类中完成了(事务的开启、提交、回滚)真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
3.Spring事务的传播性:
spring事务的传播性,要面临多个事务同时存在的时候,Spring应该如何处理这些事务的行为?TransactionDefinition
中详细定义了。
public interface TransactionDefinition {
// 支持当事务,如果当前没有事务,则新建一个事务。Spring中默认的事务传播
int PROPAGATION_REQUIRED = 0;
// 支持当前事务,如果当前没有事务,就以非事务方式执行。
int PROPAGATION_SUPPORTS = 1;
// 支持当前事务,如果当前没有事务,就抛出异常。
int PROPAGATION_MANDATORY = 2;
// 新建事务,如果当前存在事务,把当前事务挂起新建的事务和被挂起的事务没有任何关系,是两个独立的事务。
// 外层事务失败回滚之后,不能回滚内层事务执行的结果,内层事务失败,外层事务,可以选择性回滚。
int PROPAGATION_REQUIRES_NEW = 3;
// 以非事务的方式执行操作,如果当前存在事务,就把当前事务挂起。
int PROPAGATION_NOT_SUPPORTED = 4;
// 以非事务的方式执行操作,如果当前存在事务,就抛出异常。
int PROPAGATION_NEVER = 5;
// 如果以活动的事务存在,则运行在一个嵌套的事务中,如果没有活动的事务,
// 则按照REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚保存点。
// 当前方法调用子方法时,如果子方法发生异常,只回滚子方法执行过的SQL,而不回滚当前方法的事务
// 具体是怎么做到的呢?
// 大部分数据库都有一个保存点的一个概念。
// 可以在一段sql语句中设置一个标志位,如果后面的代码执行出现了异常,那么也只会把数据回滚到这个标志位所对应的数据状态。
// 然后就不会再回滚了,所以在标志位之前对数据做的操作还会保留着。
// 然而并不是所有的数据库都有这个概念!
// 在Sprin中,当数据库不能支持保存点技术时,Spring就会新建一个事务去运行代码。
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
int TIMEOUT_DEFAULT = -1;
int getPropagationBehavior();
int getIsolationLevel();
int getTimeout();
boolean isReadOnly();
@Nullable
String getName();
}
4.数据库隔离级别
隔离级别 | 值 | 导致的问题 |
---|---|---|
Read-Uncommitted | 0 | 导致脏读 |
Read-Committed | 1 | 避免脏读,允许不可重复读、允许幻读 |
Repeateble-Read | 2 | 避免脏读,不可重复读、允许幻读 |
Serializable | 3 | 串行化读,事务只能同步执行,避免了脏读、不可重复读、幻读。执行效率慢,慎重! |
脏读:
A事务对A表进行了CRUD,但是未提交,B事务读取到了A事务未提交的数据,这个时候A事务执行了回滚。这样就导致了B事务读取到了脏数据。
不可重复读:
A事务对A表进行了两次读取,在A事务第一次读取和第二次读取之间,B事务对A表的数据进行了修改,这时候A事务两次读取的结果不一致。
幻读:
A事务对某类数据进行修改,这时候B事务插入了一条同类数据,A事务提交后发现还有一条数据没被修改,就好像发生了幻觉一样。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
不可重复读(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
总结:
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响越大。
SqlServlet、Oracle默认级别:Read-Commited
Mysql InnoDB默认级别:Repeatable-Read
5.Spring的隔离级别
public interface TransactionDefinition {
// 默认隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC 的隔离几倍对应
int ISOLATION_DEFAULT = -1;
// 最低的隔离级别,允许另外一个事务可以看到这个事务未提交的数据,
// 会产生 脏读、幻读、不可重复度
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
// 保证一个事务修改的数据只有提交后才可以被发现
// 另一个事务不能读取未提交的数据
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
// 这种事务隔离级别可以防止脏读,不可重复度,但是可能出现幻读
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
// 顺序执行,最高的事务隔离级别
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
}
数据库第一类、第二类丢失更新
第一类更新丢失(回滚丢失,Lost update)
如图可见:A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。
不用担心,数据库标准定义的所有隔离界别都不允许第一类丢失更新发生。
第二类更新丢失(覆盖丢失/两次更新问题,Second lost update)
第二类丢失更新,实际上和不可重复读是同一种问题。
解决方案: 基本两种思路,一种是悲观锁,另外一种是乐观锁;
悲观锁:
1、排它锁,当事务在操作数据时把这部分数据进行锁定,直到操作完毕后再解锁,其他事务操作才可操作该部分数据。这将防止其他进程读取或修改表中的数据。(大多数情况下依靠数据库的锁机制实现)
实现:select xxx from user where age = 18 for update 对所选择的数据进行加锁处理,本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。
乐观锁
1.状态机,实现:大多数基于数据版本(Version)记录机制实现。
当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断当前版本信息与第一次取出来的版本值大小,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据,拒绝更新,让用户重新操作。
6.事务的嵌套
public class Test {
class ServiceA{
public void methodA(){
ServiceB serviceB = new ServiceB();
serviceB.methodB();
}
}
class ServiceB{
public void methodB(){
}
}
}
一、ServiceB.methodB的事务级别定义为PROPAGATION_REQUIRED。
1、如果ServiceA.methodA已经起了事务,这时调用ServiceB.methodB,会共用同一个事务,如果出现异常,ServiceA.methodA和ServiceB.methodB作为一个整体都将一起回滚。
2、如果ServiceA.methodA没有事务,ServiceB.methodB就会为自己分配一个事务。ServiceA.methodA中是不受事务控制的。如果出现异常,ServiceB.methodB不会引起ServiceA.methodA的回滚
二、ServiceA.methodA的事务级别PROPAGATION_REQUIRED,ServiceB.methodB的事务级别PROPAGATION_REQUIRES_NEW,调用ServiceB.methodB,ServiceA.methodA所在的事务就会挂起,ServiceB.methodB会起一个新的事务。
1、如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB是不会回滚的。
2、如果ServiceB.methodB失败回滚,如果他抛出的异常被ServiceA.methodA的try..catch捕获并处理,ServiceA.methodA事务仍然可能提交;如果他抛出的异常未被ServiceA.methodA捕获处理,ServiceA.methodA事务将回滚。
三、ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_NESTED,调用ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起,ServiceB.methodB会起一个新的子事务并设置savepoint。
1、如果ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚,ServiceB.methodB也将回滚。
2、如果ServiceB.methodB失败回滚,如果他抛出的异常被ServiceA.methodA的try..catch捕获并处理,ServiceA.methodA事务仍然可能提交;如果他抛出的异常未被ServiceA.methodA捕获处理,ServiceA.methodA事务将回滚。
7.事务失效
1、spring的事务注解@Transactional只能放在public修饰的方法上才起作用,如果放在其他非public(private,protected)方法上,事务不起作用,因为Spring事务的实现是使用AOP来完成的,使用其他修饰符会导致生成代理类失败。
2.数据库必须是支持事务的,比如Mysql的数据引擎MyISAM就不支持事务。
3.只有抛出非受检异常(RuntimeException)事务才会回滚,但是抛出Exception,事务不回滚。可以通过(rollbackfor = Exception.class)来表示所有的Exception都回滚。
4.内部调用
不带事务的方法调用该类中带事务的方法,不会回滚。因为spring的回滚是用过代理模式生成的,如果是一个不带事务的方法调用该类的带事务的方法,通过this.xxx()调用,而不生成代理事务,所以事务不起作用。常见解决方法,拆类。
@Service
public class EmployeeService {
@Autowired
private EmployeeDao employeeDao;
public void save(){
try {
// 此处this调用不会开启事务,数据会被保存
this.saveEmployee();
}catch (Exception e){
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.PROPAGATION_REQUIRED)
// 此处无论是PROPAGATION_REQUIRED还是PROPAGATION_REQUIRES_NEW,事务均不生效
public void saveEmployee(){
Employee employee = new Employee();
employee.setName("zhangsan");
employee.setAge("26");
employeeDao.save(employee);
throw new RuntimeException();
}
}
问题原因:
在Spring中事务是通过AOP来实现的,AOP是根据被代理对象是否接口来选择使用JavaProxy 或 CGlib实现AOP,项目启动时,会扫描对应的注解来确定那些类和方法是需要增强的,只有被AOP代理增强过的方法才能实现事务,SpringIoC容器中返回的调用的对象是代理对象而不是真实的对象,这里使用的this调用的是真实对象,并非被增强过的代理对象。
解决方案:
方法1、在方法A上开启事务,方法B不用事务或默认事务,并在方法A的catch中throw new RuntimeException(),这样使用的就是方法A的事务。(一定要throw new RuntimeException();否则异常被捕捉处理,同样不会回滚。)如下:
@Transactional() //开启事务
public void save(){
try {
// 这里this调用会使事务失效,数据会被保存
this.saveEmployee();
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException();
}
}
方法2、方法A上可以不开启事务,方法B上开启事务,并在方法A中将this调用改成动态代理调用(AopContext.currentProxy()),如下:
public void save(){
try {
EmployeeService proxy =(EmployeeService) AopContext.currentProxy();
proxy.saveEmployee();
}catch (Exception e){
e.printStackTrace();
}
}