一、事务的基本要素(ACID)

事务是指多个操作单元组成的合集,多个单元操作是整体不可分割的,要么都操作不成功,要么都成功。其必须遵循四个原则(ACID)。

  1. 原子性(Atomicity):指一个事务要么全部执行, 要么不执行,也就是说一个事务不可能只执行了一半就停止了. 比如你从取款机取钱, 这个事务可以分成两个步骤: 1 划卡, 2 出钱. 不可能划了卡, 而钱却没出来. 这两步必须同时完成. 要么就不完成。
  2. 一致性(Consistency):在事务执行前数据库的数据处于正确的状态,而事务执行完成后数据库的数据还是应该处于正确的状态,即数据完整性约束没有被破坏;如银行转帐,A 转帐给 B,必须保证 A 的钱一定转给 B,一定不会出现 A 的钱转了但 B 没收到,否则数据库的数据就处于不一致(不正确)的状态。
  3. 隔离性(Isolation):并发事务执行之间互不影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性;
  4. 持久性(Durability):事务一旦执行成功,它对数据库的数据的改变必须是永久的,不会因比如遇到系统故障或断电造成数据不一致或丢失。

二、事务的分类

  1. 数据库分为本地事务跟全局事务
    • 本地事务:普通事务,独立一个数据库,能保证在该数据库上操作的 ACID。
    • 分布式事务:涉及两个或多个数据库源的事务,即跨越多台同类或异类数据库的事务(由每台数据库的本地事务组成的),分布式事务旨在保证这些本地事务的所有操作的 ACID,使事务可以跨越多台数据库;
  2. Java 事务类型分为 JDBC 事务跟 JTA 事务
    • JDBC 事务:即为上面说的数据库事务中的本地事务,通过 connection 对象控制管理。
    • JTA 事务:JTA 指 Java 事务 API(Java Transaction API),是 Java EE 数据库事务规范, JTA 只提供了事务管理接口,由应用程序服务器厂商(如 WebSphere Application Server)提供实现,JTA 事务比 JDBC 更强大,支持分布式事务。
  3. 按是否通过编程分为声明式事务和编程式事务,参考 http://blog.csdn.net/liaohaojian/article/details/70139151
    • 声明式事务:通过 XML 配置或者注解实现。
    • 编程式事务:通过编程代码在业务逻辑时需要时自行实现,粒度更小。

三、Spring 事务的基本原理

Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring 是无法提供事务功能的。对于纯 JDBC 操作数据库,想要用到事务,可以按照以下步骤进行:

  1. 获取连接 Connection con = DriverManager.getConnection()
  2. 开启事务 con.setAutoCommit(true/false);
  3. 执行 CRUD
  4. 提交事务 / 回滚事务 con.commit() / con.rollback();
  5. 关闭连接 conn.close();

使用 Spring 的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由 Spirng 自动完成。
那么 Spring 是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解 Spring 的事务管理实现原理了。下面简单地介绍下,注解方式为例子

  1. 配置文件开启注解驱动,在相关的类和方法上通过注解 @Transactional 标识。
  2. spring 在启动的时候会去解析生成相关的 bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据 @Transaction 的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
  3. 真正的数据库层的事务提交和回滚是通过 binlog 或者 redo log 实现的。

四、数据库隔离级别

隔离级别

隔离级别值脏读不可重复读幻读

未提交读

(Read-Uncommitted)

0

