本文将带领大家领略Spring事务的风采,Spring事务是我们在日常开发中经常会遇到的,也是各种大小面试中的高频题,希望通过本文,能让大家对Spring事务有个深入的了解,无论开发还是面试,都不会让Spring事务成为拦路虎。
1、事务基本概念
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。
特点:事务是恢复和并发控制的基本单位。事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。
这四个属性通常称为 ACID 特性。
原子性(Automicity)
一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
一致性(Consistency)
事务必须是使数据库从一个一致性状态变到另一个一致性状态。
一致性与原子性是密切相关的。
隔离性(Isolation)
一个事务的执行不能被其他事务干扰。
即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性(Durability)
持久性也称永久性(Permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
2、事务的基本原理
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 的事务管理实现原理了。
下面简单地介绍下,注解方式为例子配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional 标识。
Spring 在启动的时候会去解析生成相关的 bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction 的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
真正的数据库层的事务提交和回滚是通过 binlog 或者 redo log 实现的。
3、Spring 事务的传播属性
所谓 spring 事务的传播属性,就是定义在存在多个事务同时存在的时候,spring 应该如何处理这些事务的行为。这些属性在 TransactionDefinition 中定义,具体常量的解释见下表:
4、数据库隔离级别
脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。
如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。
不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。
幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。
总结:
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle,少数数据库默认隔离级别为:Repeatable Read 比如: MySQL InnoDB
相关Mysql的事务,在《深入精通Mysql(四)》MySQL 事务机制,中高级开发面试必问!中也已经进行了详细的介绍,感兴趣的同学可以去看看。
5、Spring 中的隔离级别
6、事务的嵌套
通过上面的理论知识的铺垫,我们大致知道了数据库事务和 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()) 可以有以下两种处理方式:
捕获异常,执行异常分支逻辑
这 种 方 式 也 是 嵌 套 事 务 最 有 价 值 的 地 方 , 它 起 到 了 分 支 执 行 的 效 果 , 如 果ServiceB.MethodB()失败, 那么执行 ServiceC.MethodC(), 而 ServiceB.MethodB()已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过),这 种 特 性 可 以 用 在 某 些 特 殊 的 业 务 中 , 而 PROPAGATION_REQUIRED 和PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。
外部事务回滚/提交 代码不做任何修改, 那么如果内部事务(ServiceB.MethodB()) rollback, 那么首先 ServiceB.MethodB() 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA.MethodA()) 将根据具体的配置决定自己是commit 还是 rollback。
另外三种事务传播属性基本用不到,在此不做分析。
7、Spring 事务 API 架构图
使用 Spring 进行基本的 JDBC 访问数据库有多种选择。
Spring 至少提供了三种不同的工作模式:JdbcTemplate, 一个在 Spring2.5 中新提供的 SimpleJdbc 类能够更好的处理数据库元数据;
还有一种称之为 RDBMS Object 的风格的面向对象封装方式, 有点类似于 JDO 的查询设计。 我们在这里简要列举你采取某一种工作方式的主要理由. 不过请注意, 即使你选择了其中的一种工作模式, 你依然可以在你的代码中混用其他任何一种模式以获取其带来的好处和优势。
所有的工作模式都必须要求 JDBC 2.0 以上的数据库驱动的支持, 其中一些高级的功能可能需要 JDBC 3.0 以上的数据库驱动支持。
JdbcTemplate - 这是经典的也是最常用的 Spring 对于 JDBC 访问的方案。
这也是最低级别的封装, 其他的工作模式事实上在底层使用了 JdbcTemplate 作为其底层的实现基础。
JdbcTemplate 在 JDK 1.4 以上的环境上工作得很好。
NamedParameterJdbcTemplate - 对 JdbcTemplate 做了封装,提供了更加便捷的基于命名参数的使用方式而不是传统的 JDBC 所使用的“?”作为参数的占位符。
这种方式在你需要为某个 SQL 指定许多个参数时,显得更加直观而易用。该特性必须工作在 JDK1.4 以上。
SimpleJdbcTemplate - 这 个 类 结 合 了 JdbcTemplate 和NamedParameterJdbcTemplate 的最常用的功能,同时它也利用了一些 Java 5 的特性所带来的优势,例如泛型、varargs 和 autoboxing 等,从而提供了更加简便的 API 访问方式。需要工作在 Java 5 以上的环境中。
SimpleJdbcInsert 和 SimpleJdbcCall - 这两个类可以充分利用数据库元数据的特性来简化配置。通过使用这两个类进行编程,你可以仅仅提供数据库表名或者存储过程的名称以及一个 Map 作为参数。其中 Map 的 key 需要与数据库表中的字段保持一致。
这两个类通常和 SimpleJdbcTemplate 配合使用。这两个类需要工作在 JDK 5 以上,同时数据库需要提供足够的元数据信息。RDBMS 对象包括 MappingSqlQuery, SqlUpdate and StoredProcedure - 这种方式允许你在初始化你的数据访问层时创建可重用并且线程安全的对象。
该对象在你定义了你的查询语句,声明查询参数并编译相应的 Query 之后被模型化。一旦模型化完成,任何执行函数就可以传入不同的参数对之进行多次调用。这种方式需要工作在 JDK 1.4 以上。
异常处理
异常结构如下:
SQLExceptionTranslator 是 一 个 接 口 , 如 果 你 需 要 在 SQLException 和org.springframework.dao.DataAccessException 之间作转换,那么必须实现该接口。
转换器类的实现可以采用一般通用的做法(比如使用 JDBC 的 SQLState code),如果为了使转换更准确,也可以进行定制(比如使用 Oracle 的 error code)。
SQLErrorCodeSQLExceptionTranslator 是 SQLExceptionTranslator 的默认实现。 该实现使用指定数据库厂商的 error code,比采用 SQLState 更精确。转换过程基于一个JavaBean ( 类 型 为 SQLErrorCodes ) 中 的 error code 。
这 个 JavaBean 由SQLErrorCodesFactory 工厂类创建,其中的内容来自于 “sql-error-codes.xml”配置文 件 。
该 文 件 中 的 数 据 库 厂 商 代 码 基 于 Database MetaData 信 息 中 的DatabaseProductName,从而配合当前数据库的使用。
SQLErrorCodeSQLExceptionTranslator 使用以下的匹配规则:首 先 检 查 是 否 存 在 完 成 定 制 转 换 的 子 类 实 现 。 通 常SQLErrorCodeSQLExceptionTranslator 这个类可以作为一个具体类使用,不需要进行定制,那么这个规则将不适用。
接着将 SQLException 的 error code 与错误代码集中的 error code 进行匹配。 默认情况下错误代码集将从 SQLErrorCodesFactory 取得。 错误代码集来自 classpath 下的sql-error-codes.xml 文件,它们将与数据库 metadata 信息中的 database name 进行映射。
使用 fallback 翻译器。SQLStateSQLExceptionTranslator 类是缺省的 fallback 翻译器。config 模块NamespaceHandler 接口,DefaultBeanDefinitionDocumentReader 使用该接口来处理在 spring xml 配置文件中自定义的命名空间。
在 jdbc 模块,我们使用 JdbcNamespaceHandler 来处理 jdbc 配置的命名空间,其代码如下:
其中 , EmbeddedDatabaseBeanDefinitionParser 继 承 了AbstractBeanDefinitionParser , 解 析 元 素 , 并 使 用EmbeddedDatabaseFactoryBean 创建一个 BeanDefinition。
在core 模块
1.JdbcTeamplate 对象,其结构如下:
2.RowMapper
3.元数据 metaData 模块
本节中 Spring 应用到工厂模式,结合代码可以更具体了解。
CallMetaDataProviderFactory 创建 CallMetaDataProvider 的工厂类,其代码如下:
TableMetaDataProviderFactory 创建 TableMetaDataProvider 工厂类,其创建过程如下:
使用 SqlParameterSource 提供参数值
使用 Map 来指定参数值有时候工作得非常好,但是这并不是最简单的使用方式。Spring提供了一些其他的 SqlParameterSource 实现类来指定参数值。
我们首先可以看看BeanPropertySqlParameterSource 类,这是一个非常简便的指定参数的实现类,只要你有一个符合 JavaBean 规范的类就行了。它将使用其中的 getter 方法来获取参数值。
SqlParameter 封 装 了 定 义 sql 参 数 的 对 象 。 CallableStateMentCallback ,PrePareStateMentCallback,StateMentCallback,ConnectionCallback 回调类分别对应 JdbcTemplate 中的不同处理方法。
simple 实现
Spring 通过 DataSource 获取数据库的连接。Datasource 是 jdbc 规范的一部分,它通过 ConnectionFactory 获取。一个容器和框架可以在应用代码层中隐藏连接池和事务管理。
当使用 spring 的 jdbc 层,你可以通过 JNDI 来获取 DataSource,也可以通过你自己配置的第三方连接池实现来获取。
流行的第三方实现由 apache Jakarta Commons dbcp 和 c3p0。
TransactionAwareDataSourceProxy 作为目标 DataSource 的一个代理, 在对目标DataSource 包装的同时,还增加了 Spring 的事务管理能力, 在这一点上,这个类的功能非常像 J2EE 服务器所提供的事务化的 JNDI DataSource。
该类几乎很少被用到,除非现有代码在被调用的时候需要一个标准的 JDBC DataSource接口实现作为参数。 这种情况下,这个类可以使现有代码参与 Spring 的事务管理。
通常最好的做法是使用更高层的抽象 来对数据源进行管理,比如 JdbcTemplate 和DataSourceUtils 等等。
注意:DriverManagerDataSource 仅限于测试使用,因为它没有提供池的功能,这会导致在多个请求获取连接时性能很差。
object 模块
再聊JdbcTemplate
JdbcTemplate 是 core 包的核心类。
它替我们完成了资源的创建以及释放工作,从而简化了我们对 JDBC 的使用。 它还可以帮助我们避免一些常见的错误,比如忘记关闭数据库连接。
JdbcTemplate 将完成 JDBC 核心处理流程,比如 SQL 语句的创建、执行,而把 SQL 语句的生成以及查询结果的提取工作留给我们的应用代码。
它可以完成 SQL 查询、更新以及调用存储过程,可以对ResultSet进行遍历并加以提取。
它还可以捕获JDBC异常并将其转换成 org.springframework.dao 包中定义的,通用的,信息更丰富的异常。
使用 JdbcTemplate 进行编码只需要根据明确定义的一组契约来实现回调接口。
PreparedStatementCreator 回调接口通过给定的 Connection 创建一个PreparedStatement,包含 SQL 和任何相关的参数。
CallableStatementCreateor 实现同样的处理,只不过它创建的是 CallableStatement。 RowCallbackHandler 接口则从数据集的每一行中提取值。
我们可以在DAO 实现类中通过传递一个 DataSource 引用来完成 JdbcTemplate 的实例化,也可以在 Spring 的 IOC 容器中配置一个 JdbcTemplate 的 bean 并赋予 DAO 实现类作为一个实例。
需要注意的是 DataSource 在 Spring 的 IOC 容器中总是配制成一个bean,第一种情况下,DataSource bean 将传递给 service,第二种情况下 DataSource bean 传递给 JdbcTemplate bean。
NamedParameterJdbcTemplate
NamedParameterJdbcTemplate 类为 JDBC 操作增加了命名参数的特性支持,而不是传 统 的 使 用 ( '?' ) 作 为 参 数 的 占 位 符 。
NamedParameterJdbcTemplate 类 对JdbcTemplate 类进行了封装, 在底层,JdbcTemplate 完成了多数的工作。
浅谈分布式事务
现今互联网界,分布式系统和微服务架构盛行。一个简单操作,在服务端非常可能是由多个服务和数据库实例协同完成的。在一致性要求较高的场景下,多个独立操作之间的一致性问题显得格外棘手。
基于水平扩容能力和成本考虑,传统的强一致的解决方案(e.g.单机事务)纷纷被抛弃。其理论依据就是响当当的 CAP 原理。往往为了可用性和分区容错性,忍痛放弃强一致支持,转而追求最终一致性。
分布式系统的特性
在分布式系统中,同时满足 CAP 定律中的一致性 Consistency、可用性 Availability 和分区容错性 Partition Tolerance 三者是不可能的。
在绝大多数的场景,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。
分布式事务服务(Distributed Transaction Service,DTS)是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性。
CAP 理论告诉我们在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的,所以我们只能在一致性和可用性之间进行权衡。
为了保障系统的可用性,互联网系统大多将强一致性需求转换成最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。
数据一致性理解:
强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。
弱一致性:系统并不保证后续进程或者线程的访问都会返回最新的更新过的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。
最终一致性:弱一致性的特定形式。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。
想了解下期面试题请关注我!!!