在整个方法前加上,确实是事务未回滚导致的数

2019-10-05 02:40栏目:美高梅开户送58元官网
TAG:

网上看到说原因是: Spring之所以可以对开启@Transactional的方法进行事务管理,是因为Spring为当前类生成了一个代理类,然后在执行相关方法时,会判断这个方法有没有@Transactional注解,如果有的话,则会开启一个事务。但是,上面同一个类中A调用方式时,在调用B时,使用的并不是代理对象,从而导致this.B时也不是代理对象,从而导致@Transactional失败。所以,Spring 从同一个类中的某个方法调用另一个有注解(@Transactional)的方法时,事务会失效,些事务的时候一定要注意到这一点。

17.5.3 声明式事务的回滚

SUPPORTS:该方法在某个事务范围内被调用,则方法成为该事务的一部分。如果方法在该事务范围外被调用,该方法就在没有事务的环境下执行。

今天有个同事遇到一个问题,由于业务需求要求,在一个Service的一个方法A中有一个for循环,每次循环里面的业务逻辑有可能发生异常,这个时候就需要将这个循环内的所有数据库操作给回滚掉,但是又不能影响到之前循环里数据的更改,并且后面的循环里不发生异常的情况下也需要正常操作数据库。同事尝试了很久结果还是不能满足业务需求。大概业务逻辑需求如下:

在一段业务逻辑中对数据库异常进行了处理,使用了try...catch子句捕获异常并throw了一个自定义异常,这种情况导致了事务未回滚,示例代码如下:

由于这种行为,只有在被调用方法中的数据库操作需要保存到数据库中,而不管覆盖事务的结果如何时,才应该使用 REQUIRES_NEW 事务属性。比如,假设尝试的所有股票交易都必须被记录在一个审计数据库中。出于验证错误、资金不足或其他原因,不管交易是否失败,这条信息都需要被持久化。如果没有对审计方法使用 REQUIRES_NEW 属性,审计记录就会连同尝试执行的交易一起回滚。使用 REQUIRES_NEW 属性可以确保不管初始事务的结果如何,审计数据都会被保存。这里要注意的一点是,要始终使用 MANDATORY 或 REQUIRED 属性,而不是 REQUIRES_NEW,除非您有足够的理由来使用它,类似审计示例中的那些理由。

  • 还有帖子说手动回滚,所以上面代码catch中加上TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 但是结果并没有回滚,反而A中catch到报出错误:

大概就是 Service 中有一个方法 A,会内部调用方法 B, 方法 A 没有事务管理,方法 B 采用了声明式事务,通过在方法上声明 Transactional 的注解来做事务管理。示例代码如下:

当执行清单 5 中的 insertTrade() 方法时,猜一猜会得到下面哪一种结果:
抛出一个只读连接异常
正确插入交易订单并提交数据
什么也不做,因为传播级别被设置为 SUPPORTS
是哪一个呢?正确答案是 B。交易订单会被正确地插入到数据库中,即使只读标志被设置为 true,且事务传播模式被设置为 SUPPORTS。但这是如何做到的呢?由于传播模式被设置为 SUPPORTS,所以不会启动任何事物,因此该方法有效地利用了一个本地(数据库)事务。只读标志只在事务启动时应用。在本例中,因为没有启动任何事务,所以只读标志被忽略。

