Spring Transactional 抛出异常就能回滚事务,会在什么时候回滚

博客分类:
@Transactional spring 配置事务 注意事项
近日来,发现有很多童鞋询问:“
”,“已经声明了事务,但是无法回滚。。。”“Mybatis如果配置事务,Spring配置没起作用啊!”等等,实际上,无论怎么问或者怎么贴出代码,实际上没有人能够帮你解决这个问题的,首先Spring事务处理方式目前有五种,你用的到底是哪一种呢?回答问题的大神们不清楚,因此他们的回答和招在你那里不会起作用,因此,无论是哪一种事务处理方式首先你要弄明白你用的是哪一种,如果是杂交方式,建议选择事务处理的第四种方式:使用tx标签配置的拦截器,这个简单而且容易上手,如果用这种方式还有问题,那请往下看,有很大可能性是由于这些原因造成的,我们在罗列代码的时候一定要知其然还要知其所以然,这样你不仅能够快速的解决自己的问题,还能够把该问题解决办法与他人共享!
1. 在需要事务管理的地方加@Transactional 注解。@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上
2. @Transactional 注解只能应用到 public 可见度的方法上
。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。
3. 注意仅仅 @Transactional 注解的出现不足于开启事务行为,它仅仅 是一种元数据。必须在配置文件中使用配置元素,才真正开启了事务行为。
4. 通过 元素的 "proxy-target-class" 属性值来控制是基于接口的还是基于类的代理被创
如果 "proxy-target-class" 属值被设置为
"true",那么基于类的代理将起作用(这时需要CGLIB库cglib.jar在CLASSPATH中)。如果
"proxy-target-class" 属值被设置为 "false" 或者这个属性被省略,那么标准的JDK基于接口的代理将起作用。
&!-- JTA事务(非分布式事务), 事务配置的时候 ,不能指定dataSource属性(分布式事务,是有全局事务来管理数据库链接的)--&
&!-- 标准的JDK基于接口的代理将起作用 --&
&!-- aop切面 --&
&aop:aspectj-autoproxy proxy-target-class="false" /&
&!-- 基于类的代理将起作用 ,同时 cglib.jar必须在CLASSPATH中 --&
&!-- aop切面 --&
&aop:aspectj-autoproxy proxy-target-class="true" /&
解@Transactional cglib与java动态代理最大区别是代理目标对象不用实现接口,
那么注解要是写到接口方法上,要是使用cglib代理,这是注解事物就失效了,为了保持兼容注解最好都写到实现类方法上。
5. Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上
。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承
的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
6. @Transactional 的事务开启 ,或者是基于接口的 或者是基于类的代理被创建。所以在同一个类中一个方法调用另一个方法有事务的方法,事务是不会起作用的
public interface PersonageTempService {
//删除指定id的Personage
public void del(Integer Personageid) ;
//删除指定id的Personage,flag
public void del(Integer Personageid,boolean flag) ;
public class PersonageTempServiceBean implements PersonageTempService {
private JdbcTemplate jdbcT
public void del(Integer Personageid){
this.del(Personageid,true)
System.out.println("del success");
}catch(Exception e){
System.out.println("del failed");
@Transactional
//此时,事务根本就没有开启, 即数据库会默认提交该操作,即记录别删除掉
public void del(Integer Personageid,boolean flag){
if(flag == ture){
jdbcTemplate.update("del from Personage where id=?", new Object[]{Personageid}, new int[]{java.sql.Types.INTEGER});
throw new RuntimeException("运行期例外");
public class PersonageTempServiceBeanTest{
PersonageTempService ps = new PersonageTempServiceBean ();
ps.del(5);
7. Spring使用声明式事务处理,默认情况下,
如果被注解的数据库操作方法中发生了unchecked异常,所有的数据库操作将rollback
;如果发生的异常是checked异常,默认情况下数
据库操作还是会提
public interface PersonageService {
//删除指定id的Personage
public void del(Integer Personageid) ;
//获取Personage
public Personage getPersonage(Integer Personageid);
//PersonageServiceBean 实现了PersonageService 接口,则基于接口的还是基于类的代理 都可以实现事务
@Transactional public class PersonageServiceBean implements PersonageService {
private JdbcTemplate jdbcT
//发生了unchecked异常,事务回滚, @Transactional
public void del(Integer Personageid){
jdbcTemplate.update("del from Personage where id=?", new Object[]{Personageid},
new int[]{java.sql.Types.INTEGER});
throw new RuntimeException("运行期例外");
public interface PersonageService {
//删除指定id的Personage
public void delete(Integer Personageid) throws E
//获取Personage
public Personage getPersonage(Integer Personageid);
@Transactional
public class PersonageServiceBean implements PersonageService {
//发生了checked异常,事务不回滚,即数据库记录仍能被删除,
//checked的例外,需要我们在外部用try/catch语法对调用该方法的地方进行包含 @Transactional
public void delete(Integer Personageid) throws Exception{
jdbcTemplate.update("delete from Personage where id=?", new Object[]{Personageid},
new int[]{java.sql.Types.INTEGER});
throw new Exception("运行期例外");
但是,对于checked这种例外,默认情况下它是不会进行事务回滚的,但是
如果我们需要它进行事务回滚,这时候可以在delete方法上通过@Transaction这个注解来修改它的行为。
@Transactional
public class PersonServiceBean implements PersonService {
@Transactional(rollbackFor=Exception.class)
//rollbackFor这属性指定了,既使你出现了checked这种例外,那么它也会对事务进行回滚
public void delete(Integer personid) throws Exception{
jdbcTemplate.update("delete from person where id=?", new Object[]{personid},
new int[]{java.sql.Types.INTEGER});
throw new Exception("运行期例外");
在PersonServiceBean这个业务bean里面,有一些事务是不需要事务管理的,好比说获取数据的getPersons方法,getPerson方法。因为@Transactional 放在了类的上面。
以采用propagation这个事务属性
@Transactional(propagation=Propagation.NOT_SUPPORTED),propagation这个属性指定了
事务传播行为,我们可以指定它不支持事务,当我们这么写了之后,Spring容器在getPersons方法执行前就不会开启事务
@Transactional
public class PersonServiceBean implements PersonService {
@Transactional(propagation=Propagation.NOT_SUPPORTED)
//则此方法 就不会开启事务了
public Person getPerson(Integer personid)
浏览 17875
浏览: 107670 次
来自: 北京
lchark 写道
lchark 写道
楼主你好,我从extjs中午网看到extjs的时间控件功能,我 ...
nisersent 写道zss912317 写道请叫我东哥 写 ...
nisersent 写道zss912317 写道请叫我东哥 写 ...
(window.slotbydup=window.slotbydup || []).push({
id: '4773203',
container: s,
size: '200,200',
display: 'inlay-fix'Spring事务异常回滚,捕获异常不抛出就不会回滚 - CSDN博客
Spring事务异常回滚,捕获异常不抛出就不会回滚
最近遇到了事务不回滚的情况,我还考虑说JPA的事务有bug? 我想多了.......&&
&&为了打印清楚日志,很多方法我都加tyr catch,在catch中打印日志。但是这边情况来了,当这个方法异常时候 日志是打印了,但是加的事务却没有回滚。
& &类似这样的方法不会回滚 (一个方法出错,另一个方法不会回滚) :&
if(userSave){
userDao.save(user);
userCapabilityQuotaDao.save(capabilityQuota);
} catch (Exception e) {
(&能力开通接口,开户异常,异常信息:&+e);
下面的方法回滚(一个方法出错,另一个方法会回滚):
if(userSave){
userDao.save(user);
userCapabilityQuotaDao.save(capabilityQuota);
} catch (Exception e) {
(&能力开通接口,开户异常,异常信息:&+e);
throw new RuntimeException();
if(userSave){
userDao.save(user);
userCapabilityQuotaDao.save(capabilityQuota);
} catch (Exception e) {
(&能力开通接口,开户异常,异常信息:&+e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
为什么不会滚呢??是对Spring的事务机制就不明白。!!
& &默认spring事务只在发生未被捕获的 runtimeexcetpion时才回滚。&&
& &spring aop&&异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获runtimeexception的异常,但可以通过&&
配置来捕获特定的异常并回滚&&
&&换句话说在service的方法中不使用try catch 或者在catch中最后加上throw new runtimeexcetpion(),这样程序异常时才能被aop捕获进而回滚
&&解决方案:
&&方案1.例如service层处理事务,那么service中的方法中不做异常捕获,或者在catch语句中最后增加throw new RuntimeException()语句,以便让aop捕获异常再去回滚,并且在service上层(webservice客户端,view层action)要继续捕获这个异常并处理
&&方案2.在service层方法的catch语句中增加:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();语句,手动回滚,这样上层就无需去处理异常(现在项目的做法)
本文已收录于以下专栏:
相关文章推荐
一、使用场景举例在了解@Transactional怎么用之前我们必须要先知道@Transactional有什么用。下面举个栗子:比如一个部门里面有很多成员,这两者分别保存在部门表和成员表里面,在删除某...
(一) 用编程的方法来实现,我觉得这种方法比较灵活,控制起来比较方便,但是需要写一些额外的代码
试验方法:
         写一个单元测试,调用一个service层方法(发生对数据库进行写操作的方法--insert、update、delete)即可.
试验过程:
 &#160...
spring事务管理相关的文章已经有很多了,本人写此文章主要为自己的实验做一个记录,年纪大了,记性不好
首先先贴几个地址,有兴趣研读的同学可以参考一下:
初级使用:
http://blog.csdn....
在Spring的配置文件中,如果数据源的defaultAutoCommit设置为True了,那么方法中如果自己捕获了异常,事务是不会回滚的,如果没有自己捕获异常则事务会回滚,如下例
比如配置文件里有...
试验方法:
         写一个单元测试,调用一个service层方法(发生对数据库进行写操作的方法--insert、update、delete)即可.
试验过程:
使用spring难免要用到spring的事务管理,要用事务管理又会很自然的选择声明式的事务管理,在spring的文档中说道,spring声明式事务管理默认对非检查型异常和运行时异常进行事务回滚,而对检...
原博客地址:http://blog.csdn.net/u
一、使用场景举例
在了解@Transactional怎么用之前我们必须要先知道@Transactional有什么用。下面...
Spring Aop
异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样Aop代理才能捕获到方法的异常,才能进行回滚,默认情况下Aop只捕获RuntimeExcetpion的异常,但可...
他的最新文章
讲师:王禹华
讲师:宋宝华
您举报文章:
举报原因:
原文地址:
原因补充:
(最多只允许输入30个字)在 SegmentFault,解决技术问题
每个月,我们帮助 1000 万的开发者解决各种各样的技术问题。并助力他们在技术能力、职业生涯、影响力上获得提升。
一线的工程师、著名开源项目的作者们,都在这里:
获取验证码
已有账号?
问题对人有帮助,内容完整,我也想知道答案
问题没有实际价值,缺少关键内容,没有改进余地
还有就是多个事务方法放同一个事务方法会合并成一个事务吗?这样做会有什么隐患吗?
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
第一个问题:Spring官方建议的通知事务管理器回滚事务的方式是从事务执行上下文中抛出一个异常,当这个异常沿着堆栈抛到事务管理器中的时候,会被 catch 住,由事务管理器决定是不是需要回滚事务。
如果你使用声明式事务管理,@Transactional,默认情况下所有的 RuntimeException 会触发回滚,所有的 checked Exception 不会触发回滚。你可以通过 rollback-for 和 no-rollback-for 来调整这个配置。如果你 catch 住异常,不再抛出,异常没办法到事务管理器中,不会触发回滚操作。
第二个问题:多个事务方法放同一个事务方法,涉及到事务的传播机制,也就是 propagation,这个选项的具体配置你可以通过官方文档了解。可以控制调用另一个配置了事务的方法时如何参与当前事务。
同时当在一个事务里面,通过 this 调用当前对象里面的其他配置了事务的方法的时候,@Transactional的配置可能是无效的。具体要看你的事务管理器是通过代理方式还是CGLIB。
分享到微博?
关闭理由:
删除理由:
忽略理由:
推广(招聘、广告、SEO 等)方面的内容
与已有问题重复(请编辑该提问指向已有相同问题)
答非所问,不符合答题要求
宜作评论而非答案
带有人身攻击、辱骂、仇恨等违反条款的内容
无法获得确切结果的问题
非开发直接相关的问题
非技术提问的讨论型问题
其他原因(请补充说明)
我要该,理由是:Spring对事务的操作(@Transactional)_是时候拯救一下不开心了_新浪博客
Spring对事务的操作(@Transactional)
Spring事务的传播行为 @Transactional
标签:&spring
Spring事务的传播行为
在service类前加上@Transactional,声明这个service所有方法需要事务管理。每一个业务方法开始时都会打开一个事务。
Spring默认情况下会对运行期例外(RunTimeException)进行事务回滚。这个例外是unchecked
如果遇到checked意外就不回滚。
如何改变默认规则:
1 让checked例外也回滚:在整个方法前加上
@Transactional(rollbackFor=Exception.class)
2 让unchecked例外不回滚:
@Transactional(notRollbackFor=RunTimeException.class)
不需要事务管理的(只查询的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)
注意:&如果异常被try{}catch{}了,事务就不回滚了,如果想让事务回滚必须再往外抛try{}catch{throw
Exception}。
spring——@Transactional事务不管理jdbc,所以要自己把jdbc事务回滚。
下面给出了回滚JDBC事务的代码示例:
public&void&processT(String
orders) { &&
Context initCtx =&
&InitialContext();
javax.sql.DataSource ds =
javax.sql.DataSource)initCtx.lookup
(“java:comp/env/jdbc/OrdersDB”);
java.sql.Connection conn = ds.getConnection();
conn.setAutoCommit(&
&);&//更改JDBC事务的默认提交方式&&&
orderNo = createOrder( orders );
updateOrderStatus(orderNo, “orders created”);
&//提交JDBC事务&&&
Exception e ){ &&
conn.rollback();
&//回滚sJDBC事务&&&
&new&EJBException(“事务回滚:
“ + e.getMessage());
SQLException sqle ){
&new&EJBException(“出现SQL操作错误:
“ + sqle.getMessage());
下面给出了JTA事务代码示例:
&void&processOrder(String
orderMessage) { &&
UserTransaction transaction =
mySessionContext.getUserTransaction();
&//获得JTA事务&&&
transaction.begin();
&//开始JTA事务&&&
orderNo = sendOrder(orderMessage);
updateOrderStatus(orderNo, “order sent”);
&//提交JTA事务&&&
&catch&(Exception
transaction.rollback();
&//回滚JTA事务&&&
&catch&(SystemException
se.printStackTrace();
&new&EJBException(“事务回滚:
“ + e.getMessage());
在整个方法运行前就不会开启事务
还可以加上:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),这样就做成一个只读事务,可以提高效率。
各种属性的意义:
REQUIRED:业务方法需要在一个容器里运行。如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务。
NOT_SUPPORTED:声明方法不需要事务。如果方法没有关联到一个事务,容器不会为他开启事务,如果方法在一个事务中被调用,该事务会被挂起,调用结束后,原先的事务会恢复执行。
REQUIRESNEW:不管是否存在事务,该方法总汇为自己发起一个新的事务。如果方法已经运行在一个事务中,则原有事务挂起,新的事务被创建。
MANDATORY:该方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。如果在没有事务的环境下被调用,容器抛出例外。
SUPPORTS:该方法在某个事务范围内被调用,则方法成为该事务的一部分。如果方法在该事务范围外被调用,该方法就在没有事务的环境下执行。
NEVER:该方法绝对不能在事务范围内执行。如果在就抛例外。只有该方法没有关联到任何事务,才正常执行。
NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务,则按REQUIRED属性执行。它使用了一个单独的事务,这个事务
拥有多个可以回滚的保存点。内部事务的回滚不会对外部事务造成影响。它只对DataSourceTransactionManager事务管理器起效。
事务陷阱-1
清单 1. 使用 JDBC 的简单数据库插入
view plaincopy to
clipboardprint?@Stateless&public class
TradingServiceImpl implements TradingService
@Resource SessionContext
@Resource(mappedName="java:jdbc/tradingDS") DataSource
public long insertTrade(TradeData trade) throws Exception
Connection dbConnection =
ds.getConnection();&&&&&&&&
{&&&&&&&&&&&
Statement sql =
dbConnection.createStatement();&&&&&&&&&&&
String stmt
=&&&&&&&&&&&&&&
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE,
STATE)"&&&&&&&&&&
("&&&&&&&&&&
+ trade.getAcct() +
"','"&&&&&&&&&&
+ trade.getAction() +
"','"&&&&&&&&&&
+ trade.getSymbol() +
"',"&&&&&&&&&&
+ trade.getShares() +
","&&&&&&&&&&
+ trade.getPrice() +
",'"&&&&&&&&&&
+ trade.getState() +
"')";&&&&&&&&&&&
sql.executeUpdate(stmt,
Statement.RETURN_GENERATED_KEYS);&&&&&&&&&&&
ResultSet rs =
sql.getGeneratedKeys();&&&&&&&&&&&
if (rs.next())
{&&&&&&&&&&&&&&
rs.getBigDecimal(1).longValue();&&&&&&&&&&&
{&&&&&&&&&&&&&&
throw new Exception("Trade Order Insert
Failed");&&&&&&&&&&&
{&&&&&&&&&&&
if (dbConnection != null)
dbConnection.close();&&&&&&&&
}&&&}&@Statelesspublic
class TradingServiceImpl implements TradingService
{&& @Resource SessionContext
@Resource(mappedName="java:jdbc/tradingDS") DataSpublic
long insertTrade(TradeData trade) throws Exception
Connection dbConnection =
ds.getConnection();&&&&&
Statement sql =
dbConnection.createStatement();&&&&&&&&
String stmt
=&&&&&&&&&&&
"INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE,
STATE)"&&&&&&&&&
("&&&&&&&&&
+ trade.getAcct() +
"','"&&&&&&&&&
+ trade.getAction() +
"','"&&&&&&&&&
+ trade.getSymbol() +
"',"&&&&&&&&&
+ trade.getShares() +
","&&&&&&&&&
+ trade.getPrice() +
",'"&&&&&&&&&
+ trade.getState() +
"')";&&&&&&&&
sql.executeUpdate(stmt,
Statement.RETURN_GENERATED_KEYS);&&&&&&&&
ResultSet rs =
sql.getGeneratedKeys();&&&&&&&&
if (rs.next())
{&&&&&&&&&&&
rs.getBigDecimal(1).longValue();&&&&&&&&
{&&&&&&&&&&&
throw new Exception("Trade Order Insert
Failed");&&&&&&&&
if (dbConnection != null)
dbConnection.close();&&&&&
清单 1 中的 JDBC 代码没有包含任何事务逻辑,它只是在数据库中保存 TRADE
表中的交易订单。在本例中,数据库处理事务逻辑。
在 LUW 中,这是一个不错的单个数据库维护操作。但是如果需要在向数据库插入交易订单的同时更新帐户余款呢?如清单 2
清单 2. 在同一方法中执行多次表更新
view plaincopy to clipboardprint?public TradeData
placeTrade(TradeData trade) throws Exception
insertTrade(trade);&&&&&&&&
updateAcct(trade);&&&&&&&&
} catch (Exception up)
error&&&&&&&&
}&&&}&public
TradeData placeTrade(TradeData trade) throws Exception
insertTrade(trade);&&&&&
updateAcct(trade);&&&&&
&& } catch
(Exception up)
error&&&&&
在本例中,insertTrade() 和 updateAcct() 方法使用不带事务的标准 JDBC
代码。insertTrade() 方法结束后,数据库保存(并提交了)交易订单。如果 updateAcct()
方法由于任意原因失败,交易订单仍然会在 placeTrade() 方法结束时保存在 TRADE
表内,这会导致数据库出现不一致的数据。如果 placeTrade() 方法使用了事务,这两个活动都会包含在一个 LUW
中,如果帐户更新失败,交易订单就会回滚。
事务陷阱-2
随着 Java 持久性框架的不断普及,如 Hibernate、TopLink 和 Java 持久性 API(Java
Persistence API,JPA),我们很少再会去编写简单的 JDBC
代码。更常见的情况是,我们使用更新的对象关系映射(ORM)框架来减轻工作,即用几个简单的方法调用替换所有麻烦的 JDBC
代码。例如,要插入 清单 1 中 JDBC 代码示例的交易订单,使用带有 JPA 的 Spring Framework,就可以将
TradeData 对象映射到 TRADE 表,并用清单 3 中的 JPA 代码替换所有 JDBC 代码:
清单 3. 使用 JPA 的简单插入
view plaincopy to clipboardprint?public class TradingServiceImpl
@PersistenceContext(unitName="trading") EntityManager
public long insertTrade(TradeData trade) throws Exception
{&&&&&&&&&
em.persist(trade);&&&&&&&&&
trade.getTradeId();&&&&&&
}&&&}&public
class TradingServiceImpl
@PersistenceContext(unitName="trading") EntityM
&&& public
long insertTrade(TradeData trade) throws Exception
em.persist(trade);&&&&&&
trade.getTradeId();&&&
}}注意,清单 3 在 EntityManager 上调用了 persist()
方法来插入交易订单。很简单,是吧?其实不然。这段代码不会像预期那样向 TRADE 表插入交易订单,也不会抛出异常。它只是返回一个值 0
作为交易订单的键,而不会更改数据库。这是事务处理的主要陷阱之一:基于 ORM
的框架需要一个事务来触发对象缓存与数据库之间的同步。这通过一个事务提交完成,其中会生成 SQL
代码,数据库会执行需要的操作(即插入、更新、删除)。没有事务,就不会触发 ORM 去生成 SQL 代码和保存更改,因此只会终止方法 —
没有异常,没有更新。如果使用基于 ORM 的框架,就必须利用事务。您不再依赖数据库来管理连接和提交工作。
这些简单的示例应该清楚地说明,为了维护数据完整性和一致性,必须使用事务。不过对于在 Java
平台中实现事务的复杂性和陷阱而言,这些示例只是涉及了冰山一角。
Spring Framework @Transactional 注释陷阱-3
清单 4. 使用 @Transactional 注释
view plaincopy to clipboardprint?public class TradingServiceImpl
@PersistenceContext(unitName="trading") EntityManager
@Transactional&&&
public long insertTrade(TradeData trade) throws Exception
em.persist(trade);&&&&&&&&
trade.getTradeId();&&&&&
}&&&}&public
class TradingServiceImpl {&&
@PersistenceContext(unitName="trading") EntityM
@Transactional&& public long
insertTrade(TradeData trade) throws Exception
em.persist(trade);&&&&&
return trade.getTradeId();&&
现在重新测试代码,您发现上述方法仍然不能工作。问题在于您必须告诉 Spring
Framework,您正在对事务管理应用注释。除非您进行充分的单元测试,否则有时候很难发现这个陷阱。这通常只会导致开发人员在
Spring 配置文件中简单地添加事务逻辑,而不会使用注释。
要在 Spring 中使用 @Transactional 注释,必须在 Spring 配置文件中添加以下代码行:
view plaincopy to clipboardprint?&
transaction-manager 属性保存一个对在 Spring 配置文件中定义的事务管理器 bean
的引用。这段代码告诉 Spring 在应用事务拦截器时使用 @Transaction 注释。如果没有它,就会忽略
@Transactional 注释,导致代码不会使用任何事务。
让基本的 @Transactional 注释在 清单 4 的代码中工作仅仅是开始。注意,清单 4 使用
@Transactional 注释时没有指定任何额外的注释参数。我发现许多开发人员在使用 @Transactional
注释时并没有花时间理解它的作用。例如,像我一样在清单 4 中单独使用 @Transactional
注释时,事务传播模式被设置成什么呢?只读标志被设置成什么呢?事务隔离级别的设置是怎样的?更重要的是,事务应何时回滚工作?理解如何使用这个注释对于
确保在应用程序中获得合适的事务支持级别非常重要。回答我刚才提出的问题:在单独使用不带任何参数的 @Transactional
注释时,传播模式要设置为 REQUIRED,只读标志设置为 false,事务隔离级别设置为
READ_COMMITTED,而且事务不会针对受控异常(checked exception)回滚。
@Transactional 只读标志陷阱
我在工作中经常碰到的一个常见陷阱是 Spring @Transactional
注释中的只读标志没有得到恰当使用。这里有一个快速测试方法:在使用标准 JDBC 代码获得 Java 持久性时,如果只读标志设置为
true,传播模式设置为 SUPPORTS,清单 5 中的 @Transactional 注释的作用是什么呢?
清单 5. 将只读标志与 SUPPORTS 传播模式结合使用 — JDBC
view plaincopy to clipboardprint?@Transactional(readOnly = true,
propagation=Propagation.SUPPORTS)&&&public
long insertTrade(TradeData trade) throws Exception
Code...&&&}&@Transactional(readOnly
= true, propagation=Propagation.SUPPORTS)public long
insertTrade(TradeData trade) throws Exception
{&& //JDBC Code...}
当执行清单 5 中的 insertTrade()
方法时,猜一猜会得到下面哪一种结果:抛出一个只读连接异常&正确插入交易订单并提交数据&什么也不做,因为传播级别被设置为
SUPPORTS&是哪一个呢?正确答案是 B。交易订单会被正确地插入到数据库中,即使只读标志被设置为
true,且事务传播模式被设置为 SUPPORTS。但这是如何做到的呢?由于传播模式被设置为
SUPPORTS,所以不会启动任何事物,因此该方法有效地利用了一个本地(数据库)事务。只读标志只在事务启动时应用。在本例中,因为没有启动任何事
务,所以只读标志被忽略。
Spring Framework @Transactional 注释陷阱-4
清单 6 中的 @Transactional 注释在设置了只读标志且传播模式被设置为 REQUIRED
时,它的作用是什么呢?
清单 6. 将只读标志与 REQUIRED 传播模式结合使用 — JDBC
view plaincopy to clipboardprint?@Transactional(readOnly = true,
propagation=Propagation.REQUIRED)&&&public
long insertTrade(TradeData trade) throws Exception
code...&&&}&@Transactional(readOnly
= true, propagation=Propagation.REQUIRED)public long
insertTrade(TradeData trade) throws Exception
{&& //JDBC code...}
执行清单 6 中的 insertTrade() 方法会得到下面哪一种结果呢:
抛出一个只读连接异常&正确插入交易订单并提交数据&什么也不做,因为只读标志被设置为
true&根据前面的解释,这个问题应该很好回答。正确的答案是
A。会抛出一个异常,表示您正在试图对一个只读连接执行更新。因为启动了一个事务(REQUIRED),所以连接被设置为只读。毫无疑问,在试图执行
SQL 语句时,您会得到一个异常,告诉您该连接是一个只读连接。
关于只读标志很奇怪的一点是:要使用它,必须启动一个事务。如果只是读取数据,需要事务吗?答案是根本不需要。启动一个事务来执行只读操作会增加处
理线程的开销,并会导致数据库发生共享读取锁定(具体取决于使用的数据库类型和设置的隔离级别)。总的来说,在获取基于 JDBC 的
Java 持久性时,使用只读标志有点毫无意义,并会启动不必要的事务而增加额外的开销。
使用基于 ORM 的框架会怎样呢?按照上面的测试,如果在结合使用 JPA 和 Hibernate 时调用
insertTrade() 方法,清单 7 中的 @Transactional 注释会得到什么结果?
清单 7. 将只读标志与 REQUIRED 传播模式结合使用 — JPA
view plaincopy to clipboardprint?@Transactional(readOnly = true,
propagation=Propagation.REQUIRED)&&&public
long insertTrade(TradeData trade) throws Exception
em.persist(trade);&&&&&
trade.getTradeId();&&&}&@Transactional(readOnly
= true, propagation=Propagation.REQUIRED)public long
insertTrade(TradeData trade) throws Exception
em.persist(trade);&& return
trade.getTradeId();}
清单 7 中的 insertTrade() 方法会得到下面哪一种结果:
抛出一个只读连接异常&正确插入交易订单并提交数据&什么也不做,因为
readOnly 标志被设置为 true&正确的答案是
B。交易订单会被准确无误地插入数据库中。请注意,上一示例表明,在使用 REQUIRED 传播模式时,会抛出一个只读连接异常。使用
JDBC 时是这样。使用基于 ORM 的框架时,只读标志只是对数据库的一个提示,并且一条基于 ORM 框架的指令(本例中是
Hibernate)将对象缓存的 flush 模式设置为
NEVER,表示在这个工作单元中,该对象缓存不应与数据库同步。不过,REQUIRED
传播模式会覆盖所有这些内容,允许事务启动并工作,就好像没有设置只读标志一样。
这令我想到了另一个我经常碰到的主要陷阱。阅读了前面的所有内容后,您认为如果只对 @Transactional
注释设置只读标志,清单 8 中的代码会得到什么结果呢?
清单 8. 使用只读标志 — JPA
view plaincopy to clipboardprint?@Transactional(readOnly =
true)&&&public
TradeData getTrade(long tradeId) throws Exception
return em.find(TradeData.class,
tradeId);&&&}&@Transactional(readOnly
= true)public TradeData getTrade(long tradeId) throws Exception
{&& return
em.find(TradeData.class, tradeId);}
清单 8 中的 getTrade() 方法会执行以下哪一种操作?
启动一个事务,获取交易订单,然后提交事务&获取交易订单,但不启动事务&正确的答案是
A。一个事务会被启动并提交。不要忘了,@Transactional 注释的默认传播模式是
REQUIRED。这意味着事务会在不必要的情况下启动。根据使用的数据库,这会引起不必要的共享锁,可能会使数据库中出现死锁的情况。此外,启动和停止
事务将消耗不必要的处理时间和资源。总的来说,在使用基于 ORM
的框架时,只读标志基本上毫无用处,在大多数情况下会被忽略。但如果您坚持使用它,请记得将传播模式设置为 SUPPORTS(如清单 9
所示),这样就不会启动事务:清单 9. 使用只读标志和 SUPPORTS 传播模式进行选择操作
view plaincopy to clipboardprint?@Transactional(readOnly = true,
propagation=Propagation.SUPPORTS)&&&public
TradeData getTrade(long tradeId) throws Exception
return em.find(TradeData.class,
tradeId);&&&}&@Transactional(readOnly
= true, propagation=Propagation.SUPPORTS)public TradeData
getTrade(long tradeId) throws Exception
{&& return
em.find(TradeData.class, tradeId);}
另外,在执行读取操作时,避免使用 @Transactional 注释,如清单 10 所示:
清单 10. 删除 @Transactional 注释进行选择操作
view plaincopy to clipboardprint?public TradeData getTrade(long
tradeId) throws Exception
return em.find(TradeData.class,
tradeId);&&&}&public
TradeData getTrade(long tradeId) throws Exception
{&& return
em.find(TradeData.class, tradeId);}
REQUIRES_NEW 事务属性陷阱
不管是使用 Spring Framework,还是使用 EJB,使用 REQUIRES_NEW
事务属性都会得到不好的结果并导致数据损坏和不一致。REQUIRES_NEW
事务属性总是会在启动方法时启动一个新的事务。许多开发人员都错误地使用 REQUIRES_NEW
属性,认为它是确保事务启动的正确方法。
Spring Framework @Transactional 注释陷阱-5
清单 11. 使用 REQUIRES_NEW 事务属性
view plaincopy to
clipboardprint?@Transactional(propagation=Propagation.REQUIRES_NEW)&&&public
long insertTrade(TradeData trade) throws Exception
{...}&&&@Transactional(propagation=Propagation.REQUIRES_NEW)&&&public
void updateAcct(TradeData trade) throws Exception
{...}&@Transactional(propagation=Propagation.REQUIRES_NEW)public
long insertTrade(TradeData trade) throws Exception {...}
@Transactional(propagation=Propagation.REQUIRES_NEW)public void
updateAcct(TradeData trade) throws Exception {...}
注意,清单 11 中的两个方法都是公共方法,这意味着它们可以单独调用。当使用 REQUIRES_NEW
属性的几个方法通过服务间通信或编排在同一逻辑工作单元内调用时,该属性就会出现问题。例如,假设在清单 11
中,您可以独立于一些用例中的任何其他方法来调用 updateAcct() 方法,但也有在 insertTrade() 方法中调用
updateAcct() 方法的情况。现在如果调用 updateAcct()
方法后抛出异常,交易订单就会回滚,但帐户更新将会提交给数据库,如清单 12 所示:
清单 12. 使用 REQUIRES_NEW 事务属性的多次更新
view plaincopy to
clipboardprint?@Transactional(propagation=Propagation.REQUIRES_NEW)&&&public
long insertTrade(TradeData trade) throws Exception
em.persist(trade);&&&&&
updateAcct(trade);&&&&&
//exception occurs here! Trade rolled back but account update is
...&&&}&@Transactional(propagation=Propagation.REQUIRES_NEW)public
long insertTrade(TradeData trade) throws Exception
em.persist(trade);&&
updateAcct(trade);&& //exception
occurs here! Trade rolled back but account update is
not!&& ...}
之所以会发生这种情况是因为 updateAcct() 方法中启动了一个新事务,所以在 updateAcct()
方法结束后,事务将被提交。使用 REQUIRES_NEW
事务属性时,如果存在现有事务上下文,当前的事务会被挂起并启动一个新事务。方法结束后,新的事务被提交,原来的事务继续执行。
由于这种行为,只有在被调用方法中的数据库操作需要保存到数据库中,而不管覆盖事务的结果如何时,才应该使用 REQUIRES_NEW
事务属性。比如,假设尝试的所有股票交易都必须被记录在一个审计数据库中。出于验证错误、资金不足或其他原因,不管交易是否失败,这条信息都需要被持久
化。如果没有对审计方法使用 REQUIRES_NEW 属性,审计记录就会连同尝试执行的交易一起回滚。使用 REQUIRES_NEW
属性可以确保不管初始事务的结果如何,审计数据都会被保存。这里要注意的一点是,要始终使用 MANDATORY 或 REQUIRED
属性,而不是 REQUIRES_NEW,除非您有足够的理由来使用它,类似审计示例中的那些理由。
事务回滚陷阱
我将最常见的事务陷阱留到最后来讲。遗憾的是,我在生产代码中多次遇到这个错误。我首先从 Spring Framework
开始,然后介绍 EJB 3。
到目前为止,您研究的代码类似清单 13 所示:
清单 13. 没有回滚支持
view plaincopy to
clipboardprint?@Transactional(propagation=Propagation.REQUIRED)&&&public
TradeData placeTrade(TradeData trade) throws Exception
insertTrade(trade);&&&&&&&&
updateAcct(trade);&&&&&&&&
} catch (Exception up)
error&&&&&&&&
}&&&}&@Transactional(propagation=Propagation.REQUIRED)public
TradeData placeTrade(TradeData trade) throws Exception
insertTrade(trade);&&&&&
updateAcct(trade);&&&&&
&& } catch
(Exception up)
error&&&&&
}}假设帐户中没有足够的资金来购买需要的股票,或者还没有准备购买或出售股票,并抛出了一个受控异常(例如
FundsNotAvailableException),那么交易订单会保存在数据库中吗?还是整个逻辑工作单元将执行回滚?答案出乎意料:根据受控异
常(不管是在 Spring Framework 中还是在 EJB 中),事务会提交它还未提交的所有工作。使用清单
13,这意味着,如果在执行 updateAcct() 方法期间抛出受控异常,就会保存交易订单,但不会更新帐户来反映交易情况。
这可能是在使用事务时出现的主要数据完整性和一致性问题了。运行时异常(即非受控异常)自动强制执行整个逻辑工作单元的回滚,但受控异常不会。因此,清单
13 中的代码从事务角度来说毫无用处;尽管看上去它使用事务来维护原子性和一致性,但事实上并没有。
尽管这种行为看起来很奇怪,但这样做自有它的道理。首先,不是所有受控异常都是不好的;它们可用于事件通知或根据某些条件重定向处理。但更重要的
是,应用程序代码会对某些类型的受控异常采取纠正操作,从而使事务全部完成。例如,考虑下面一种场景:您正在为在线书籍零售商编写代码。要完成图书的订
单,您需要将电子邮件形式的确认函作为订单处理的一部分发送。如果电子邮件服务器关闭,您将发送某种形式的 SMTP
受控异常,表示邮件无法发送。如果受控异常引起自动回滚,整个图书订单就会由于电子邮件服务器的关闭全部回滚。通过禁止自动回滚受控异常,您可以捕获该异
常并执行某种纠正操作(如向挂起队列发送消息),然后提交剩余的订单。
Spring Framework @Transactional 注释陷阱-6
使用 Declarative 事务模式时,必须指定容器或框架应该如何处理受控异常。在 Spring Framework 中,通过
@Transactional 注释中的 rollbackFor 参数进行指定,如清单 14 所示:
清单 14. 添加事务回滚支持 — Spring
view plaincopy to
clipboardprint?@Transactional(propagation=Propagation.REQUIRED,
rollbackFor=Exception.class)&&&public
TradeData placeTrade(TradeData trade) throws Exception
insertTrade(trade);&&&&&&&&
updateAcct(trade);&&&&&&&&
} catch (Exception up)
error&&&&&&&&
}&&&}&@Transactional(propagation=Propagation.REQUIRED,
rollbackFor=Exception.class)public TradeData placeTrade(TradeData
trade) throws Exception {&& try
insertTrade(trade);&&&&&
updateAcct(trade);&&&&&
&& } catch
(Exception up)
error&&&&&
注意,@Transactional 注释中使用了 rollbackFor
参数。这个参数接受一个单一异常类或一组异常类,您也可以使用 rollbackForClassName 参数将异常的名称指定为 Java
类型。还可以使用此属性的相反形式(noRollbackFor)指定除某些异常以外的所有异常应该强制回滚。通常大多数开发人员指定
Exception.class 作为值,表示该方法中的所有异常应该强制回滚。
在回滚事务这一点上,EJB 的工作方式与 Spring Framework 稍微有点不同。EJB 3.0 规范中的
@TransactionAttribute 注释不包含指定回滚行为的指令。必须使用
SessionContext.setRollbackOnly() 方法将事务标记为执行回滚,如清单 15 所示:
清单 15. 添加事务回滚支持 — EJB
view plaincopy to
clipboardprint?@TransactionAttribute(TransactionAttributeType.REQUIRED)&&&public
TradeData placeTrade(TradeData trade) throws Exception
insertTrade(trade);&&&&&&&&
updateAcct(trade);&&&&&&&&
} catch (Exception up)
error&&&&&&&&
sessionCtx.setRollbackOnly();&&&&&&&&
}&&&}&@TransactionAttribute(TransactionAttributeType.REQUIRED)public
TradeData placeTrade(TradeData trade) throws Exception
insertTrade(trade);&&&&&
updateAcct(trade);&&&&&
&& } catch
(Exception up)
error&&&&&
sessionCtx.setRollbackOnly();&&&&&
setRollbackOnly()
方法后,就不能改变主意了;惟一可能的结果是在启动事务的方法完成后回滚事务。本系列后续文章中描述的事务策略将介绍何时、何处使用回滚指令,以及何时使用
REQUIRED 与 MANDATORY 事务属性。
Isolation Level(事务隔离等级)
1、Serializable:最严格的级别,事务串行执行,资源消耗最大;2、REPEATABLE
READ:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。3、READ
COMMITTED:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。4、Read
Uncommitted:保证了读取过程中不会读取到非法数据。隔离级别在于处理多事务的并发问题。我们知道并行可以提高数据库的吞吐量和效率,但是并不是所有的并发事务都可以并发运行。我们首先说并发中可能发生的3中不讨人喜欢的事情1:
reads--读脏数据。也就是说,比如事务A的未提交(还依然缓存)的数据被事务B读走,如果事务A失败回滚,会导致事务B所读取的的数据是错误的。2:
non-repeatable
reads--数据不可重复读。比如事务A中两处读取数据-total-的值。在第一读的时候,total是100,然后事务B就把total的数据改成
200,事务A再读一次,结果就发现,total竟然就变成200了,造成事务A数据混乱。3: phantom
reads--幻象读数据,这个和non-repeatable
reads相似,也是同一个事务中多次读不一致的问题。但是non-repeatable
reads的不一致是因为他所要取的数据集被改变了(比如total的数据),但是phantom
reads所要读的数据的不一致却不是他所要读的数据集改变,而是他的条件数据集改变。比如Select account.id where
account.name="ppgogo*",第一次读去了6个符合条件的id,第二次读取的时候,由于事务b把一个帐号的名字由"dd"改
成"ppgogo1",结果取出来了7个数据。
事务属性中的readOnly标志表示对应的事务应该被最优化为只读事务。
博客等级:
博客积分:0
博客访问:4,818
关注人气:0
荣誉徽章:

我要回帖

更多关于 jdbc 抛出异常 回滚 的文章

 

随机推荐