提交读(不可重复读

(Read-committed)

1

可重复读

(repeatable-read)

2

串行化 

(serializable)

3

脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。

不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。

幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

总结:

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle

少数数据库默认隔离级别为:Repeatable Read 比如: MySQL InnoDB

补充:

  1、SQL 规范所规定的标准,不同的数据库具体的实现可能会有些差异

  2、mysql 中默认事务隔离级别是可重复读时并不会锁住读取到的行

  3、事务隔离级别为读提交时,写数据只会锁住相应的行

  4、事务隔离级别为可重复读时 ,如果有索引(包括主键索引)的时候,以索引列为条件更新数据,会存在间隙锁间隙锁、行锁、下一键锁的问题,从而锁住一些行;如果没有索引,更新数据时会锁住整张表

  5、事务隔离级别为串行化时,读写数据都会锁住整张表

   6、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,鱼和熊掌不可兼得啊。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为 Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

五、Spring 中的隔离级别

常量解释
ISOLATION_DEFAULT这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。另外四个与 JDBC 的隔离级别相对应。
ISOLATION_READ_UNCOMMITTED这是事务最低的隔离级别,它充许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。
ISOLATION_READ_COMMITTED保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。
ISOLATION_REPEATABLE_READ这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。
ISOLATION_SERIALIZABLE这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。
  1. ISOLATION_DEFAULT:用底层数据库的默认隔离级别,数据库管理员设置什么就是什么
  2. ISOLATION_READ_UNCOMMITTED(未提交读):最低隔离级别、事务未提交前,就可被其他事务读取(会出现幻读、脏读、不可重复读)
  3. ISOLATION_READ_COMMITTED(提交读):一个事务提交后才能被其他事务读取到(该隔离级别禁止其他事务读取到未提交事务的数据、所以还是会造成幻读、不可重复读)、sql server 默认级别
  4. ISOLATION_REPEATABLE_READ(可重复读):可重复读,保证多次读取同一个数据时,其值都和事务开始时候的内容是一致,禁止读取到别的事务未提交的数据(该隔离基本可防止脏读,不可重复读(重点在修改),但会出现幻读(重点在增加与删除))(MySql 默认级别,更改可通过 set transaction isolation level 级别)
  5. ISOLATION_SERIALIZABLE(序列化):代价最高最可靠的隔离级别(该隔离级别能防止脏读、不可重复读、幻读)
    1. 丢失更新:两个事务同时更新一行数据,最后一个事务的更新会覆盖掉第一个事务的更新,从而导致第一个事务更新的数据丢失,这是由于没有加锁造成的;
    2. 幻读:同样的事务操作过程中,不同时间段多次(不同事务)读取同一数据,读取到的内容不一致(一般是行数变多或变少)。
    3. 脏读:一个事务读取到另外一个未提及事务的内容,即为脏读。
    4. 不可重复读:同一事务中,多次读取内容不一致(一般行数不变,而内容变了)。

幻读与不可重复读的区别:幻读的重点在于插入与删除,即第二次查询会发现比第一次查询数据变少或者变多了,以至于给人一种幻象一样,而不可重复读重点在于修改,即第二次查询会发现查询结果比第一次查询结果不一致,即第一次结果已经不可重现了。

数据库隔离级别越高,执行代价越高,并发执行能力越差,因此在实际项目开发使用时要综合考虑,为了考虑并发性能一般使用提交读隔离级别,它能避免丢失更新和脏读,尽管不可重复读和幻读不能避免,但可以在可能出现的场合使用悲观锁或乐观锁来解决这些问题。

悲观锁与乐观锁可参考:http://blog.csdn.net/liaohaojian/article/details/62416972

事务的并发问题

  1、脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据

  2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果 不一致。

  3、幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

  小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

拓展:

Spring 事务在应用中可能会失效的情况有很多,主要涉及到事务的配置、数据库引擎、代码逻辑等方面。以下是一些可能导致 Spring 事务失效的常见情况:

  1. 不正确的事务管理器配置:

    • 如果配置文件中没有正确指定事务管理器或者使用的事务管理器不支持事务,事务可能无法正常工作。
  2. 方法未被 Spring 代理:

    • Spring 的事务管理依赖于 AOP(面向切面编程),如果一个方法未被 Spring 代理(通常是因为它被同一个类中的另一个方法调用),事务可能不会生效。
  3. RuntimeException 未被抛出:

    • Spring 事务默认只在遇到 RuntimeException 及其子类时回滚,如果在事务中抛出了其他类型的异常,可能导致事务不回滚。
  4. 嵌套事务问题:

    • 如果在一个事务方法中调用了另一个带有事务注解的方法,并且两者的事务传播属性不兼容,可能导致嵌套事务的问题。
  5. 非公开方法调用:

    • Spring 事务通过 AOP 实现,如果事务注解所在的类的方法是通过类的实例而不是通过 Spring 容器获取的实例调用的,事务可能不会生效。
  6. 事务注解放置位置不当:

    • 如果 @Transactional 注解放置在了不支持事务的地方,比如私有方法、静态方法等,事务可能不会生效。
  7. 自调用问题:

    • 当一个事务方法内部调用同一类中的另一个事务方法时,事务可能不会生效,因为 Spring 默认会使用基于代理的 AOP,无法拦截自调用。
  8. 并发问题:

    • 在多线程环境下,如果多个线程同时访问事务方法,可能导致并发问题,需要考虑事务的隔离级别和并发控制机制。
  9. 数据库引擎问题:

    • 不同的数据库引擎对事务的支持程度不同,有些数据库引擎可能不支持某些事务特性,导致事务失效。
  10. 未开启代理模式:

    • 如果在配置类上没有使用 @EnableTransactionManagement 注解开启代理模式,Spring 无法为带有 @Transactional 注解的方法创建代理,事务也会失效。

在使用 Spring 事务时,建议详细查看日志、配置和代码,确保事务配置正确,并注意处理可能导致事务失效的特殊情况。

六、事务的传播属性

精简:

Spring 事务传播级别定义了在一个事务方法执行时,如何处理嵌套的事务。在 Spring 中,事务传播级别由 Propagation 枚举表示。以下是 Spring 事务传播级别的常见取值:

  1. REQUIRED(默认):

    • 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是最常用的传播级别。
  2. SUPPORTS

    • 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
  3. MANDATORY

    • 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  4. REQUIRES_NEW

    • 总是创建一个新的事务,如果当前存在事务,则挂起该事务。
  5. NOT_SUPPORTED

    • 总是以非事务方式执行操作,如果当前存在事务,则挂起该事务。
  6. NEVER

    • 总是以非事务方式执行操作,如果当前存在事务,则抛出异常。
  7. NESTED

    • 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则与 REQUIRED 类似。不同之处在于,嵌套事务是被嵌套在外部事务中的一个独立事务,它有自己的提交和回滚。
  8. NESTED_READ_COMMITTED

    • 类似于 NESTED,但嵌套事务的隔离级别被设置为 READ_COMMITTED

所谓 spring 事务的传播属性,就是定义在存在多个事务同时存在的时候,spring 应该如何处理这些事务的行为。这些属性在 TransactionDefinition 中定义,具体常量的解释见下表:

常量名称常量解释
PROPAGATION_REQUIRED支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择,也是 Spring 默认的事务的传播。
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起。新建的事务将和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚之后,不能回滚内层事务执行的结果,内层事务失败抛出异常,外层事务捕获,也可以不处理回滚操作
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED

如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按 REQUIRED 属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对 DataSourceTransactionManager 事务管理器起效。

通过上面的理论知识的铺垫,我们大致知道了数据库事务和 spring 事务的一些属性和特点,接下来我们通过分析一些嵌套事务的场景,来深入理解 spring 事务传播的机制。

假设外层事务 Service A 的 Method A() 调用 内层 Service B 的 Method B()

PROPAGATION_REQUIRED(spring 默认)

如果 ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候 spring 已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。

假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。

这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。

PROPAGATION_REQUIRES_NEW

比如我们设计 ServiceA.methodA() 的事务级别为 PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。

那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。

他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交 (主要看 B 抛出的异常是不是 A 会回滚的异常)。

PROPAGATION_SUPPORTS

假设 ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到 ServiceB.methodB() 时,如果发现 ServiceA.methodA() 已经开启了一个事务,则加入当前的事务,如果发现 ServiceA.methodA() 没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。

PROPAGATION_NESTED

现在的情况就变得比较复杂了, ServiceB.methodB() 的事务属性被配置为 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? 
ServiceB#methodB 如果 rollback, 那么内部事务 (即 ServiceB#methodB) 将回滚到它执行前的 SavePoint 而外部事务 (即 ServiceA#methodA) 可以有以下两种处理方式:

a、捕获异常,执行异常分支逻辑

void methodA() { 
 
        try { 
 
            ServiceB.methodB(); 
 
        } catch (SomeException) { 
 
            // 执行其他业务, 如 ServiceC.methodC(); 
 
        } 
 
    }

这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据 (相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。

b、 外部事务回滚 / 提交 代码不做任何修改, 那么如果内部事务 (ServiceB#methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务 (即 ServiceA#methodA) 将根据具体的配置决定自己是 commit 还是 rollback

另外三种事务传播属性基本用不到,在此不做分析。

七、Spring 事务接口总结

对于项目中需要使用到事务的地方,我建议开发者还是使用 spring 的 TransactionCallback 接口来实现事务,不要盲目使用 spring 事务注解,如果一定要使用注解,那么一定要对 spring 事务的传播机制和隔离级别有个详细的了解,否则很可能发生意想不到的效果。

根据底层所使用的不同的持久化 API 或框架,使用如下:

  • DataSourceTransactionManager:适用于使用 JDBC 和 iBatis 进行数据持久化操作的情况,在定义时需要提供底层的数据源作为其属性,也就是 DataSource
  • HibernateTransactionManager:适用于使用 Hibernate 进行数据持久化操作的情况,与 HibernateTransactionManager 对应的是 SessionFactory
  • JpaTransactionManager:适用于使用 JPA 进行数据持久化操作的情况,与 JpaTransactionManager 对应的是 EntityManagerFactory