`package com.dubboconsumer.dubboConsumer.service.imp;import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;import com.dubboconsumer.dubboConsumer.model.User;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;@Service@Transactionalpublic class DemoService { @Resource private DemoMapper demoMapper; public void A(){ for (int i = 1;i<5;i++){ try { this.B; }catch (Exception e){ e.printStackTrace(); } } } @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW) public void B{ try { User user = new User(); if { user.setId; user.setName; user.setAge; demoMapper.insertUser; int a = 1/0;//抛出ArithmeticException异常 }else { user.setId; user.setName; user.setAge; demoMapper.insertUser; } }catch (Exception e){ e.printStackTrace(); //TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); throw new RuntimeException; } }}

如果可能的话,强烈推荐您使用声明式事务方式回滚事务,对于编程式事务,如果你强烈需要它,也是可以使用的,but its usage flies in the face of achieving a clean POJO-based architecture.(没懂...)

view plaincopy to clipboardprint?
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
throw up;
}
}

疑问,确实像往常一样在service上添加了注解@Transactional,为什么查询数据库时还是发现有数据不一致的情况,想想肯定是事务没起作用,出现异常的时候数据没有回滚。于是就对相关代码进行了一番测试,结果发现一下踩进了两个坑,确实是事务未回滚导致的数据不一致。

}
注意,清单 3 在 EntityManager 上调用了 persist() 方法来插入交易订单。很简单,是吧?其实不然。这段代码不会像预期那样向 TRADE 表插入交易订单,也不会抛出异常。它只是返回一个值 0 作为交易订单的键,而不会更改数据库。这是事务处理的主要陷阱之一:基于 ORM 的框架需要一个事务来触发对象缓存与数据库之间的同步。这通过一个事务提交完成,其中会生成 SQL 代码,数据库会执行需要的操作(即插入、更新、删除)。没有事务,就不会触发 ORM 去生成 SQL 代码和保存更改,因此只会终止方法 — 没有异常,没有更新。如果使用基于 ORM 的框架,就必须利用事务。您不再依赖数据库来管理连接和提交工作。

运行代码A中catch到算术异常,这时再看数据库中数据,只有1、3、4三条记录,说明插入2这个数据时发生异常数据被回滚,没有插入数据库。

@Transactional(rollbackForClassName={"Exception"})
或者
@Transactional(rollbackFor={Exception.class})

@Transactional
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}
public class TradingServiceImpl {
@PersistenceContext(unitName=”trading”) EntityManager em;

大概意思是这个类里面没有事务,不能进行手动回滚。

public void resolvePosition() {
 try {
   // some business logic...
 } catch (NoProductInStockException ex) {
   // trigger rollback programmatically
   TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
 }
}

Spring Framework @Transactional 注释陷阱-5

这样修改之后执行到 int a = 1/0; 异常会被try.catch到,但是再手动抛出异常时,异常又被方法A中Catch到,数据库里还是插入了1、2、3、4四条数据,所以事务并没有生效。

主要掌握声明式的事务管理。

清单 7 中的 insertTrade() 方法会得到下面哪一种结果:

  • 首先数据库方面,因为我们的开发库之前就有事务回滚各种操作,我本地数据库(写demo用的本地库)的存储引擎也是InnoDB(mysql的引擎是InnoDB才支持事务回滚)。
  • 异常类型方面,在方法B中的业务逻辑加上了try.catch块,然后再手动抛出一个RuntimeException,@Transactional加上rollbackFor = Exception.class

在你遇到异常不想回滚事务的时候,同样的你也可指定不回滚的规则,下面的一个例子告诉你,即使遇到未处理的 InstrumentNotFoundException 异常时,Spring FrameWork 的事务框架同样会提交事务,而不回滚。

在回滚事务这一点上,EJB 的工作方式与 Spring Framework 稍微有点不同。EJB 3.0 规范中的 @TransactionAttribute 注释不包含指定回滚行为的指令。必须使用 SessionContext.setRollbackOnly() 方法将事务标记为执行回滚,如清单 15 所示:

你可以精确的配置异常类型,指定此异常类事务回滚,包括 checked 异常。下面的xml代码片段展示了如何配置checked异常引起事务回滚,应用自定义异常类型:

readOnly
事务属性中的readOnly标志表示对应的事务应该被最优化为只读事务。

然后在原来的Service中调用DoInsertService的B方法,

下面总结一下经验教训:

在本例中,insertTrade() 和 updateAcct() 方法使用不带事务的标准 JDBC 代码。insertTrade() 方法结束后,数据库保存(并提交了)交易订单。如果 updateAcct() 方法由于任意原因失败,交易订单仍然会在 placeTrade() 方法结束时保存在 TRADE 表内,这会导致数据库出现不一致的数据。如果 placeTrade() 方法使用了事务,这两个活动都会包含在一个 LUW 中,如果帐户更新失败,交易订单就会回滚。

org.springframework.transaction.NoTransactionException: No transaction aspect-managed TransactionStatus in scope at org.springframework.transaction.interceptor.TransactionAspectSupport.currentTransactionStatus(TransactionAspectSupport.java:122) at com.dubboconsumer.dubboConsumer.service.imp.DemoService.B(DemoService.java:44) at com.dubboconsumer.dubboConsumer.service.imp.DemoService.A(DemoService.java:20) at com.dubboconsumer.dubboConsumer.service.imp.DemoService$$FastClassBySpringCGLIB$$c7449c96.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:669) ...

单元测试代码如下:

NOT_SUPPORTED:声明方法不需要事务。如果方法没有关联到一个事务,容器不会为他开启事务,如果方法在一个事务中被调用,该事务会被挂起,调用结束后,原先的事务会恢复执行。

package com.dubboconsumer.dubboConsumer.service.imp;import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;import com.dubboconsumer.dubboConsumer.model.User;import org.springframework.stereotype.Repository;import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;@Repository@Transactional(rollbackFor = Exception.class)public class DoInsertService { @Resource private DemoMapper demoMapper; public void B{ User user = new User(); if { user.setId; user.setName; user.setAge; demoMapper.insertUser; int a = 1/0; //抛出ArithmeticException异常 }else { user.setId; user.setName; user.setAge; demoMapper.insertUser; } }}

还有更灵活的回滚规则配置方法,同时指定什么异常回滚,什么异常不回滚。当Spring FrameWork 的事务框架捕获到一个异常的时候,会去匹配配置的回滚规则来决定是否标记回滚事务,使用匹配度最强的规则结果。因此,下面的配置例子表达的意思是,除了异常 InstrumentNotFoundException 之外的任何异常都会导致事务回滚。

NEVER:该方法绝对不能在事务范围内执行。如果在就抛例外。只有该方法没有关联到任何事务,才正常执行。

如果第一次循环没有发生异常,第二次循环发生异常,第三、四次循环都没有发生异常,那么最后数据表里有 1 3 4 三条记录,第二次插入数据没有写入,所以不能单单的在Service上加上事务。按照这个业务需求我的想法在Service上加上@Transactional事务,将可能发生异常的业务代码提到另外一个方法B中,在B方法上面加上一个@Transactional注解,并且将B方法上事务传播行为改成PROPAGATION_REQUIRES_NEW(创建新事务,无论当前存不存在事务,都创建新事务),并且在第二次循环手动设置一个算术异常,Demo代码如下:

看完官方文档这节内容找到了问题的答案,原来是因为我们自定义的异常不是 RuntimeException。我的解决办法是,在注解@Transactional中添加 rollbackFor={BizException.class}。可能你会问我为什么不将自定义异常修改为继承RuntimeException,因为我需要BizException是一个checked 异常。

1 让checked例外也回滚:在整个方法前加上 @Transactional(rollbackFor=Exception.class)

package com.dubboconsumer.dubboConsumer.service.imp;import com.dubboconsumer.dubboConsumer.Mapper.DemoMapper;import com.dubboconsumer.dubboConsumer.model.User;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;@Service@Transactionalpublic class DemoService { @Resource private DemoMapper demoMapper; public void A(){ for (int i = 1;i<5;i++){ try { this.B; }catch (Exception e){ e.printStackTrace(); } } } @Transactional(propagation = Propagation.REQUIRES_NEW) public void B{//可能发生异常的方法B User user = new User(); if { user.setId; user.setName; user.setAge; demoMapper.insertUser;//往user表插入一条用户记录 int a = 1/0;//抛出ArithmeticException异常 }else { user.setId; user.setName; user.setAge; demoMapper.insertUser; } }}

上面代码中的声明式事务在出现异常的时候,事务是不会回滚的。在代码中我虽然捕获了异常,但是同时我也抛出了异常,为什么事务未回滚呢?猜测是异常类型不对,于是开始查询原因,翻看了Spring的官方文档,找到了答案。下面是翻译自Spring官网。

@Transactional
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
}

for(int i = 1;i<5;i++){ /*** **业务逻辑: 例如:每循环一次向数据库user表插入一条记录**/}

与其有同等作用的注解形式如下:

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 not!

}
@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!

}

package com.dubboconsumer.dubboConsumer.service.imp;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;@Servicepublic class DemoService { @Resource private DoInsertService doInsertService; public void A(){ for (int i = 1;i<5;i++){ try { doInsertService.B; }catch (Exception e){ e.printStackTrace(); } } }}

1. Service类内部方法调用

view plaincopy to clipboardprint?
@Stateless
public class TradingServiceImpl implements TradingService {
@Resource SessionContext ctx;
@Resource(mappedName=”java:jdbc/tradingDS”) DataSource ds;

问题还是没有解决,然后我又去网上翻各种帖子,在一个类似问题帖子回复中看到说,事务没有回滚可能是因为写在同一个类里,然后我就试了新建一个DoInsertService ,将方法B中的业务逻辑放到这个新建的DoInsertService ,并且在Service上加上@Transactional,代码如下:

Spring的声明式事务是通过AOP实现的

Isolation Level(事务隔离等级)

运行代码,在代码执行到第二个循环int a = 1/0 时抛出异常,异常被方法A中catch到,但是数据库没有回滚,而是插入了 1、 2、3、4 四条记录,说明事务并没有生效。然后网上找各种帖子,各种尝试:

编程式的事务管理

让基本的 @Transactional 注释在 清单 4 的代码中工作仅仅是开始。注意,清单 4 使用 @Transactional 注释时没有指定任何额外的注释参数。我发现许多开发人员在使用 @Transactional 注释时并没有花时间理解它的作用。例如,像我一样在清单 4 中单独使用 @Transactional 注释时,事务传播模式被设置成什么呢?只读标志被设置成什么呢?事务隔离级别的设置是怎样的?更重要的是,事务应何时回滚工作?理解如何使用这个注释对于确保在应用程序中获得合适的事务支持级别非常重要。回答我刚才提出的问题:在单独使用不带任何参数的 @Transactional 注释时,传播模式要设置为 REQUIRED,只读标志设置为 false,事务隔离级别设置为 READ_COMMITTED,而且事务不会针对受控异常(checked exception)回滚。

上一节中介绍了如何设置开启Spring事务,一般在你的应用的Service层代码中设置,这一节将介绍在简单流行的声明式事务中如何控制事务回滚。

执行清单 6 中的 insertTrade() 方法会得到下面哪一种结果呢:

通过使用TransactionTemplate 手动管理事务

public long insertTrade(TradeData trade) throws Exception { 
   em.persist(trade); 
   return trade.getTradeId(); 
} 

总结一下导致事务不回滚的两个原因,一是Service类内部方法调用,二是try...catch异常。

REQUIRES_NEW 事务属性陷阱

在Spring FrameWork 的事务框架中推荐的事务回滚方法是,在当前执行的事务上下文中抛出一个异常。如果异常未被处理,当抛出异常调用堆栈的时候,Spring FrameWork 的事务框架代码将捕获任何未处理的异常,然后并决定是否将此事务标记为回滚。

在 LUW 中,这是一个不错的单个数据库维护操作。但是如果需要在向数据库插入交易订单的同时更新帐户余款呢?如清单 2 所示:

注:Unchecked Exception包括Error与RuntimeException. RuntimeException的所有子类也都属于此类。另一类就是checked Exception。

原文:

public class RabbitServiceImplTest {

  @Autowired
  private RabbitService rabbitService;

  // 事务未开启
  @Test
  public void testA(){
    rabbitService.methodA("rabbit");
  }

  // 事务开启
  @Test
  public void testB(){
    rabbitService.methodB("rabbit");
  }
}

清单 7. 将只读标志与 REQUIRED 传播模式结合使用 — JPA

2. try...catch异常

3 不需要事务管理的(只查询的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)

声明式的事务管理

public long insertTrade(TradeData trade) throws Exception {   
   em.persist(trade);   
   return trade.getTradeId();   
}   

以上这篇完美解决Spring声明式事务不回滚的问题就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持脚本之家。

view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
//JDBC code…
}
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
//JDBC code…
}

对于加了@Transactional注解的方法来说,在调用代理类的方法时,会先通过拦截器TransactionInterceptor开启事务,然后在调用目标类的方法,最后在调用结束后,TransactionInterceptor 会提交或回滚事务,大致流程如下图:

view plaincopy to clipboardprint?
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
sessionCtx.setRollbackOnly();
throw up;
}
}
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public TradeData placeTrade(TradeData trade) throws Exception {
try {
insertTrade(trade);
updateAcct(trade);
return trade;
} catch (Exception up) {
//log the error
sessionCtx.setRollbackOnly();
throw up;
}
}
调用 setRollbackOnly() 方法后,就不能改变主意了;惟一可能的结果是在启动事务的方法完成后回滚事务。本系列后续文章中描述的事务策略将介绍何时、何处使用回滚指令,以及何时使用 REQUIRED 与 MANDATORY 事务属性。

@Service
public class RabbitServiceImpl implements RabbitService {

  @Autowired
  private RabbitDao rabbitDao;
  @Autowired
  private TortoiseDao tortoiseDao;

  @Override
  public Rabbit methodA(String name){
    return methodB(name);
  }

  @Transactional(propagation = Propagation.REQUIRED)
  public boolean methodB(String name){
    rabbitDao.insertRabbit(name);
    tortoiseDao.insertTortoise(name);
    return true;
  }

}

transaction-manager 属性保存一个对在 Spring 配置文件中定义的事务管理器 bean 的引用。这段代码告诉 Spring 在应用事务拦截器时使用 @Transaction 注释。如果没有它,就会忽略 @Transactional 注释,导致代码不会使用任何事务。

在默认配置中,Spring FrameWork 的事务框架代码只会将出现runtime, unchecked 异常的事务标记为回滚;也就是说事务中抛出的异常时RuntimeException或者是其子类,这样事务才会回滚(默认情况下Error也会导致事务回滚)。在默认配置的情况下,所有的 checked 异常都不会引起事务回滚。

清单 8. 使用只读标志 — JPA

public class BizException extends Exception {
  // 自定义异常
}

抛出一个只读连接异常
正确插入交易订单并提交数据
什么也不做,因为只读标志被设置为 true
根据前面的解释,这个问题应该很好回答。正确的答案是 A。会抛出一个异常,表示您正在试图对一个只读连接执行更新。因为启动了一个事务(REQUIRED),所以连接被设置为只读。毫无疑问,在试图执行 SQL 语句时,您会得到一个异常,告诉您该连接是一个只读连接。

实际应用中很少使用

view plaincopy to clipboardprint?
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}
@Transactional(readOnly = true, propagation=Propagation.REQUIRED)
public long insertTrade(TradeData trade) throws Exception {
em.persist(trade);
return trade.getTradeId();
}

与其有同样作用的注解形式如下:   

清单 1 中的 JDBC 代码没有包含任何事务逻辑,它只是在数据库中保存 TRADE 表中的交易订单。在本例中,数据库处理事务逻辑。

开发中推荐使用(代码侵入最少)

  • “VALUES (”
  • 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()) {
    return rs.getBigDecimal(1).longValue();
    } else {
    throw new Exception(“Trade Order Insert Failed”);
    }
    } finally {
    if (dbConnection != null) dbConnection.close();
    }
    }
    }
    @Stateless
    public class TradingServiceImpl implements TradingService {
    @Resource SessionContext ctx;
    @Resource(mappedName=”java:jdbc/tradingDS”) DataSource ds;
    public long insertTrade(TradeData trade) throws Exception {
    Connection dbConnection = ds.getConnection();
    try {
    Statement sql = dbConnection.createStatement();
    String stmt =
    “INSERT INTO TRADE (ACCT_ID, SIDE, SYMBOL, SHARES, PRICE, STATE)”
  • “VALUES (”
  • 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()) {
    return rs.getBigDecimal(1).longValue();
    } else {
    throw new Exception(“Trade Order Insert Failed”);
    }
    } finally {
    if (dbConnection != null) dbConnection.close();
    }
    }
    }

版权声明:本文由美高梅开户送58元官网发布于美高梅开户送58元官网,转载请注明出处:在整个方法前加上,确实是事务未回滚导致的数