Spring虽然提供了灵活方便的事务管理功能,但是这些功能都是基于底层数据库本身的事务处理机制工作的,要深入了解Spring的事务管理和配置,有必要对数据库事务的基础知识进行学习!
数据库事务(简称:事务)
是数据库管理系统(DBMS)执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
并非任意的对数据库的操作序列都是数据库事务。数据库事务拥有以下四个特性,习惯上被称之为ACID特性。
原子性(Atomic):表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才能提交,事务中任何一个数据操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。。
一致性(Consistency):事务操作成功后,数据库所处的状态和他的业务规则是一致的,即数据不会被破坏。如从A账户转账100元到B账户,不管操作成功与否,A和B的存款总额是不变的。
隔离性(Isolation):多个事务操作并发执行时,不同的事务拥有各自的数据空间,一个事务的执行不应影响其他事务的执行。准确的说并非要求做到完全无干扰,数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,但并发性越弱。
持久性(Durability):一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证能够通过某种机制恢复数据。
在这些事务特性中,数据“一致性”是最终目标,其他的特性都是为达到这个目标的措施、要求或手段。
数据库管理系统一般采用重执行日志保证原子性、一致性和持久性,重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发 生错误退出,数据库即可以根据重执行日志撤销已经执行的操作。此外,对于已经提交的事务,即使数据库崩溃,在重启数据库时也能够根据日志对尚未持久化的数 据进行相应的重执行操作。
和Java程序采用对象锁机制进行线程同步类似,数据库管理系统采用数据库锁机制保证事务的隔离性。当多个事务试图对相同的数据进行操作时,只有持有锁的事务才能操作数据,直到前一个事务完成后,后面的事务才有机会对数据进行操作。Oracle数据库还使用了数据版本的机制,在回滚段为数据的每个 变化都保存一个版本,使数据的更改不影响数据的读取。
数据库系统的四种事务隔离级别
尽管数据库为用户提供了锁的DML操作方式,但直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制。只要用户指定会话的事务隔离级 别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加上适合的锁。此外数据库还会维护这些锁,当一个资源上的锁数目太多时,自动进行 锁升级以提高系统的运行性能,而这一过程对用户来说完全是透明的。
Read Commited:读已提交数据(会出现不可重复读和幻读)
Read Uncommited:读未提交数据(会出现脏读,不可重复读和幻读)。
Repeatable Read:可重复读(会出现幻读)
Serializable:串行化
SQL 92推荐使用REPEATABLE READ(MySQL默认隔离级别)以保证数据的读一致性,不过用户可以根据应用的需要选择适合的隔离等级。
数据并发的问题
一个数据库可能拥有多个访问客户端,这些客户端都可以并发方式访问数据库。数据库中的相同数据可能同时被多个事务访问,如果没有采取必要的隔离措 施,就会导致各种并发问题,破坏数据的完整性。这些问题可以归结为5类,包括3类数据读问题(脏读、不可重复读和幻象读)以及2类数据更新问题(第一类丢 失更新和第二类丢失更新)。下面,我们分别通过实例讲解引发问题的场景。
脏读:
A事务读取B事务尚未提交的更改数据,并在这个数据的基础上操作。如果恰巧B事务回滚,那么A事务读到的数据就是无效的。
在Oracle数据库中,不会发生脏读的情况。
不可重复读:
在同一事务中,多次读取同一数据返回的结果有所不同。换句话说就是,后续读取可以读到另一事务已提交的更新数据。相反,“可重复读”在同一事务中多次读取数据时,能够保证所读数据一样,也就是,后续读取不能读到另一事务已提交的更新数据。
幻读:
A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题。象读一般发生在计算统计数据的事务中。
幻象读和不可重复读是两个容易混淆的概念,前者是指读到了其他事务已经提交的新增数据,而后者是指读到了其他事务已经提交的更改数据(更改或删除), 为了避免这两种情况,采取的对策是不同的,防止读取到更改数据,只需要对操作的数据添加行级锁,阻止操作中的数据发生变化,而防止读取到新增数据,则往往 需要添加表级锁——将整个表锁定,防止新增数据(Oracle使用多版本数据的方式实现)。
数据库锁机制
数据并发会引发很多问题,在一些场合下有些问题是允许的,但在另外一些场合下可能却是致命的。数据库通过锁的机制解决并发访问的问题,虽然不同的数据库在实现细节上存在差别,但原理基本上是一样的。
按锁定的对象的不同,一般可以分为表锁定和行锁定,前者对整个表进行锁定,而后者对表中特定行进行锁定。从并发事务锁定的关系上看,可以分为共享 锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定既防止其他的独占锁定,也防止其他的共享锁定。为了更改数据,数据库必须在进 行更改的行上施加行独占锁定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE语句都会隐式采用必要的行锁定。下面我们介绍一下Oracle数据库常用的5种锁定。
- 行共享锁定:一般通过SELECT FOR UPDATE语句隐式获得行共享锁定,在Oracle中用户也可以通过LOCK TABLE IN ROW SHARE MODE语句显式获得行共享锁定。行共享锁定并不防止对数据行进行更改的操作,但是可以防止其他会话获取独占性数据表锁定。允许进行多个并发的行共享和行 独占性锁定,还允许进行数据表的共享或者采用共享行独占锁定。
- 行独占锁定:通过一条INSERT、UPDATE或DELETE语句隐式获取,或者通过一条LOCK TABLE IN ROW EXCLUSIVE MODE语句显式获取。这个锁定可以防止其他会话获取一个共享锁定、共享行独占锁定或独占锁定。
- 表共享锁定:通过LOCK TABLE IN SHARE MODE语句显式获得。这种锁定可以防止其他会话获取行独占锁定(INSERT、UPDATE或DELETE),或者防止其他表共享行独占锁定或表独占锁 定,它允许在表中拥有多个行共享和表共享锁定。该锁定可以让会话具有对表事务级一致性访问,因为其他会话在用户提交或者回溯该事务并释放对该表的锁定之前 不能更改这个被锁定的表。
- 表共享行独占:通过LOCK TABLE IN SHARE ROW EXCLUSIVE MODE语句显式获得。这种锁定可以防止其他会话获取一个表共享、行独占或者表独占锁定,它允许其他行共享锁定。这种锁定类似于表共享锁定,只是一次只能 对一个表放置一个表共享行独占锁定。如果A会话拥有该锁定,则B会话可以执行SELECT FOR UPDATE操作,但如果B会话试图更新选择的行,则需要等待。
- 表独占:通过LOCK TABLE IN EXCLUSIVE MODE显式获得。这个锁定防止其他会话对该表的任何其他锁定。
不同的隔离级别采用不同的锁类型来实现,在四种隔离级别中,Serializable的隔离级别最高,Read Uncommited的隔离级别最低。
- 大多数据库默认的隔离级别为Read Commited,如SqlServer
- 当然也有少部分数据库默认的隔离级别为Repeatable_Read ,如Mysql,
- Oracle数据库支持READ COMMITTED和SERIALIZABLE两种事务隔离性级别,不支持READ UNCOMMITTED和REPEATABLE READ这两种隔离性级别。虽然SQL标准定义的默认事务隔离性级别是SERIALIZABLE,但是Oracle数据库默认使用的事务隔离性级别却是READ COMMITTED.
Spring对事务管理的支持
Spring提供了两种事务管理方式:
编程式事务管理:
编写程序式的事务管理可以清楚的定义事务的边界,可以实现细粒度的事务控制,比如你可以通过程序代码来控制你的事务何时开始,何时结束等,与后面介绍的声明式事务管理相比,它可以实现细粒度的事务控制。
声明式事务管理:
如果你并不需要细粒度的事务控制,你可以使用声明式事务,在Spring中,你只需要在Spring配置文件中做一些配置,即可将操作纳入到事务管理中,解除了和代码的耦合, 这是对应用代码影响最小的选择。当你不需要事务管理的时候,可以直接从Spring配置文件中移除该设置
如果使用Spring, 我们就不再需要手工控制事务:
事务传播规则(传播行为)
传播行为:定义关于客户端和被调用方法的事物边界
传播行为 |
意义 |
REQUIRED (默认值) |
业务方法需要在一个事务中运行。如果方法运行时,已经处在一个事务中,那么加入到该事务,否则为自己创建一个新的事务 |
NOT_SUPPORTED |
声明方法不需要事务。如果方法没有关联到一个事务,容器不会为它开启事务。如果方法在一个事务中被调用,该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行 |
REQUIRESNEW |
属性表明不管是否存在事务,业务方法总会为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务会被挂起,新的事务会被创建,直到方法执行结束,新事务才算结束,原先的事务才会恢复执行 |
MANDATORY |
该属性指定业务方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果业务方法在没有事务的环境下调用,容器就会抛出例外。 |
SUPPORTS |
这一事务属性表明,如果业务方法在某个事务范围内被调用,则方法成为该事务的一部分。如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行 |
Never |
指定业务方法绝对不能在事务范围内执行。如果业务方法在某个事务中执行,容器会抛出例外,只有业务方法没有关联到任何事务,才能正常执行 |
NESTED |
如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按REQUIRED属性执行.它使用了一个单独的事务, 这个事务拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效 |
事务管理器
spring没有直接管理事务,而是将管理事务的责任委托给JTA或相应的持久性机制所提供的某个特定平台的事物实现
事务管理器实现 | 目标 |
org.springframework.jdbc.datasource.DataSourceTransactionManager | 在单一的JDBC Datasource中的管理事务 |
org.springframework.orm.hibernate3.HibernateTransactionManager | 当持久化机制是hibernate时,用它来管理事务 |
org.springframework.jdo.JdoTransactionManager | 当持久化机制是Jdo时,用它来管理事务。 |
org.springframework.transaction.jta.JtaTransactionManager | 使用一个JTA实现来管理事务。在一个事务跨越多个资源时必须使用 |
org.springframework.orm.ojb.PersistenceBrokerTransactionManager | 当apache的ojb用作持久化机制时,用它来管理事务。 |
声明式事务管理——基于XML配置
在spring配置文件中引入tx命名空间
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- 加载外部的配置文件 -->
<context:property-placeholder location="classpath:cn/itcast/spring/p_jdbc/jdbc.properties"/>
<!-- 一、配置数据库连接池 ComboPooledDataSource -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 基本的连接信息 -->
<property name="jdbcUrl" value="${jdbcUrl}"></property>
<property name="driverClass" value="${driverClass}"></property>
<property name="user" value="${username}"></property>
<property name="password" value="${password}"></property>
<!-- 一些管理的配置 -->
<!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
<property name="initialPoolSize" value="3"></property>
<!--连接池中保留的最小连接数。Default: 3 -->
<property name="minPoolSize" value="3"></property>
<!--连接池中保留的最大连接数。Default: 15 -->
<property name="maxPoolSize" value="5"></property>
<!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
<property name="acquireIncrement" value="3"></property>
<!--最大空闲时间,1800秒内未使用则连接被丢弃,若为0则永不丢弃。Default: 0 -->
<property name="maxIdleTime" value="1800"></property>
</bean>
<!-- 三、配置声明式事务管理(告诉它哪些方法需要事务) -->
<!-- 1,配置事务管理器(应根据情况使用合适的事务管理器) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 2,配置管理事务的“通知” -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置哪些方法要使用什么样的事务配置,没有匹配到的方法不会为其管理事务 -->
<tx:attributes>
<!-- 指定所有以add,delete,update开头的方法都使用事务,
默认read-only="false" -->
<tx:method name="add*" />
<tx:method name="delete*" />
<tx:method name="update*" />
<!-- 指定所有以query开头的方法都是使用只读的事务 -->
<tx:method name="query*" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 3,配置切面 -->
<aop:config>
<!-- 指定拦截所有Dao中的方法 -->
<aop:advisor advice-ref="txAdvice" pointcut="execution(* *..*Dao*.*(..))" />
</aop:config>
声明式事务管理——基于注解配置
在spring中要做的配置
<!-- 加载外部的配置文件 -->
<context:property-placeholder location="classpath:cn/itcast/spring/p_jdbc/jdbc.properties"/>
<!-- 一、配置数据库连接池 ComboPooledDataSource -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 基本的连接信息 -->
<property name="jdbcUrl" value="${jdbcUrl}"></property>
<property name="driverClass" value="${driverClass}"></property>
<property name="user" value="${username}"></property>
<property name="password" value="${password}"></property>
<!-- 一些管理的配置 -->
<!--初始化时获取三个连接,取值应在minPoolSize与maxPoolSize之间。Default: 3 -->
<property name="initialPoolSize" value="3"></property>
<!--连接池中保留的最小连接数。Default: 3 -->
<property name="minPoolSize" value="3"></property>
<!--连接池中保留的最大连接数。Default: 15 -->
<property name="maxPoolSize" value="5"></property>
<!--当连接池中的连接耗尽的时候c3p0一次同时获取的连接数。Default: 3 -->
<property name="acquireIncrement" value="3"></property>
<!--最大空闲时间,1800秒内未使用则连接被丢弃,若为0则永不丢弃。Default: 0 -->
<property name="maxIdleTime" value="1800"></property>
</bean>
<!-- 三、配置声明式事务管理(告诉它哪些方法需要事务) -->
<!-- 1,配置事务管理器(应根据情况使用合适的事务管理器) -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- ==== 基于注解的方式配置事务 ==== -->
<tx:annotation-driven transaction-manager="transactionManager"/>
在类上或方法上写注解声明要使用事务
@Transactional(readOnly = true) public class UserDao { @Transactional(readOnly = false) public void save(final User user) {
//....
}
}
@Transactional注解说明
方法的事务设置将被优先执行。例如: UserServiceImpl类在类的级别上被注解为只读事务,但是,这个类中的 addUser(User uesr) 方法的@Transactional注解的事务设置将优先于类级别注解的事务设置。
@Transactional注解可以被继承,即:在父类上声明了这个注解,则子类中的所有public方法也都是会开事务的。