Spring事务传播原理及数据库事务操作原理
数据库事务原理详解
事务基本概念
事务(Transaction)是访问并可以更新数据库中各项数据项的一个程序执行单元(Unit)。
特点
食物时恢复和并发控制的基本单位。事务应该具有四个属性(ACID)
原子性(Automicity)
一个事务是一个不可分割的工作单位,事务中包含的诸多操作要么都做,要么都不做。
一致性(COnsistency)
事务必须是数据库从一个一致性状态变成另外一个一致性状态。已执行与原子形势密切相关的。
隔离性(Isolation)
一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能相互干扰。
持久性(Durability)
持久性也称为永久性(Permanence),只一个事务一旦提交,他对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
事务的基本原理
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring是无法提供事务功能的,对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
获取连接Connection con = DriverManager.getConnection()
开启事务con.setAutoCommit(true/false)
执行CRUD
提交事务/回滚事务con.commit()/con.rollback()
关闭连接conn.close()
使用Spring的事务管理功能后,我们可以不再写步骤2和4的代码,而是Spring自动完成。那么Spring是如何在我们书写的CRUD之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子,配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
Spring在启动的时候回去解析生成相关的Bean,这个时候回查看拥有相关的注解和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回归事务)。 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
Spring事务的传播性
所谓Spring事务的传播性数据,就是定义存在多大个事五同时存在的时候,Spring应该如何处理这些食物的行为。这些属性在TransactionDefinition中定义,具体常量的解释见下表:
常量名称 | 常量解释 |
---|---|
PROPAGATION_REQUIRED | 支持当前事务,如果当前没有事务,就新建一个事务。这是常见的选择,也是Spring默认的事务的传播 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。新建的事务将和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚之后,不能回滚内层是事务执行的结果,内层事务失败抛出异常,外层事务捕获,也可以不处理回滚操作。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 支持当前事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按照REQUIRED属性执行。它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。 |
数据库隔离级别
隔离级别 | 隔离级别的值 | 导致的问题 |
---|---|---|
Read-Uncommitted | 0 | 导致脏读 |
Read-Committed | 1 | 避免脏读,允许不可重复读和幻读 |
Repeatable-Read | 2 | 避免脏读,不可重复读,允许幻读 |
Serializable | 3 | 串行化读,食物只能一个一个执行,避免脏读,不可重复读,幻读。执行效率慢,使用时慎重 |
脏读:一事务对数据进行了增删改,但未提交,另外一个事务可以读取到未提交的数据。如果第一个是事务回滚了,那么第二个事务就读到了脏数据。
不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据不一致的。
幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。
总结:
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。大多数的数据库默认隔离级别为Read Committed,比如SQLServer、Oracle。少数数据库默认隔离级别为Repeatable Read 比如Mysql InnoDB。
Spring中的隔离级别
常量 | 解释 |
---|---|
ISOLATION_DEFAULT | 这是个PlatfromTransactionManager默认的隔离级别,使用数据库的默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。 |
ISOLATION_READ_UNCOMMITTED | 这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻读。 |
ISOLATION_READ_COMMITTED | 保证一个事务的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。 |
ISOLATION_REPEATABLE_READ | 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻读。 |
ISOLATION_SERIALIZABLE | 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。 |
事务的嵌套
通过上面的理论的铺垫,我们大致知道了数据库事务和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。 另外的三种事务传播属性基本用不到,再次不做分析。
Spring事务API架构图
使用Spring进行基本的JDBC访问数据库有很多种选择。Spring至少提供了三种不同的工作模式:JdbcTemplate,一个在Spring2.5中新提供的SimpleJdbc类能够更好的处理数据库元数据;还有一种称之为RDBMS Object的风格的面向对象封装方式,有点类似于JDO的查询设计。我们在这里简要列举你才去某一种工作方式的主要理由。不过请注意,即使你选择了其中的一种工作模式,你依然可以在你的代码中混用其他任何一种模式已获取带来的好处和优势。所有的工作模式都是必须要求JDBC2.0以上的数据库驱动的支持,其中一些高级的功能可能需要JDBC3.0以上的数据库驱动支持。
JDBCTemplate
这是景丹的也是最常用的Spring对于JDBC访问的方案。这也是最低的级别的封装,其他的工作模式事实上在底层使用来了JdbcTemplate作为其底层的实现基础。JdbcTemplate在JDK1.4以上的环境上工作的很好。
NamedParameterJdbcTemplate
对jdbcTemplate做了封装,也提供了更加便捷的机遇命名参数的使用方式而不是传统的JDBC所使用的?作为参数的占位符。这种方式在你需要为某个SQL指定许多个参数时,显得更加直观而易用。该特性必须工作在JDK1.4以上。
SimpleJdbcTemplate
这个类结合了JdbcTemplate和NamedParameterTemplate的最常用的功能,同时它也利用了一些java5的特性所带来的优势,例如泛型、varargs和autoboxing等,从而提供了更加简便的API访问方式。需要工作JDK5以上的环境中。
SimpleJdbcInsert和SimpleJdbcCall
这两个类可以充分利用数据库元数据的特性来简化配置。通过使用这两个类进行编程,你可以仅仅提供数据库表明或者存储过程的名称以及一个Map作为参数。其中Map的key需要与数据库表中的字段保持一致。这两个类通常和SimpleJdbcTemplate配合使用。这两个类需要工作在JDK 5以上,同时数据库需要提供足够的元数据信息。
RDBMS对象包括MapppingSqlQuery,SqlUpdate and StoredProcedure
这种方式允许你在初始化你的数据访问时创建可重用并且线程安全的对象。该对象在你定义了你的查询语句,生命查询参数并编译相应的Query之后被模型化。一单模型化完成,任何执行函数就可以传入不同的参数对之进行多次调用。这中方式需要工作在JDK1.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。顺便介绍一下用到的软件包org.w3c.dom。
软件包org.w3c.dom:为文档对象模型(DOM)提供接口,该模型是Java API for XML Processing的组件API。该Document Object Model Level 2 Core API允许程序动态访问和更新文档的内容和结构。
Attr:Attr接口表示Element对象中的属性。
CDATASection:CDATA节用于转义文本块,该文本块包含的字符如果不转义则会被视为标记。
CharacterData:CharacterData接口使用属性集合和用于访问DOM中字符数据的方法扩展节点。
Comment:此接口继承自CharacterData表示注释的内容,即起始‘ ’之间的所有字符。
Document:Document接口表示整个HTML或XML文档。
DocumentFragment:DocumentFragment是“轻量级”或“最小”Document对象。
DocumentType:每个geDocument都有doctype属性,该属性的值可以为null,也可以为DocumentType对象。
DOMConfiguration:该DOMConfiguration接口表示文档的配置,并维护一个可识别的参数表。
DOMError:DOMError是一个描述错误的接口。
DOMErrorHandler:DOMErrorHandler是在报告处理XML数据时发生的错误或在进行某些其他处理(如验证文档)时DOM实现可以调用的回调接口。
DOMImplementation:DOMImplementation接口为执行独立于文档对象模型的任何特定实例的操作提供了许多方法。
DOMImplementationList:DOMImplementationList接口提供对DOM实现的有序集合的抽象,没有定义或约束如何实现此集合。
DOMImplementationSource:此接口允许DOM实现程序根据请求的功能和版本提供一个或多个实现,如下所述。
DOMLocator:DOMLocator是一个描述位置(如发生错误的位置)的接口。
DOMStringList:DOMStringList接口提供对DOMString值得有序集合的抽象,没有定义或约束此集合是如何实现。
Element:Element接口表示HTML或XML文档中的一个元素。
Entity:此接口表示在XML文档中解析和未解析的已知实体。
EntityReference:EntityReference节点可以用来在树中表示实体引用。
NamedNodeMap:实现NamedNodeMap接口的对象用于表示可以通过名称访问的节点的集合。
NameList接口提供对并行的名称和名称空间值对(可以为null值)的有序集合的抽象,无需定义或约束如何实现此集合。
Node:该Node接口是整个文档对象模型的主要数据类型。
NodeList:NodeList接口提供对节点的有序集合的抽象,没有定义或约束如何实现此集合。
Notation:此接口表示在DTD中声明的表示法。
ProcessingInstruction:ProcessingInstruction接口表示“处理指令”,该指令作为一种在文档的文本中保持特定于处理器的信息的方法在XML中使用。
Text:该Text接口继承自CharacterData,并且表示Element或Attr的文本内容(在Xml中称为字符数据)。
TypeInfo:TypeInfo接口表示从Element或Attr节点引用的类型,用于文档相关的模式指定。
UserDataHandler:当使用Node.setUserData()将一个对象与节点上的键相关联时,当克隆、导入或重命名该对象关联的节点时应用程序可以提供调用的处理程序。
core模块
JdbcTemplate对象,其结构如下:
RowMapper
元数据metaData模块
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 和c3po。
TransactionAwareDataSourceProxy作为目标DataSource的一个代理,在对目标DataSource包装的同时,还增加了Spring的事务管理能力,在这一点上,这个类的功能非常想J2EE服务器所提供的事务化的JNDI DataSource。
Note
该类几乎很少被用到,除非现有代码在被调用的时候需要一个标准的JDBC DataSource接口实现作为参数。这中情况下,这个类可以实现有代码参与Spring事务管理。通常最好的做法是使用更高层的抽象来对数据源进行管理,比如JdbcTemplate和DataSourceUtils等等。
Object模块
JdbcTemplate
JdbcTemplate是core包的核心类。它替我们完成了资源的创建以及释放工作,从而简化了我们对JDBC的使用。它还可以帮助我们避免一些常见的错误,比如忘记关闭数据库连接。JdbcTemplate将完成JDBC核心处理流程,比如SQL语句的创建、执行,而把SQL语句的生成以及查询结果的提取工作留给我们的应用代码。它可以完成SQL查询、更新以及调用存储过程,可以对ResultSet进行遍历并加以提取。它还可以捕获JDBC异常并将其转换成org.springframework.dao包中定义的,通用的,信息更丰富的异常。使用JdbcTemplate进行编码只需要根据明确定义的一组契约来实现对调接口。PreparedStatementCreator回调接口通过给定的Connection创建一个PreparedStatement,包含SQL和任何相关的参数。CallableStatementCreator实现相同的处理,只不过它创建的CallableStatement。RowCallbackHandler接口则从数据集的每一行中提取值。我们可以从DAO实现类中通过传递一个DataSource引用来完成JdbcTemplate的实例化,也可以在Spring的IOC容器中配置一个JdbcTemplate的bean并复制DAO实现类作为一个实例。需要注意的是DataSource在Spring的Ioc容器中总是配置成一个bean,第一种情况下,DataSource bean将传递给service,第二种情况下DataSource bean传递给JdbcTemplate bean。
NamedParameterJdbcTemplate
为JDBC 操作增加了命名参数的特性支持,而不是传统的使用?作为参数的占位符。NamedParameterJdbcTemplate类对JdbcTemplate类进行封装,在底层,JdbcTemplate完成了多数的工作。
浅谈分布式事务
现今互联网界,分布式系统和微服务架构盛行。一个简单操作,在服务端非常可能是由多个服务和数据库实例协同完成的。在一致性要求较高的场景下,多个独立操作之间的一致性问题显得格外棘手。
基于水平扩容能力和成本考虑,传统的强一致的解决方案(e.g.单机事务)纷纷被抛弃。其理论依据就是响当当的CAP原理。往往为了可用性和分区容错性,忍痛放弃强一致支持,转而追求最终一致性。
分布式系统的特性
在分布式系统中,同时满足CAP定律中的一致性Consistency、可用性Availability和分区容错性Partition Tolerance三者是不可能的。在绝大多数的场景,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证最终一致性。
分布式事务服务(Distributed Transaction Service,DTS)是一个分布式事务框架,用来保障在大规模分布式环境下事务的最终一致性。
CAP理论告诉我们在分布式存储系统中,最多只能实现上面的亮点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的,所以我们只能在一致性和可用性之间进行权衡。
为了保证系统的可用性,互联网系统大多将强一致性需求转为最终一致性的需求,并通过系统执行幂等性的保证,保证数据的最终一致性。
数据一致性理解
强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这中是对用户最友好的,就是用户上一次写什么,下一次保证能读什么。根据CAP理论,这中实现需要牺牲可用性。
弱一致性:系统不保证后续进程或者线程的访问都会返回最新的更新的值。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。
最终一致性:弱一致性的特定形式。系统保证没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS是一个典型的最终一致性系统。