例子:拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。
例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。
1. 脏读: 脏读指的是错误的或无意的数据,这些数据可能从未存在于数据库中。假设我们有两个事务T1和T2并发运行。 现在,如果T1插入/更新了一些行,T2在T1提交之前读取这些行。T2在这里执行了脏读取,因为T1可能决定回滚/中止,并且永远不会提交,所以T2读取的内容永远不存在。
例如:在事务之前A的余额=1000T1事务开始T1读取A的余额=1000T1更新A的余额=500(可能是转给B了)T2开始T2读取A的余额=500【脏读】T1回滚
这里T2读A的余额=500是脏读,由于T1事务转500从A->B被终止了,同时T2读到了错误的A余额。考虑另一个错乱,如果T2想转800给C。看到A只有500将返回“余额不足”错误,即使此时A实际余额有1000(假设T1没发生/回滚了)。
2. 脏写: 类似脏读,脏写可能发生在T1进行时,T2写入某个值。这意味着当T1提交时,它还将提交T2的更改,T2将回滚这些更改。 导致数据库无意的写入。
例如:T1事务开始T1更新A的余额=500【可能转给B了】T2事务开始T2读取A的余额=500【由于T1已经更新了】T2更新A余额=300【可能转给C200了】【脏写】T1提交事务【提交A余额=300】T2回滚【意味着A->C转的200事务并未发生】
所以在这里,因为A→C从未发生过,只有A→B的500,所以预期的A的余额= 500,但由于脏写,A的余额被错误地更新为300。
3. 不可重复读: 当一个事务尝试多次读取数据库行并且每次都得到不同的结果时,就会发生这种情况。例如,如果T1在两次不同的时间读取DB行,并且在两次读取之间,T2更新该行。
考虑如下例子:T1事务开始T1读取A的余额=1000【第一次读】T2事务开始T2读取A的余额=1000T2写入A的余额=500【假设A->B转了500】T2事务提交T1读取A的余额=500【第二次读】问题出现
因此,顾名思义,当事务进行重复读取时,它会得到不同的值。
4. 幻读: 顾名思义,它意味着一些诡异的阅读发生了。如果T1查询某个范围的行(比如N行),则会发生这种情况,同时T2插入了与T1相同查询条件匹配的额外行。 然后,如果T1再次搜索,它将获得额外的行(幻读)。
例如:T1事务开始T1查询:select * from Table where X > 2【假设返回100行】T2事务开始T2插入一行X=150T1执行相同的查询,这次返回的结果是101行。
因此,如上所述,我们可以有上述类型的并发问题,有4个隔离级别来处理这些问题。
在讨论隔离级别之前,让我们先了解数据库的锁。1、读(共享)锁:如果T1在一行上拿到读锁,T2仍然可以读该行。这意味着T1和T2都可以在同一行上读(共享锁)。而且,由于T1持有读锁,并且“读不阻塞写”,T2仍然可以通过获取写锁来更新该行。2、写(独占)锁:如果T1持有一行的写锁,则T2不能读或写该行。(写锁阻塞读锁)。 这意味着如果在一行上设置了写锁,则没有其他事务可以读/写该行。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 可能 | 可能 | 可能 |
READ COMMITTED | 不可能 | 可能 | 可能 |
REPEATABLE READ | 不可能 | 不可能 | 可能 |
SERIALIZABLE | 不可能 | 不可能 | 不可能 |
也就是说:
在 READ UNCOMMITTTED 隔离级别下,可能发生脏读,不可重复读和幻读现象;
在 READ COMMITTED 隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
在 REPEATABLE READ 隔离级别下,可能发生幻读现象,但是不可能发生脏读和不可重复读现象;
在 SERIALIZABLE 隔离级别下,上述各种现象都不可能发生。
默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。如下图:
由于两个方法属于同一个物理事务,如果发生回滚,则两者都回滚。
顾名思义就是可以支持事务,如果b.save()在事务环境中运行,则以事务形式运行,否则以非事务运行。
必须在一个事务中运行,也就是说,b.save()只能在已有事务的方法中被调用,如果当前事物不存在,会抛异常。
总是会创建一个新事务(包括物理事务),该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。如下图:
顾名思义不支持事务,当处于存在事务的上下文环境中运行时,b.save()会暂停当前已开启的事务,意味着a.save()的事务被挂起直至b.save()以非事务方法运行完毕后,a.save()的事务继续执行。
绝不能在事务环境中运行,如果a.save()里声明了使用事务,而b.save()的事务类型声明为never,那么只能以抛异常告终。
与Mandatory相反,Mandatory意思是强制要求上下文中有事务(外层有事务),否则抛异常,而Never是上下文中不能有事务(外层无事务),否则抛异常。
嵌套事务支持。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
Nested和RequiresNew的区别:
RequiresNew每次都创建新的独立的物理事务,而Nested只有一个物理事务;Nested嵌套事务回滚或提交不会导致外部事务回滚或提交,但外部事务回滚将导致嵌套事务回滚,而RequiresNew由于都是全新的事务,所以之间是无关联的
Nested使用JDBC 3的保存点实现,即如果使用低版本驱动将导致不支持嵌套事务
使用嵌套事务,必须确保具体事务管理器实现的nestedTransactionAllowed属性为true,否则不支持嵌套事务,如DataSourceTransactionManager默认支持,而HibernateTransactionManager默认不支持,需要我们来开启。
总结
1.死活不要事务的
2.可有可无的
3.必须有事务的
@Transactional 只能用于 public 的方法上,否则事务会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。