一.事务的四大特性
原子性:在一个事务中,像增删改(DML)要么全部成功,要么全部失败。
一致性:事务完成时,必须使所有数据都保持一致的状态,例如:在银行转账,A向B转1000,A扣1000,B加1000要一起成功或失败。
隔离性:多个事务的执行是互不干扰的。
持久性:事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。
二.并发事务带来哪些问题?
在典型的应⽤程序中,多个事务并发运⾏,经常会操作相同的数据来完成各⾃的任务(多个⽤户对同⼀数据进⾏操作)。并发虽然是必须的,但可能会导致以下的问题。
更新丢失:两个事务同时更新,第二个事务回滚会覆盖第一个事务更新的数据,导致更新丢失。或事务1执行更新操作,在事务1结束前事务2也更新,则事务1的更新结果被事务2的覆盖了。
脏读(Dirty read): 当⼀个事务正在访问数据并且对数据进⾏了修改,⽽这种修改还没有提交到数据库中,这时另外⼀个事务也访问了这个数据,然后使⽤了这个数据。因为这个数据是还没有提交的数据,那么另外⼀个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
不可重复读(Unrepeatableread): 指在⼀个事务内多次读同⼀数据。在这个事务还没有结束时,另⼀个事务也访问该数据。那么,在第⼀个事务中的两次读数据之间,由于第⼆个事务的修改导致第⼀个事务两次读取的数据可能不太⼀样。这就发⽣了在⼀个事务内两次读到的数据是不⼀样的情况,因此称为不可重复读。
幻读(Phantom read): 幻读与不可重复读类似。它发⽣在⼀个事务(T1)读取了⼏⾏数据,接着另⼀个并发事务(T2)插⼊了⼀些数据时。在随后的查询中,第⼀个事务(T1)就会发现多了⼀些原本不存在的记录,就好像发⽣了幻觉⼀样,所以称为幻读。
不可重复读和幻读区别:
不可重复读的重点是修改⽐如多次读取⼀条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除⽐如多次读取⼀条记录发现记录增多或减少了。
三.事务隔离的级别
为此我们需要通过提供不同类型的“锁”机制针对数据库事务进行不同程度的并发访问控制,由此产生了不同的事务隔离级别:隔离级别(低->高)。SQL、SQL2标准定义了四种隔离级别:
●读未提交(Read Uncommitted)
含义解释:只限制同一数据写事务禁止其他写事务。解决”更新丢失”。(一事务写时禁止其他事务写)
名称解释:可读取未提交数据
所需的锁:排他写锁
●读提交(Read Committed)
含义解释:只限制同一数据写事务禁止其它读写事务。解决”脏读”,以及”更新丢失”。(一事务写时禁止其他事务读写)
名称解释:必须提交以后的数据才能被读取
所需的锁:排他写锁、瞬间共享读锁
●可重复读(Repeatable Read)
含义解释:限制同一数据写事务禁止其他读写事务,读事务禁止其它写事务(允许读)。解决”不可重复读”,以及”更新丢失”和”脏读”。(一事务写时禁止其他事务读写、一事务读时禁止其他事务写)
注意没有解决幻读,解决幻读的方法是增加范围锁(range lock)或者表锁。
名称解释:能够重复读取
所需的锁:排他写锁、共享读锁
●串行化(Serializable)
含义解释:限制所有读写事务都必须串行化实行。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。(一事务写时禁止其他事务读写、一事务读时禁止其他事务读写)
所须的锁:范围锁或表锁
下表是各隔离级别对各种异常的控制能力。
|
更新丢失 |
脏读 |
不可重复读 |
幻读 |
RU(读未提交) |
避免 |
|
|
|
RC(读提交) |
避免 |
避免 |
|
|
RR(可重复读) |
避免 |
避免 |
避免 |
|
S(串行化) |
避免 |
避免 |
避免 |
避免
|
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,数据完整性越好,但执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。
四.事务传播行为
事务传播行为指的是当一个事务方法被另一个事务方法调用时,这个方法是怎么运行的。
举例说明:有两个事务方法,一个是方法A,一个是方法B,方法A中调用了方法B,那方法B是开启一个事务运行还是在方法A中的事务中运行,是由方法B的事务传播行为控制的。
在Spring中定义了7中事务传播行为:
传播行为 |
含义 |
PROPAGATION_REQUIRED |
表示当前方法必须运行在事务中,如果当前事务存在,方法将会在该事务中运行,否则,会启动一个新的事务 |
PROPAGATION_SUPPORTS |
表示该方法不需要事务上下文,但是如果存在当前的事务的话,那么该方法会在这个事务汇总运行 |
PROPAGATION_MANDATORY |
表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_REQUIRED_NEW |
表示当前方法必须运行在它自己的事务中。一个新的事务将被启动,如果存在当前事务,在该方法执行期间,当期事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NOT_SUPPORTED |
表示当前方法不应该运行在事务中,如果存在当前事务,在该方法运行期间,当前事务将被挂起,如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NEVER |
表示当前方法不应该运行在事务上下文中,如果当前正有一个事务在运行,则抛出异常 |
PROPAGATION_NESTED |
表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独的提交或回滚。如果当前事务不存在,那么其行为和 PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的,可以参考资源管理器的文档来确认它们是够支持嵌套事务 |
1、PROPAGATION_REQUIRED
存在事务就支持该事务,不存在则开启一个事务
演示代码:
@Component
public class Transaction {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
jdbcTemplate.update("insert into users value (12,'wangwu','000000')");
methodB();
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {
jdbcTemplate.update("insert into actor value (20,'范闲',now())");
// 此处抛出异常,方法A和方法B的操作都将回滚
int i = 100 / 0;
}
}
单独调用方法B,因为上下文不存在事务,所以会开启一个事务
当调用方法A时,因为上下文不存在事务,所以会开启一个事务,当执行到方法B时,方法B发现存在一个事务,方法B就不会开启新的事务,而是在方法A中的事务中执行。
2、PROPAGATION_SUPPORTS
如果存在事务则支持当前事务,如果不存在事务则无事务运行
演示代码:
@Component
public class Transaction {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
jdbcTemplate.update("insert into users value (12,'wangwu','000000')");
methodB();
}
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {
jdbcTemplate.update("insert into actor value (20,'范闲',now())");
// 存在事务回滚操作,不存在事务则不回滚
int i = 100 / 0;
}
单独调用方法B,无事务运行
调用方法A,方法B加入方法A的事务,事务的运行
3、PROPAGATION_MANDATORY
如果存在一个事务,则事务的运行,没有事务则抛出异常
@Component
public class Transaction {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
jdbcTemplate.update("insert into users value (12,'wangwu','000000')");
methodB();
}
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {
jdbcTemplate.update("insert into actor value (20,'范闲',now())");
}
}
直接调用方法B,因为没有事务所有会抛出异常
No existing transaction found for transaction marked with propagation 'mandatory'
调用方法A,方法B会加入到方法A的事务中去
4、PROPAGATION_REQUIRED_NEW
使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
会开启一个事务,如果存在一个事务,则会把存在的事务挂起
@Component
public class Transaction {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
doSomeTingA();
methodB();
doSomeTingB();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
jdbcTemplate.update("insert into actor value (20,'范闲',now())");
}
public void doSomeTingA() {
jdbcTemplate.update("insert into users value (12,'wangwu','000000')");
}
public void doSomeTingB() {
jdbcTemplate.update("insert into actor value (21,'范思辙',now())");
}
}
当调用
main(){
methodA();
}
相当于调用
main(){
TransactionManager tm = null;
try{
//获得一个JTA事务管理器
tm = getTransactionManager();
tm.begin();//开启一个新的事务
Transaction ts1 = tm.getTransaction();
doSomeThing();
tm.suspend();//挂起当前事务
try{
tm.begin();//重新开启第二个事务
Transaction ts2 = tm.getTransaction();
methodB();
ts2.commit();//提交第二个事务
} Catch(RunTimeException ex) {
ts2.rollback();//回滚第二个事务
} finally {
//释放资源
}
//methodB执行完后,恢复第一个事务
tm.resume(ts1);
doSomeThingB();
ts1.commit();//提交第一个事务
} catch(RunTimeException ex) {
ts1.rollback();//回滚第一个事务
} finally {
//释放资源
}
}
在这里,把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。Ts2是否成功并不依赖于 ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了 methodB之外的其它代码导致的结果却被回滚了
5、PROPAGATION_NOT_SUPPORTED
PROPAGATION_NOT_SUPPORTED总是非事务的运行,并且挂起任何存在的事务,使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。
6、PROPAGATION_NEVER
总是非事务的运行,如果存在一个活动的事务,则抛出异常
7.PROPAGATION_NESTED
如果一个活动的事务存在,则运行在一个嵌套事务中,如果没有活动事务,则按照PROPAGATION_REQUIRED属性执行
这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。
需要JDBC 驱动的java.sql.Savepoint类。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true(属性值默认为false)。
@Component
public class Transaction {
@Autowired
JdbcTemplate jdbcTemplate;
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
doSomeTingA();
methodB();
doSomeTingB();
}
@Transactional(propagation = Propagation.NESTED)
public void methodB() {
jdbcTemplate.update("insert into actor value (20,'范闲',now())");
}
public void doSomeTingA() {
jdbcTemplate.update("insert into users value (12,'wangwu','000000')");
}
public void doSomeTingB() {
jdbcTemplate.update("insert into actor value (21,'范思辙',now())");
}
}
单独执行方法B,按照PROPAGATION_REQUIRED属性执行
如果调用方法A,相当于如下效果:
main(){
Connection con = null;
Savepoint savepoint = null;
try{
con = getConnection();
con.setAutoCommit(false);
doSomeThingA();
savepoint = con2.setSavepoint();
try{
methodB();
} catch(RuntimeException ex) {
con.rollback(savepoint);
} finally {
//释放资源
}
doSomeThingB();
con.commit();
} catch(RuntimeException ex) {
con.rollback();
} finally {
//释放资源
}
}
当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:
它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTATrasactionManager实现可能有不同的支持方式。
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back.