把最近几天看的和事务相关的知识梳理一下 方便自己以后复习。
一. 事务的概念
数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成(来自维基百科)。 简单点说,事务可以被看作一个单元的一系列SQL语句的集合。事务的概念来自于两个独立的需求:并发数据库访问,系统错误恢复。
二. 事务的ACID特性
1. 原子性:事务是数据库的逻辑工作单位,事务中包括的诸多操作要么都做,要么都不做。
2. 一致性:事务执行的结果必须是使数据库从一个一致性状态变成另一个一致性状态。当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。 例如数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所做的修改有一部分已写入物理数据库中,这时数据库就处于不一致性的状态。 其实一致性和原子性是紧密联系的。
3. 隔离性:一个事务的内部操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
4. 持久性: 一个事务一旦提交,它对数据库的修改就是永久的,之后发生的任何故障都不应对这个结果有影响。
三. 事务的隔离级别
1. 如果不对数据库进行并发控制,可能会产生异常:
- 丢失更新:两个事务都同时更新一行数据,但是第二个事务中途失败退出导致对数据的两次修改都失效了。这是由于系统没有执行任何锁操作 因此并发事务并没有被隔离开来。
- 脏读 :脏读就是指当A事务正在访问数据,并对数据进行修改,但这次修改的事务还没有提交到数据库中。这时,B事务恰好访问到这个已修改但未提交的数据。 之后A事务可能放弃了这次修改,或者修改成另外一个值并提交了A事务。那么B事务读取的那个值就是一个脏数据。 例如. 我现在的工资为10000, 公司财务一天将我的工资改为了20000(但未提交事务);恰好此时我去读取自己的工资 ,发现自己的工资突然变成了20000,开心的飞起!到处去跟人炫耀。然而财务发现了自己的失误,回滚了事务,我的工资又变为了10000。我刚刚读取的20000的工资就是一个脏数据。
- 不可重复读 :是指在一个事务内,多次读同一数据,在这个事务还没有结束时,另外一个事务也访问同一数据并修改提交。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,第一个事务两次读到的数据可能是不一样的,因此称为不可重复读。 同样一个案例,有一天我无聊去看自己工资,还是10000没有变化,但操作并没有完成,还在事务A里,只是执行其他操作;这时财务接到通知把我的工资改成了20000并且提交了事务B;我在事务A快结束时又去看了下自己工资,咦!怎么成20000了。
- 幻读 : 是指当事务不是独立执行时发生的一种现象,例如一个事务用where子句来检索一个表的数据,另一个事务插入一条新的记录,并且符合where条件,这样,第一个事务用同一个where条件来检索数据后,就会多出一条记录,就好像发生了幻觉一样。 还是这个案例, 有一天我去看工资是5000的小伙伴有几个人,一看7个菜鸡,此时操作并没有完成,还在事务A中;与此同时,财务接到通知把我的工资改成了10000并且提交了事务B;我在事务A里又去看一遍看工资是5000的小伙伴有几个人(可能真的实在太无聊了),一看怎么只有6个菜鸡了,少的那个还是我,这一切就像幻觉一样。
2.为了避免上面出现几种情况在标准SQL规范中定义了4个事务隔离级别,不同隔离级别对事务处理不同 。
- 读未提交(Read Uncommitted): 也称未提交读。允许脏读但不允许更新丢失。
- 读已提交(Read Committed): 也称提交读。允许不可重复读但不允许脏读。
- 可重复读(Repeatable Read): 允许幻读,但不允许不可重复读和脏读。
- 可串行化(Serializable):不允许不可重复读、脏读和幻读。
注:虽然最安全的是Serializable,但是伴随而来的也是高昂的数据库性能开销。
3. 事务隔离的实现——锁
- 共享锁(S锁):用于只读操作(SELECT),锁定共享的资源。共享锁不会阻止其他用户读,但是阻止其他的用户写和修改。
- 更新锁(U锁):用于可更新的资源中。防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁。
- 独占锁(X锁,也叫排他锁):一次只能有一个独占锁用在一个资源上,并且阻止其他所有的锁包括共享锁。写是独占锁,可以有效的防止“脏读”。
Read Uncommited:如果一个事务已经开始写数据,则另外一个事务不允许同时对这部分进行写操作,但允许其他读事务。该隔离级别可以通过“排他写锁”实现。事务隔离的最低级别,防止更新丢失,仅可保证不读取物理损坏的数据。与READ COMMITTED 隔离级相反,它允许读取已经被其它用户修改但尚未提交确定的数据,即可能出现脏读。
Read Committed:这可以通过“瞬间共享读锁”和“排他写锁”实现,读数据的事务允许存在其他事务,但是未提交的写事务则禁止任何其他事务,所以事务A和事务B对数据进行读取时总是一致的。Oracle默认的级别,在此隔离级下,SELECT 命令不会返回尚未提交(Uncommitted) 的数据,即不会出现脏读。 弊端:假设事务A在没有提交之前对数据进行修改,那么事务B读取到的某个值可能会在其读取后被A更改并提交事务,从而导致了B以相同条件再次读取时该值不可被重复获取,即不可重复读;或者当B再次用相同的where字句时得到了和前一次不一样数据的结果集,即幻读。
Repeatable Read:读取数据的事务将会禁止写事务(但允许存在并发的读事务),写事务则禁止任何其他事务。可以通过“共享读锁”和“排他写锁”实现。MySQL默认的级别,在此隔离级下,用SELECT 命令读取的数据在整个命令执行过程中不会被更改。此选项会影响系统的效能,非必要情况最好不用此隔离级。 弊端:在A事务没有结束之前,B事务可以插入新记录到表中,那么A事务再次用相同的where字句查询时,得到的结果数可能上一次的不一致,也就是幻像数据。
Serializable:读加共享锁,写加排他锁,读写互斥,以防止在事务完成之前由其他用户更新行或向数据集中插入行。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作事务访问到。事务隔离的最高级别,事务之间完全隔离。如果事务在可串行读隔离级别上运行,则可以保证任何并发重叠事务均是串行的。这是最严格的锁,也是对数据库性能开销也是最大的选择。
4. Spring事务的隔离级别
- ISOLATION_DEFAULT: 这是一个PlatfromTransactionManager默认的隔离级别,使用当前数据库默认的事务隔离级别。另外四个与标准SQL规范定义的隔离级别相对应。
- ISOLATION_READ_UNCOMMITTED: 这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
- ISOLATION_READ_COMMITTED: 保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据。
- ISOLATION_REPEATABLE_READ: 这种事务隔离级别可以防止脏读,不可重复读,但是可能出现幻像读。
- ISOLATION_SERIALIZABLE: 这是花费最高代价但是最可靠的事务隔离级别,事务被处理为串行执行。防止脏读,不可重复读,幻读。
四. Spring事务传播机制
propagation :key属性确定代理应该给哪个方法增加事务行为,这样的属性最重要的部分是传播行为。有以下选项可供使用:
- PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。 例如,ServiceB.methodB的事务级别定义为PROPAGATION_REQUIRED。那么当执行ServiceA.methodA的时候,ServiceA.methodA已经起了事务,这时调用ServiceB.methodB方法,ServiceB.methodB看到自己已经运行在ServiceA.methodA的事务内部,就不再起新的事务。但假如ServiceB.methodB运行的时候发现自己没有在事务中,他就会为自己分配一个事务。这样,在ServiceA.methodA或者在ServiceB.methodB内的任何地方出现异常,事务都会被回滚。即使ServiceB.methodB的事务已经被提交,只要ServiceA.methodA接下来发生回滚,ServiceB.methodB也要回滚。
- PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。
- PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。
- PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起,执行完当前新建事务以后,上下文事务再恢复执行。因为ServiceB.methodB是新起一个事务,那么就存在两个不同的事务。如果ServiceB.methodB已经提交,然而ServiceA.methodA失败回滚,但是ServiceB.methodB是不会回滚的。如果ServiceB.methodB失败回滚,他抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可能提交。
- PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起,执行当前逻辑,结束后恢复上下文的事务。
- PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常。
- PROPAGATION_NESTED--作为当前事务的一个嵌套子事务,被定义为“NESTED”的事务和它的父事务是相依的,它的提交是要等和它的父事务一块提交的(子事务先提交,父事务再提交)。同样,如果父事务最后回滚,它也要回滚的。如果子事务回滚,父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。Nested事务的好处就是他有一个savepoint。 如下所示: ServiceB.methodB失败回滚,那么ServiceA.methodA也会回滚到savepoint点上,ServiceA.methodA可以选择另外一个分支,比如ServiceC.methodC继续执行,来尝试完成自己的事务。
**********************************************************************************************************
ServiceA {
/**
* methodA事务传播属性配置为 PROPAGATION_REQUIRED
*/
void methodA() {
try {
savepoint;
ServiceB.methodB(); //methodB事务传播属性配置为 PROPAGATION_NESTED
} catch (Exception) {
// 执行其他业务, 例如ServiceC.methodC();
}
}
}
**********************************************************************************************************
五. 遇到的一些问题的总结(持续更新)
1. 通过注解的方式管理事务时,只有将@Transactional 注解到 public 方法,才能成功管理事务。在JAVA8中接口新增了default方法,在default方法上添加@Transactional 注解是能成功管理事务的。对此我的理解jdk1.8中接口的default方法就是public的,如果有其他的解释,请告诉我,感谢!!
2. 一个没有@Transactional 注解的方法内部调用同一个类中有@Transactional 注解的方法,有@Transactional 注解的方法的事务被忽略,不会生效。要想达到预期效果可以在这个service类中@Autowired这个类本身的对象, 用注入的这个对象去调用带@Transactional 注解的方法即可生效。
3. 不同类之间的方法调用,例如ServiceA的方法methodA()调用ServiceB的方法methodB(),只要方法methodA()或methodB()配置了事务,运行中就会开启事务,产生代理,事务正常起作用。如果A、B两个方法都配置了事务,两个事务具体以何种方式传播,取决于设置的事务传播特性。
4. 类中方法上的事务配置优先级高于类外层的事务配置。
注:产生上面2、3问题的原因主要是由于Spring AOP 代理机制导致的,因为不同类之间的方法调用实际上是使用Spring生成的代理对象去调用方法,同一个类之间可以理解为this.method()。
参考文章: http://uule.iteye.com/blog/1109647
https://hit-alibaba.github.io/interview/basic/db/Transaction.html
https://blog.csdn.net/yuanlaishini2010/article/details/45792069