Spring事务的传播行为 Transactional.docx
《Spring事务的传播行为 Transactional.docx》由会员分享,可在线阅读,更多相关《Spring事务的传播行为 Transactional.docx(20页珍藏版)》请在冰豆网上搜索。
![Spring事务的传播行为 Transactional.docx](https://file1.bdocx.com/fileroot1/2022-11/20/85f0a8ed-cda0-4b4e-9fa6-5273401aa330/85f0a8ed-cda0-4b4e-9fa6-5273401aa3301.gif)
Spring事务的传播行为Transactional
Spring事务的传播行为@Transactional
Spring事务的传播行为
在service类前加上@Transactional,声明这个service所有方法需要事务管理。
每一个业务方法开始时都会打开一个事务。
Spring默认情况下会对运行期例外(RunTimeException)进行事务回滚。
这个例外是unchecked
如果遇到checked意外就不回滚。
如何改变默认规则:
1让checked例外也回滚:
在整个方法前加上@Transactional(rollbackFor=Exception.class)
2让unchecked例外不回滚:
@Transactional(notRollbackFor=RunTimeException.class)
3不需要事务管理的(只查询的)方法:
@Transactional(propagation=Propagation.NOT_SUPPORTED)
注意:
如果异常被try{}catch{}了,事务就不回滚了,如果想让事务回滚必须再往外抛try{}catch{throwException}。
spring——@Transactional事务不管理jdbc,所以要自己把jdbc事务回滚。
下面给出了回滚JDBC事务的代码示例:
Java代码publicvoidprocessT(String
orders){
Context
initCtx=newInitialContext();
javax.sql.DataSource
ds=javax.sql.DataSource)initCtx.lookup
(“java:
comp/env/jdbc/OrdersDB”);
java.sql.Connection
conn=ds.getConnection();
try{
conn.setAutoCommit(false);//更改JDBC事务的默认提交方式
orderNo
=createOrder(orders);
updateOrderStatus(orderNo,
“orderscreated”);
mit();//提交JDBC事务
}catch(
Exceptione){
try{
conn.rollback();//回滚sJDBC事务
thrownewEJBException(“事务回滚:
“+e.getMessage());
}catch(
SQLExceptionsqle){
thrownewEJBException(“出现SQL操作错误:
“+sqle.getMessage());
}
}
}
下面给出了JTA事务代码示例:
Java代码publicvoidprocessOrder(String
orderMessage){
UserTransaction
transaction=mySessionContext.getUserTransaction();//获得JTA事务
try{
transaction.begin();//开始JTA事务
orderNo
=sendOrder(orderMessage);
updateOrderStatus(orderNo,
“ordersent”);
mit();//提交JTA事务
}catch(Exception
e){
try{
transaction.rollback();//回滚JTA事务
}catch(SystemException
se){
se.printStackTrace();
}
thrownewEJBException(“事务回滚:
“+e.getMessage());
}
}
在整个方法运行前就不会开启事务
还可以加上:
@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),这样就做成一个只读事务,可以提高效率。
各种属性的意义:
REQUIRED:
业务方法需要在一个容器里运行。
如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务。
NOT_SUPPORTED:
声明方法不需要事务。
如果方法没有关联到一个事务,容器不会为他开启事务,如果方法在一个事务中被调用,该事务会被挂起,调用结束后,原先的事务会恢复执行。
REQUIRESNEW:
不管是否存在事务,该方法总汇为自己发起一个新的事务。
如果方法已经运行在一个事务中,则原有事务挂起,新的事务被创建。
MANDATORY:
该方法只能在一个已经存在的事务中执行,业务方法不能发起自己的事务。
如果在没有事务的环境下被调用,容器抛出例外。
SUPPORTS:
该方法在某个事务范围内被调用,则方法成为该事务的一部分。
如果方法在该事务范围外被调用,该方法就在没有事务的环境下执行。
NEVER:
该方法绝对不能在事务范围内执行。
如果在就抛例外。
只有该方法没有关联到任何事务,才正常执行。
NESTED:
如果一个活动的事务存在,则运行在一个嵌套的事务中。
如果没有活动事务,则按REQUIRED属性执行。
它使用了一个单独的事务,这个事务拥有多个可以回滚的保存点。
内部事务的回滚不会对外部事务造成影响。
它只对DataSourceTransactionManager事务管理器起效。
事务陷阱-1
清单1.使用JDBC的简单数据库插入
viewplaincopytoclipboardprint?
@Stateless
publicclassTradingServiceImplimplementsTradingService{
@ResourceSessionContextctx;
@Resource(mappedName="java:
jdbc/tradingDS")DataSourceds;publiclonginsertTrade(TradeDatatrade)throwsException{
ConnectiondbConnection=ds.getConnection();
try{
Statementsql=dbConnection.createStatement();
Stringstmt=
"INSERTINTOTRADE(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);
ResultSetrs=sql.getGeneratedKeys();
if(rs.next()){
returnrs.getBigDecimal
(1).longValue();
}else{
thrownewException("TradeOrderInsertFailed");
}
}finally{
if(dbConnection!
=null)dbConnection.close();
}
}
}
@Stateless
publicclassTradingServiceImplimplementsTradingService{
@ResourceSessionContextctx;
@Resource(mappedName="java:
jdbc/tradingDS")DataSourceds;
publiclonginsertTrade(TradeDatatrade)throwsException{
ConnectiondbConnection=ds.getConnection();
try{
Statementsql=dbConnection.createStatement();
Stringstmt=
"INSERTINTOTRADE(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);
ResultSetrs=sql.getGeneratedKeys();
if(rs.next()){
returnrs.getBigDecimal
(1).longValue();
}else{
thrownewException("TradeOrderInsertFailed");
}
}finally{
if(dbConnection!
=null)dbConnection.close();
}
}
}
清单1中的JDBC代码没有包含任何事务逻辑,它只是在数据库中保存TRADE表中的交易订单。
在本例中,数据库处理事务逻辑。
在LUW中,这是一个不错的单个数据库维护操作。
但是如果需要在向数据库插入交易订单的同时更新帐户余款呢?
如清单2所示:
清单2.在同一方法中执行多次表更新
viewplaincopytoclipboardprint?
publicTradeDataplaceTrade(TradeDatatrade)throwsException{
try{
insertTrade(trade);
updateAcct(trade);
returntrade;
}catch(Exceptionup){
//logtheerror
throwup;
}
}
publicTradeDataplaceTrade(TradeDatatrade)throwsException{
try{
insertTrade(trade);
updateAcct(trade);
returntrade;
}catch(Exceptionup){
//logtheerror
throwup;
}
}
在本例中,insertTrade()和updateAcct()方法使用不带事务的标准JDBC代码。
insertTrade()方法结束后,数据库保存(并提交了)交易订单。
如果updateAcct()方法由于任意原因失败,交易订单仍然会在placeTrade()方法结束时保存在TRADE表内,这会导致数据库出现不一致的数据。
如果placeTrade()方法使用了事务,这两个活动都会包含在一个LUW中,如果帐户更新失败,交易订单就会回滚。
事务陷阱-2
随着Java持久性框架的不断普及,如Hibernate、TopLink和Java持久性API(JavaPersistenceAPI,JPA),我们很少再会去编写简单的JDBC代码。
更常见的情况是,我们使用更新的对象关系映射(ORM)框架来减轻工作,即用几个简单的方法调用替换所有麻烦的JDBC代码。
例如,要插入清单1中JDBC代码示例的交易订单,使用带有JPA的SpringFramework,就可以将TradeData对象映射到TRADE表,并用清单3
中的JPA代码替换所有JDBC代码:
清单3.使用JPA的简单插入
viewplaincopytoclipboardprint?
publicclassTradingServiceImpl{
@PersistenceContext(unitName="trading")EntityManagerem;publiclonginsertTrade(TradeDatatrade)throwsException{
em.persist(trade);
returntrade.getTradeId();
}
}
publicclassTradingServiceImpl{
@PersistenceContext(unitName="trading")EntityManagerem;
publiclonginsertTrade(TradeDatatrade)throwsException{
em.persist(trade);
returntrade.getTradeId();
}
}
注意,清单3在EntityManager上调用了persist()方法来插入交易订单。
很简单,是吧?
其实不然。
这段代码不会像预期那样向TRADE表插入交易订单,也不会抛出异常。
它只是返回一个值0作为交易订单的键,而不会更改数据库。
这是事务处理的主要陷阱之一:
基于ORM的框架需要一个事务来触发对象缓存与数据库之间的同步。
这通过一个事务提交完成,其中会生成SQL代码,数据库会执行需要的操作(即插入、更新、删除)。
没有事务,就不会触发ORM去生成SQL代码和保存更改,因此只会终止方法
—没有异常,没有更新。
如果使用基于ORM的框架,就必须利用事务。
您不再依赖数据库来管理连接和提交工作。
这些简单的示例应该清楚地说明,为了维护数据完整性和一致性,必须使用事务。
不过对于在Java平台中实现事务的复杂性和陷阱而言,这些示例只是涉及了冰山一角。
SpringFramework@Transactional注释陷阱-3
清单4.使用@Transactional注释
viewplaincopytoclipboardprint?
publicclassTradingServiceImpl{
@PersistenceContext(unitName="trading")EntityManagerem;@Transactional
publiclonginsertTrade(TradeDatatrade)throwsException{
em.persist(trade);
returntrade.getTradeId();
}
}
publicclassTradingServiceImpl{
@PersistenceContext(unitName="trading")EntityManagerem;
@Transactional
publiclonginsertTrade(TradeDatatrade)throwsException{
em.persist(trade);
returntrade.getTradeId();
}
}
现在重新测试代码,您发现上述方法仍然不能工作。
问题在于您必须告诉SpringFramework,您正在对事务管理应用注释。
除非您进行充分的单元测试,否则有时候很难发现这个陷阱。
这通常只会导致开发人员在Spring配置文件中简单地添加事务逻辑,而不会使用注释。
要在Spring中使用@Transactional注释,必须在Spring配置文件中添加以下代码行:
viewplaincopytoclipboardprint?
<tx:
annotation-driventransaction-manager="transactionManager"/>
<tx:
annotation-driventransaction-manager="transactionManager"/>
transaction-manager属性保存一个对在Spring配置文件中定义的事务管理器bean的引用。
这段代码告诉Spring在应用事务拦截器时使用@Transaction注释。
如果没有它,就会忽略@Transactional注释,导致代码不会使用任何事务。
让基本的@Transactional注释在清单4的代码中工作仅仅是开始。
注意,清单4使用@Transactional注释时没有指定任何额外的注释参数。
我发现许多开发人员在使用@Transactional注释时并没有花时间理解它的作用。
例如,像我一样在清单4中单独使用@Transactional注释时,事务传播模式被设置成什么呢?
只读标志被设置成什么呢?
事务隔离级别的设置是怎样的?
更重要的是,事务应何时回滚工作?
理解如何使用这个注释对于确保在应用程序中获得合适的事务支持级别非常重要。
回答我刚才提出的问题:
在单独使用不带任何参数的
@Transactional注释时,传播模式要设置为REQUIRED,只读标志设置为false,事务隔离级别设置为READ_COMMITTED,而且事务不会针对受控异常(checkedexception)回滚。
@Transactional只读标志陷阱
我在工作中经常碰到的一个常见陷阱是Spring@Transactional注释中的只读标志没有得到恰当使用。
这里有一个快速测试方法:
在使用标准JDBC代码获得Java持久性时,如果只读标志设置为true,传播模式设置为SUPPORTS,清单5中的@Transactional注释的作用是什么呢?
清单5.将只读标志与SUPPORTS传播模式结合使用—JDBC
viewplaincopytoclipboardprint?
@Transactional(readOnly=true,propagation=Propagation.SUPPORTS)
publiclonginsertTrade(TradeDatatrade)throwsException{
//JDBCCode...
}
@Transactional(readOnly=true,propagation=Propagation.SUPPORTS)
publiclonginsertTrade(TradeDatatrade)throwsException{
//JDBCCode...
}
当执行清单5中的insertTrade()方法时,猜一猜会得到下面哪一种结果:
抛出一个只读连接异常
正确插入交易订单并提交数据
什么也不做,因为传播级别被设置为SUPPORTS
是哪一个呢?
正确答案是B。
交易订单会被正确地插入到数据库中,即使只读标志被设置为true,且事务传播模式被设置为SUPPORTS。
但这是如何做到的呢?
由于传播模式被设置为SUPPORTS,所以不会启动任何事物,因此该方法有效地利用了一个本地(数据库)事务。
只读标志只在事务启动时应用。
在本例中,因为没有启动任何事务,所以只读标志被忽略。
SpringFramework@Transactional注释陷阱-4
清单6中的@Transactional注释在设置了只读标志且传播模式被设置为REQUIRED时,它的作用是什么呢?
清单6.将只读标志与REQUIRED传播模式结合使用—JDBC
viewplaincopytoclipboardprint?
@Transactional(readOnly=true,propagation=Propagation.REQUIRED)
publiclonginsertTrade(TradeDatatrade)throwsException{
//JDBCcode...
}
@Transactional(readOnly=true,propagation=Propagation.REQUIRED)
publiclonginsertTrade(TradeDatatrade)throwsException{
//JDBCcode...
}
执行清单6中的insertTrade()方法会得到下面哪一种结果呢:
抛出一个只读连接异常
正确插入交易订单并提交数据
什么也不做,因为只读标志被设置为true
根据前面的解释,这个问题应该很好回答。
正确的答案是A。
会抛出一个异常,表示您正在试图对一个只读连接执行更新。
因为启动了一个事务(REQUIRED),所以连接被设置为只读。
毫无疑问,在试图执行SQL语句时,您会得到一个异常,告诉您该连接是一个只读连接。
关于只读标志很奇怪的一点是:
要使用它,必须启动一个事务。
如果只是读取数据,需要事务吗?
答案是根本不需要。
启动一个事务来执行只读操作会增加处理线程的开销,并会导致数据库发生共享读取锁定(具体取决于使用的数据库类型和设置的隔离级别)。
总的来说,在获取基于JDBC的Java持久性时,使用只读标志有点毫无意义,并会启动不必要的事务而增加额外的开销。
使用基于ORM的框架会怎样呢?
按照上面的测试,如果在结合使用JPA和Hibernate时调用insertTrade()方法,清单7中的@Transactional注释会得到什么结果?
清单7.将只读标志与REQUIRED传播模式结合使用—JPA
viewplaincopytoclipboardprint?
@Transactional(readOnly=true,propagation=Propagation.REQUIRED)
publiclonginsertTrade(TradeDatatrade)throwsException{
em.persist(trade);
returntrade.getTradeId();
}
@Transactional(readOn