标签:spring 事务管理 数据库隔离级别 修改丢失 乐观锁
Spring 事务管理
理解Spring的事务管理,需要了解以下几个概念:
每条线程只可以拥有一个活动的数据库连接,称为“当前连接”。
一般数据库事务遵循“开启事务—>操作—>提交事务”三个步骤。在单线程环境中,不能调换它们的顺序;但是在多线程环境中,如果数据库连接需要共享,将会打破这个顺序,如线程A将线程B的事务一起提交了。
为了解决该问题,采用“当前连接”来存放数据库连接,并与线程绑定(采用ThreadLocal)。也就是说在线程A下启动的事务,不会影响到线程B的数据库事务,它们之间使用的数据库连接彼此互不干扰。
注意:当前的数据库连接是可以被随时更换的(此时不考虑引用计数的值)。在独立事务中,如果当前连接已存在事务,则会新建一个数据库连接作为当前连接并开启它的事务。
程序在执行期间,如果持有数据库连接,需要使用“引用计数”标记。
引用计数用来确定当前数据库连接(数据库连接是共享的,连接池)是否可以被关闭。当引用计数为0或小于0时,认为应用程序不再需要该连接,可以放心关闭。
当从连接池获取连接时,该连接的引用计数将增加1;当连接池回收该该连接时,引用计数减1。
事务管理器在创建事务对象时,需要知道当前数据库连接是否已经具有事务状态。如果尚未开启事务,事务管理器认为这个连接是新的(new状态),此时事务管理器收到commit请求时,可以放心提交事务。
如果当前连接存在事务,则很有可能在事务管理器创建事务对象之前已经对数据进行了操作,在这种情况下就不能随意的进行commit或rollback操作。
事务状态用来标记当前连接的事务状态是如何的;并且辅助事务管理器决定究竟如何处理事务提交和回滚操作。如下
public void state()
{
DataSource ds= ......;
Connection conn = DataSourceUtil.getConnection(ds);//取得数据库连接,会导致引用计数+1
conn.setAutoCommit(false);//开启事务
conn.execute("update ...");//预先执行的 update 语句
TransactionStatus status = tm.getTransaction(PROPAGATION_REQUIRED);//加入到已有事务,引用计数+1
…… //执行数据库插入
tm.commit(status);//引用计数-1
conn.commit();//递交事务
DataSourceUtil.releaseConnection(conn,ds);//释放连接,引用计数-1
}
在上面的代码中,插入数据之前,将插入数据的操作加入到已有的事务中。在插入数据后,即使显示进行了提交操作,但是由于它的事务已加入到外层事务中,因此这个提交操作是被忽略的;当外层事务提交时,才会整体提交。
l 原子性(Atomicity):事务中的所有操作,要么都做,要么都不做
l 一致性(Consistency):事务前后,数据库的状态是一致的
l 隔离性(Isolation):一个事务的执行不被其他事务干扰
l 持久性(Duration):一个事务一旦提交,对数据库的数据改变是永久性的
l 脏读
事务T1修改数据后,并将其写会数据库。事务T2读取同一数据后,T1由于某种原因回滚,此时数据被恢复到原来的值,而T2读取的值就与数据库中的数据不一致,即T2读取到的是错误的数据(读到未提交的数据),如下
|
事务T1 |
事务T2 |
t1 |
开启事务 |
|
t2 |
|
开启事务 |
t3 |
取出数据age=20 |
|
t4 |
更新数据age=30 |
|
t5 |
|
读取数据age=30 |
t6 |
rollback,age=20 |
|
t7 |
|
|
l 不可重复读
不可重复读是指事务T1读取数据后,事务T2执行更新(插入或删除)操作,当T1再次读取数据时,前后两次数据不一致。
对于事务T2插入或删除的操作,造成T1再次读取数据(报表统计)时前后两次不一致的情况,称为“幻读”。
l 修改丢失
两个事务T1和T2读入同一数据并修改,T2提交的结果破坏了T1提交的结果,导致T1的修改丢失,如下
|
事务T1 |
事务T2 |
t1 |
开启事务 |
|
t2 |
|
开启事务 |
t3 |
取出数据age=20 |
读取age=20 |
t4 |
更新数据age=30 |
|
t5 |
|
更新数据age=32 |
t6 |
提交事务 |
|
t7 |
|
提交事务 |
为了解决事务并发引起的问题,数据库采用了事务的隔离级别来解决上述问题。
l READ_UNCOMMITED(不采用事务控制)
l READ_COMMIT
l REPEATABLE_READ
l SERIALIZABLE
隔离级别与可以解决的问题如下:
|
脏读 |
重复读 |
幻读 |
Read Uncommitted |
否 |
否 |
否 |
Read Commited |
是 |
否 |
否 |
Repeatable Read |
是 |
是 |
否 |
Serializable |
是 |
是 |
是 |
注意:如果要完全正确统计报表,则需要设置隔离级别为可串行化;如果允许一定范围的误差,可以设置隔离级别为可重复读。
数据库使用锁来实现事务之间的隔离:
l 当一个事务访问某个资源时,如果执行的是select语句,则加上共享锁;如果执行的是insert、update或delete语句,则加上排它锁。
l 当第二个事务访问同样的资源时,如果执行select语句,则加上共享锁;如果执行的是insert、update、delete语句,则加上排他锁。当第二个事务加锁时,必须依据第一个事务对资源加锁的类型,来确定是等待第一个事务解锁,还是立即加锁。
共享锁用于读取数据,它允许其他事务同时读取锁定的资源,但不允许其他事务更新它。但是否会对select语句加锁,与数据库的隔离级别有关,如Mysql只有当隔离级别为Serializable时,才会select语句加共享锁。
排他锁,它锁定的资源,其他事务不能读取也不能修改。当一个事务执行insert、update或delete语句时,数据库会自动对操作的资源加排他锁。
更新锁在更新操作的初始化阶段用来锁定可能要修改的资源,从而避免使用共享锁造成的死锁,如下
|
事务T1 |
事务T2 |
t1 |
查询Id为1的记录,加共享锁 |
|
t2 |
|
查询Id为1的记录,加共享锁 |
t3 |
执行更新操作,申请排他锁,发现记录上已存在T2的共享锁,等待事务T2结束 |
|
t4 |
|
执行更新操作,申请排他锁,发现记录上已存在T1的共享锁,等待事务T1结束 |
死锁发生 |
如执行updateuser set balance=1200 where id=1时,在数据库中分为两个操作:
l 查询ID为1的记录,查询时为更新锁,允许其他事务查询,以提高并发性;
l 执行更新操作,在更新前会先把锁升级为排他锁;
|
事务T1 |
事务T2 |
t1 |
查询Id为1的记录,加更新锁 |
|
t2 |
|
查询Id为1的记录,申请更新锁,发现记录上已存在T1的更新锁,只能等待T1解锁 |
t3 |
执行更新操作,更新锁升级为排他锁 |
|
t4 |
事务结束,排他锁解除 |
|
t5 |
|
执行更新操作,更新锁升级为排他锁 |
t6 |
|
事务结束,排他锁解除 |
共享锁,排他锁,更新锁的锁兼容矩阵如下:
申请锁 |
共享锁 |
更新锁 |
排他锁 |
已加锁 |
|||
共享锁 |
Yes |
Yes |
No |
更新锁 |
Yes |
NO |
No |
排他锁 |
No |
No |
No |
当采用数据库默认的隔离级别(如Mysql为RepeatabelRead)时,可能会出现修改丢失的问题,如下
|
事务T1 |
事务T2 |
t1 |
开始事务 |
|
t2 |
balance=20 |
开始事务 |
t3 |
|
balance=20 |
t4 |
balance+=10(30) |
|
t5 |
提交事务 |
|
t6 |
|
balance-=5(15) |
t7 |
|
提交事务 |
t1时刻,事务T1开始;
t2时刻,事务T1查询数据,(Mysql默认隔离级别,并不为会select语句加共享锁);事务T2开始;
t3时刻,事务T2查询数据(Mysql默认隔离级别,并不为会select语句加共享锁);
t4时刻:事务T1更新数据,加更新锁;
t5时刻:事务T1提交,释放更新锁;
t6时刻:事务T2更新数据,加更新锁;
t7时刻:事务T2提交,释放更新锁;
最后,事务T2更新的数据,覆盖了事务T1的更新。由此可见,但两个或多个并发事务读取统一资源,然后基于最初读到的数据更新该资源,就会发生更新都是问题。
对于简单的更新操作来说,我们可以直接采用update语句,来避免更新丢失问题。但对于一些复杂的更新,往往需要先查询数据,再更新,有如下解决方案:
1) 悲观锁
悲观锁通过为select语句添加排他锁,来防止修改丢失。手工为select语句加排他锁的SQL语法如下:
select * from user where id=1 for update
注意:JPA并没有提供悲观锁的直接使用方式,若要使用悲观锁,需要使用nativesql来查询。
2) 乐观锁
乐观锁,并不是一种锁,而是一种乐观的加锁方案。原理如下:
它要求在数据库表中增加一个版本字段(version),每次执行update语句时,这个字段都会累加;当其他事务以旧版本的值更新数据时,由于前面的事务的更新操作,使version字段累加,所以其他事务会找不到相应的记录,依此来避免修改丢失。
在项目中使用乐观锁,需要在实体中增加version字段,并标注@javax.persistence.Version,在执行更新操作时实体管理器会自动更新version字段的值。
在Spring中,数据的插入、更新、删除操作必须处于事务环境中,才能操作。当在一个Bean中调用另一个Bean中的方法时,就可能会产生嵌套事务。出现嵌套事务时,前一个方法是否处在事务环境将对下一个方法产生产生什么样的影响,这成为事务的传播属性。在Spring中,事务的传播属性取值有:
l PROPAGATION_REQUIRED
如果处在事务环境中,将使用当前事务(加入已有事务);否则,开启新事务,此时提交或回滚操作都交给新开启的事务。
l PROPAGATION_SUPPORTS
如果存在事务,则使用当前事务;若不存在事务,则不使用事务;
l PROPAGATION_MANDATORY
如果存在事务,则使用当前事务;若不存在事务,将抛出异常;
l PROPAGATION_REQUIRES_NEW
创建一个新事务(内部事务);若当前存在事务,则挂起当前事务(外部事务);新开启的内部事务被完全commite或rollback而不依赖于外部事务,它拥有自己的隔离范围,自己的锁等。当内部事务开始执行时,外部事务被挂起;内部事务结束时,外部事务将继续执行。因此两个事务是相对独立的,互不影响。
所谓“挂起”指的是将当前线程使用的数据库连接,暂时保存起来不使用;取而代之的是一个新的数据库连接。当内部执行完毕后,将释放当前的连接,将挂起的数据库连接重新设为当前数据库连接。
l PROPAGATION_NOT_SUPPORT
不使用事务;若当前存在事务,则挂起当前事务,也就使用新的数据库连接,新的连接不使用事务;
l PROPAGATION_NEVER
不使用事务;若当前存在事务,则抛出异常
l PROPAGATION_NESTED
若存在当前事务(外部事务),则使用嵌套事务;如果不存在当前事务,行为类似于PROPAGATION_REQUIRED,要求要求事务管理器或者使用JDBC3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)。
PROPAGATION_NESTED开始一个“嵌套的”事务(子事务,Savepoint),它是已经存在事务的一个真正的子事务。嵌套事务开始执行时,它将先保存一个savepoint,如果嵌套事务失败,将回滚到savepoint。嵌套事务是外部事务的一部分,只有外部事务提交时,嵌套事务也会被提交;外部事务回滚时,嵌套事务也回滚。因此,外部事务和嵌套事务是一个整体。但如果是嵌套事务回滚时,只回滚到嵌套事务开始执行时的保存点(对于外部事务选择回滚,也可以选择不回滚),因此,嵌套事务可以起到分支执行的效果。如果ServiceB.methodB 失败,那么执行ServiceC.methodC(), 而ServiceB.methodB 已经回滚到它执行之前的SavePoint, 所以不会产生脏数据(相当于此方法从未执行过),这种特性可以用在某些特殊的业务中,而PROPAGATION_REQUIRED 和PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。
如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持SQL执行期间的读一致性; 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询SQL必须保证整体的读一致性,否则,在前条SQL查询之后,后条SQL查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态。
此时,应该启用事务支持read-only="true"表示该事务为只读事务,比如上面说的多条查询的这种情况可以使用只读事务,由于只读事务不存在数据的修改,因此数据库将会为只读事务提供一些优化手段。
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${databaseDriver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="initialSize" value="1"/>
<property name="minIdle" value="1"/>
<property name="maxActive" value="100"/>
<property name="maxIdle" value="20"/>
<property name="maxWait" value="1000"/>
</bean>
<!--jpa-->
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/>
<property name="persistenceUnitName" value="template"/>
<property name="packagesToScan" value="org.ssl.template.model"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="true" />
<property name="generateDdl" value="true"/>
</bean>
</property>
</bean>
<!--hibernate事务管理-->
<!--
<beanid="transactionManager"
class="org.springframework.orm.hibernate4.HibernateTransactionManager"
p:sessionFactory-ref="sessionFactory"
/>
-->
<!--jpa事务管理-->
<bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<aop:config>
<aop:pointcut id="txMethod" expression="execution(*com.ssl.template.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txMethod"/>
</aop:config>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="add*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="init*" propagation="REQUIRED" />
<tx:method name="*" propagation="REQUIRED" read-only="true"/>
</tx:attributes>
</tx:advice>
<!--注解管理事务-->
<!--
<tx:annotation-driventransaction-manager="txManager"/>
-->
版权声明:本文为博主原创文章,未经博主允许不得转载。
标签:spring 事务管理 数据库隔离级别 修改丢失 乐观锁
原文地址:http://blog.csdn.net/sunshuolei/article/details/48031307