spring——@Transactional為啥不生效

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可以用作在那些地方

從源碼可以看出來

  1. 作用于類:當把@Transactional 注解放在類上時单雾,表示所有該類的public方法都配置相同的事務屬性信息赚哗。
  2. 作用于方法:當類配置了@Transactional,方法也配置了@Transactional硅堆,方法的事務會覆蓋類的事務配置信息屿储。
  3. 作用于接口:不推薦這種使用方法,因為一旦標注在Interface上并且配置了Spring AOP 使用CGLib動態(tài)代理渐逃,將會導致@Transactional注解失效

@Transactional的屬性

  1. 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 效果一樣编兄。
  2. isolation 屬性
    isolation :事務的隔離級別,默認值為 Isolation.DEFAULT声登。
    Isolation.DEFAULT:使用底層數(shù)據(jù)庫默認的隔離級別狠鸳。
    Isolation.READ_UNCOMMITTED
    Isolation.READ_COMMITTED
    Isolation.REPEATABLE_READ
    Isolation.SERIALIZABLE
  3. timeout 屬性
    timeout :事務的超時時間,默認值為 -1悯嗓。如果超過該時間限制但事務還沒有完成件舵,則自動回滾事務。
  4. readOnly 屬性
    readOnly :指定事務是否為只讀事務脯厨,默認值為 false铅祸;為了忽略那些不需要事務的方法,比如讀取數(shù)據(jù)合武,可以設置 read-only 為 true临梗。
  5. rollbackFor 屬性
    rollbackFor :用于指定能夠觸發(fā)事務回滾的異常類型,可以指定多個異常類型稼跳。
  6. 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ā)生回滾愿题。

  1. TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務蛙奖;如果當前沒有事務潘酗,則以非事務的方式繼續(xù)運行。
  2. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行雁仲,如果當前存在事務仔夺,則把當前事務掛起。
  3. TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行攒砖,如果當前存在事務缸兔,則拋出異常日裙。

5 @Transactional 注解屬性 rollbackFor 設置錯誤

rollbackFor 可以指定能夠觸發(fā)事務回滾的異常類型。Spring默認拋出了未檢查unchecked異常(繼承自 RuntimeException 的異常)或者 Error才回滾事務惰蜜;其他異常不會觸發(fā)回滾事務昂拂。如果在事務中拋出其他類型的異常,但卻期望 Spring 能夠回滾事務抛猖,就需要指定 rollbackFor屬性格侯。


image.png

6 數(shù)據(jù)庫引擎不支持事務

這種情況出現(xiàn)的概率并不高,事務能否生效數(shù)據(jù)庫引擎是否支持事務是關鍵财著。常用的MySQL數(shù)據(jù)庫默認使用支持事務的innodb引擎联四。一旦數(shù)據(jù)庫引擎切換成不支持事務的myisam,那事務就從根本上失效了撑教。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末朝墩,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子伟姐,更是在濱河造成了極大的恐慌收苏,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玫镐,死亡現(xiàn)場離奇詭異倒戏,居然都是意外死亡,警方通過查閱死者的電腦和手機恐似,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門杜跷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人矫夷,你說我怎么就攤上這事葛闷。” “怎么了双藕?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵淑趾,是天一觀的道長。 經(jīng)常有香客問我忧陪,道長扣泊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任嘶摊,我火速辦了婚禮延蟹,結果婚禮上,老公的妹妹穿的比我還像新娘叶堆。我一直安慰自己阱飘,他們只是感情好,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沥匈,像睡著了一般蔗喂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上高帖,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天缰儿,我揣著相機與錄音,去河邊找鬼棋恼。 笑死返弹,一個胖子當著我的面吹牛锈玉,可吹牛的內容都是我干的爪飘。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼拉背,長吁一口氣:“原來是場噩夢啊……” “哼师崎!你這毒婦竟也來了?” 一聲冷哼從身側響起椅棺,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤犁罩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后两疚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體床估,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年诱渤,在試婚紗的時候發(fā)現(xiàn)自己被綠了丐巫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡勺美,死狀恐怖递胧,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情赡茸,我是刑警寧澤缎脾,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站占卧,受9級特大地震影響遗菠,放射性物質發(fā)生泄漏。R本人自食惡果不足惜华蜒,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一辙纬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧友多,春花似錦牲平、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜈抓。三九已至,卻和暖如春昂儒,著一層夾襖步出監(jiān)牢的瞬間沟使,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工渊跋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留腊嗡,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓拾酝,卻偏偏與公主長得像燕少,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蒿囤,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345