Spring 針對 Java Transaction API (JTA)弄唧、JDBC、Hibernate 和 Java Persistence API (JPA) 等事務 API纲酗,實現(xiàn)了一致的編程模型,而 Spring 的聲明式事務功能更是提供了極其方便的事務配置方式,配合 Spring Boot 的自動配置,大多數(shù) Spring Boot 項目只需要在方法上標記 @Transactional 注解赂摆,即可一鍵開啟方法的事務性配置。
在實際工作中憎妙,只是簡單的在方法上加上@Transactional 库正,就理所當然的認為能控制事務曲楚,這里面是有些坑厘唾,如果沒用對,事務是不生效的龙誊,導致數(shù)據(jù)不一致抚垃,需要大量的排查,以及清洗數(shù)據(jù),下面聊一聊鹤树,什么情況下會導致事務不生效铣焊。
事務
- 編程式事務:是指在代碼中手動的管理事務的提交、回滾等操作罕伯,代碼侵入性比較強曲伊,如下示例:
try {
//TODO something
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new InvoiceApplyException("異常失敗");
}
- 聲明式事務:基于AOP面向切面的,它將具體業(yè)務與事務處理部分解耦追他,代碼侵入性很低坟募,所以在實際開發(fā)中聲明式事務用的比較多。聲明式事務也有兩種實現(xiàn)方式邑狸,一是基于TX和AOP的xml配置文件方式懈糯,二種就是基于@Transactional注解了。
@Transactional
public void createUserPublic(User user){
userRepository.save(user);
}
@Transactional介紹
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
/**
* Alias for {@link #transactionManager}.
* @see #transactionManager
*/
@AliasFor("transactionManager")
String value() default "";
/**
* A <em>qualifier</em> value for the specified transaction.
* <p>May be used to determine the target transaction manager,
* matching the qualifier value (or the bean name) of a specific
* {@link org.springframework.transaction.PlatformTransactionManager}
* bean definition.
* @since 4.2
* @see #value
*/
@AliasFor("value")
String transactionManager() default "";
/**
* The transaction propagation type.
* <p>Defaults to {@link Propagation#REQUIRED}.
* @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior()
*/
Propagation propagation() default Propagation.REQUIRED;
/**
* The transaction isolation level.
* <p>Defaults to {@link Isolation#DEFAULT}.
* <p>Exclusively designed for use with {@link Propagation#REQUIRED} or
* {@link Propagation#REQUIRES_NEW} since it only applies to newly started
* transactions. Consider switching the "validateExistingTransactions" flag to
* "true" on your transaction manager if you'd like isolation level declarations
* to get rejected when participating in an existing transaction with a different
* isolation level.
* @see org.springframework.transaction.interceptor.TransactionAttribute#getIsolationLevel()
* @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setValidateExistingTransaction
*/
Isolation isolation() default Isolation.DEFAULT;
/**
* The timeout for this transaction (in seconds).
* <p>Defaults to the default timeout of the underlying transaction system.
* <p>Exclusively designed for use with {@link Propagation#REQUIRED} or
* {@link Propagation#REQUIRES_NEW} since it only applies to newly started
* transactions.
* @see org.springframework.transaction.interceptor.TransactionAttribute#getTimeout()
*/
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
/**
* A boolean flag that can be set to {@code true} if the transaction is
* effectively read-only, allowing for corresponding optimizations at runtime.
* <p>Defaults to {@code false}.
* <p>This just serves as a hint for the actual transaction subsystem;
* it will <i>not necessarily</i> cause failure of write access attempts.
* A transaction manager which cannot interpret the read-only hint will
* <i>not</i> throw an exception when asked for a read-only transaction
* but rather silently ignore the hint.
* @see org.springframework.transaction.interceptor.TransactionAttribute#isReadOnly()
* @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly()
*/
boolean readOnly() default false;
/**
* Defines zero (0) or more exception {@link Class classes}, which must be
* subclasses of {@link Throwable}, indicating which exception types must cause
* a transaction rollback.
* <p>By default, a transaction will be rolling back on {@link RuntimeException}
* and {@link Error} but not on checked exceptions (business exceptions). See
* {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)}
* for a detailed explanation.
* <p>This is the preferred way to construct a rollback rule (in contrast to
* {@link #rollbackForClassName}), matching the exception class and its subclasses.
* <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class clazz)}.
* @see #rollbackForClassName
* @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
*/
Class<? extends Throwable>[] rollbackFor() default {};
/**
* Defines zero (0) or more exception names (for exceptions which must be a
* subclass of {@link Throwable}), indicating which exception types must cause
* a transaction rollback.
* <p>This can be a substring of a fully qualified class name, with no wildcard
* support at present. For example, a value of {@code "ServletException"} would
* match {@code javax.servlet.ServletException} and its subclasses.
* <p><b>NB:</b> Consider carefully how specific the pattern is and whether
* to include package information (which isn't mandatory). For example,
* {@code "Exception"} will match nearly anything and will probably hide other
* rules. {@code "java.lang.Exception"} would be correct if {@code "Exception"}
* were meant to define a rule for all checked exceptions. With more unusual
* {@link Exception} names such as {@code "BaseBusinessException"} there is no
* need to use a FQN.
* <p>Similar to {@link org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(String exceptionName)}.
* @see #rollbackFor
* @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
*/
String[] rollbackForClassName() default {};
/**
* Defines zero (0) or more exception {@link Class Classes}, which must be
* subclasses of {@link Throwable}, indicating which exception types must
* <b>not</b> cause a transaction rollback.
* <p>This is the preferred way to construct a rollback rule (in contrast
* to {@link #noRollbackForClassName}), matching the exception class and
* its subclasses.
* <p>Similar to {@link org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(Class clazz)}.
* @see #noRollbackForClassName
* @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
*/
Class<? extends Throwable>[] noRollbackFor() default {};
/**
* Defines zero (0) or more exception names (for exceptions which must be a
* subclass of {@link Throwable}) indicating which exception types must <b>not</b>
* cause a transaction rollback.
* <p>See the description of {@link #rollbackForClassName} for further
* information on how the specified names are treated.
* <p>Similar to {@link org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(String exceptionName)}.
* @see #noRollbackFor
* @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
*/
String[] noRollbackForClassName() default {};
@Transactional可以用作在那些地方
從源碼可以看出來
- 作用于類:當把@Transactional 注解放在類上時单雾,表示所有該類的public方法都配置相同的事務屬性信息赚哗。
- 作用于方法:當類配置了@Transactional,方法也配置了@Transactional硅堆,方法的事務會覆蓋類的事務配置信息屿储。
- 作用于接口:不推薦這種使用方法,因為一旦標注在Interface上并且配置了Spring AOP 使用CGLib動態(tài)代理渐逃,將會導致@Transactional注解失效
@Transactional的屬性
- propagation屬性
propagation 代表事務的傳播行為扩所,默認值為 Propagation.REQUIRED,其他的屬性信息如下:
Propagation.REQUIRED:如果當前存在事務朴乖,則加入該事務祖屏,如果當前不存在事務,則創(chuàng)建一個新的事務买羞。( 也就是說如果A方法和B方法都添加了注解袁勺,在默認傳播模式下,A方法內部調用B方法畜普,會把兩個方法的事務合并為一個事務 )
Propagation.SUPPORTS:如果當前存在事務期丰,則加入該事務;如果當前不存在事務吃挑,則以非事務的方式繼續(xù)運行钝荡。
Propagation.MANDATORY:如果當前存在事務,則加入該事務舶衬;如果當前不存在事務埠通,則拋出異常。
Propagation.REQUIRES_NEW:重新創(chuàng)建一個新的事務逛犹,如果當前存在事務端辱,暫停當前的事務梁剔。( 當類A中的 a 方法用默認Propagation.REQUIRED模式,類B中的 b方法加上采用 Propagation.REQUIRES_NEW模式舞蔽,然后在 a 方法中調用 b方法操作數(shù)據(jù)庫荣病,然而 a方法拋出異常后,b方法并沒有進行回滾渗柿,因為Propagation.REQUIRES_NEW會暫停 a方法的事務 )
Propagation.NOT_SUPPORTED:以非事務的方式運行个盆,如果當前存在事務,暫停當前的事務朵栖。
Propagation.NEVER:以非事務的方式運行砾省,如果當前存在事務,則拋出異常混槐。
Propagation.NESTED :和 Propagation.REQUIRED 效果一樣编兄。 - isolation 屬性
isolation :事務的隔離級別,默認值為 Isolation.DEFAULT声登。
Isolation.DEFAULT:使用底層數(shù)據(jù)庫默認的隔離級別狠鸳。
Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE - timeout 屬性
timeout :事務的超時時間,默認值為 -1悯嗓。如果超過該時間限制但事務還沒有完成件舵,則自動回滾事務。 - readOnly 屬性
readOnly :指定事務是否為只讀事務脯厨,默認值為 false铅祸;為了忽略那些不需要事務的方法,比如讀取數(shù)據(jù)合武,可以設置 read-only 為 true临梗。 - rollbackFor 屬性
rollbackFor :用于指定能夠觸發(fā)事務回滾的異常類型,可以指定多個異常類型稼跳。 - noRollbackFor屬性**
noRollbackFor:拋出指定的異常類型盟庞,不回滾事務,也可以指定多個異常類型汤善。
@Transactional失效場景
準備工作
做一個demo什猖,方便演示
- 數(shù)據(jù)庫對應的實體類
@Data
@Table(name = "test_user")
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/**
* 姓名
*/
private String name;
/**
* 年齡
*/
private Integer age;
/**
* 聯(lián)系電話
*/
private String phone;
}
- 數(shù)據(jù)庫訪問類
為了方便演示,使用的sprigjpa红淡,已可以改造現(xiàn)在比較流行的mybatis
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
}
- 創(chuàng)建一個service
Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
public int createUserWrong1(User user){
try {
this.createUserPrivate(user);
} catch (Exception e) {
e.printStackTrace();
}
return userRepository.findAll().size();
}
/**
* 方法私有
* @param user
*/
@Transactional
private void createUserPrivate(User user){
userRepository.save(user);
//if (user.getAge()==100){
// throw new RuntimeException("invalid age");
//}
}
/**
* 方法私有
* @param user
*/
@Transactional
public void createUserPublic(User user){
userRepository.save(user);
if (user.getAge()==100){
throw new RuntimeException("invalid age");
}
}
}
- 再創(chuàng)建一個controller
RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/user/save")
public int saveUser(@RequestBody User user){
return userService.createUserWrong1(user);
}
}
1 @Transactional 應用在非 public 修飾的方法上
/**
* 方法私有
* @param user
*/
@Transactional
private void createUserPrivate(User user){
userRepository.save(user);
if (user.getAge()==100){
throw new RuntimeException("invalid age");
}
}
因為在Spring AOP 代理時不狮, TransactionInterceptor (事務攔截器)在目標方法執(zhí)行前后進行攔截,DynamicAdvisedInterceptor(CglibAopProxy 的內部類)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法會間接調用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法在旱,獲取Transactional 注解的事務配置信息摇零。
protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
此方法會檢查目標方法的修飾符是否為 public,不是 public則不會獲取@Transactional 的屬性配置信息颈渊。
注意:protected遂黍、private 修飾的方法上使用 @Transactional 注解终佛,雖然事務無效俊嗽,但不會有任何報錯
2 同一個類中方法調用雾家,導致@Transactional失效
原因:必須通過代理過的類從外部調用目標方法才能生效
public int createUserWrong1(User user){
try {
this.createUserPublic(user);
} catch (Exception e) {
e.printStackTrace();
}
return userRepository.findAll().size();
}
//方法內部調用
Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
//方法外部調用 aop代理的是UserService 所以生效
Creating new transaction with name [com.kb.pit.transactional.service.UserService.createUserPublic]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
由于使用Spring AOP代理造成的,因為只有當事務方法被當前類以外的代碼調用時绍豁,才會由Spring生成的代理對象來管理芯咧。
如果我們把@Transactional放到外部調用的方法上,這樣代理的就是userService
3 異常被你的 catch“吃了”導致@Transactional失效
@Transactional
public int createUserWrong2(User user){
try {
userRepository.save(user);
throw new RuntimeException("invalid age");
} catch (Exception e) {
e.printStackTrace();
}
return userRepository.findAll().size();
}
}
事務生效竹揍,但是數(shù)據(jù)沒有回滾敬飒,因為捕獲了異常,需要主動回滾
- 只有異常傳播出了標記了 @Transactional 注解的方法芬位,事務才能回滾,在 Spring 的 TransactionAspectSupport 里有個 invokeWithinTransaction 方法无拗,里面就是處理事務的邏輯∶恋铮可以看到英染,只有捕獲到異常才能進行后續(xù)事務處理:
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
- 默認情況下,出現(xiàn) RuntimeException(非受檢異常)或 Error 的時候被饿,Spring 才會回滾事務四康。打開 Spring 的 DefaultTransactionAttribute 類能看到如下代碼塊,可以發(fā)現(xiàn)相關證據(jù)狭握,通過注釋也能看到 Spring 這么做的原因闪金,大概的意思是受檢異常一般是業(yè)務異常,或者說是類似另一種方法的返回值论颅,出現(xiàn)這樣的異嘲タ眩可能業(yè)務還能完成,所以不會主動回滾恃疯;而 Error 或 RuntimeException 代表了非預期的結果撼泛,應該回滾
/**
* The default behavior is as with EJB: rollback on unchecked exception
* ({@link RuntimeException}), assuming an unexpected outcome outside of any
* business rules. Additionally, we also attempt to rollback on {@link Error} which
* is clearly an unexpected outcome as well. By contrast, a checked exception is
* considered a business exception and therefore a regular expected outcome of the
* transactional business method, i.e. a kind of alternative return value which
* still allows for regular completion of resource operations.
* <p>This is largely consistent with TransactionTemplate's default behavior,
* except that TransactionTemplate also rolls back on undeclared checked exceptions
* (a corner case). For declarative transactions, we expect checked exceptions to be
* intentionally declared as business exceptions, leading to a commit by default.
* @see org.springframework.transaction.support.TransactionTemplate#execute
*/
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
4 @Transactional 注解屬性 propagation 設置錯誤
這種失效是由于配置錯誤,若是錯誤的配置以下三種 propagation澡谭,事務將不會發(fā)生回滾愿题。
- TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務蛙奖;如果當前沒有事務潘酗,則以非事務的方式繼續(xù)運行。
- TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行雁仲,如果當前存在事務仔夺,則把當前事務掛起。
- TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行攒砖,如果當前存在事務缸兔,則拋出異常日裙。
5 @Transactional 注解屬性 rollbackFor 設置錯誤
rollbackFor 可以指定能夠觸發(fā)事務回滾的異常類型。Spring默認拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error才回滾事務惰蜜;其他異常不會觸發(fā)回滾事務昂拂。如果在事務中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務抛猖,就需要指定 rollbackFor屬性格侯。
6 數(shù)據(jù)庫引擎不支持事務
這種情況出現(xiàn)的概率并不高,事務能否生效數(shù)據(jù)庫引擎是否支持事務是關鍵财著。常用的MySQL數(shù)據(jù)庫默認使用支持事務的innodb引擎联四。一旦數(shù)據(jù)庫引擎切換成不支持事務的myisam,那事務就從根本上失效了撑教。