【Java 開發(fā)常見的坑】——@Transactional 事務(wù)失效

Spring中通過@Transactional注解動(dòng)態(tài)代理對(duì)目標(biāo)方法的增強(qiáng),可以很方便的回滾事務(wù)垒迂。但是械姻,如果不熟悉使用@Transactional注解的話,卻會(huì)有很多隱藏的坑不容易被發(fā)現(xiàn)机断,往往是在線上環(huán)境才出現(xiàn)問題楷拳,通過一番排查才找到問題所在绣夺,以下是本人實(shí)際工作中或是瀏覽其他相關(guān)博客模擬實(shí)現(xiàn)的場(chǎng)景,以此加深記憶和記錄欢揖。

1.@Transactional注解標(biāo)記的方法是private

2.@Transactional注解標(biāo)記的方法不是Spring注入的bean調(diào)用

3.@Transactional注解沒有顯示聲明rollbackFor屬性

4.@Transactional注解標(biāo)記的方法內(nèi)陶耍,使用try...catch捕獲異常

5.@Transactional注解使用默認(rèn)的傳播機(jī)制

打開@Transactional注解的內(nèi)容

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}
@Transactional屬性詳解

廢話不多說,直接上案例她混!

以下的案例都是模擬新增用戶的流程该酗,為了簡便畴栖,使用Spring Data JPA操作數(shù)據(jù)庫。

User實(shí)體類

 @Entity
 public class User {
     @Id
     @GeneratedValue(strategy = GenerationType.AUTO)
     private Integer id;
     private String name;
    
    // 省略getter、setter
 }

UserDao類

@Repository
public interface UserDao extends JpaRepository<User,Integer> {
}

Controller類

@RestController
public class TransactionController {
    @Autowired
    private UserService userService;

    @GetMapping("test")
    public void test(){
        userService.createUser();
    }
}

1.@Transactional注解標(biāo)記的方法是private

接下來看下Service實(shí)現(xiàn)類

@Service
public class UserService{

    @Autowired
    private UserDao userDao;

    /**
     * 創(chuàng)建用戶
     */
    public void createUser() {
        insertUser();
    }

    @Transactional
    private void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("錯(cuò)誤");
    }
}

訪問:http://localhost:8080/test后趟庄,可以發(fā)現(xiàn)控制臺(tái)報(bào)錯(cuò)证鸥,證明有拋出異常镜盯,那么事務(wù)是否有回滾呢吗浩?查看一下數(shù)據(jù)庫的User表,卻發(fā)現(xiàn)有新增用戶信息来累,就證明事務(wù)并沒有回滾算吩,事務(wù)回滾失效了!

這是為什么呢佃扼?這就需要知道@Transactional的原理偎巢,實(shí)際上就是Spring中的AOP,使用@Transactional注解兼耀,Spring就會(huì)通過動(dòng)態(tài)代理的方式增強(qiáng)目標(biāo)方法压昼。所以private的方法是無法被代理,所以動(dòng)態(tài)代理失效瘤运,無法回滾事務(wù)窍霞!

既然知道原因,那是不是將private方法改為public就行啦拯坟?

@Service
public class UserService{

    @Autowired
    private UserDao userDao;

    /**
     * 創(chuàng)建用戶
     */
    public void createUser() {
        insertUser();
    }

    @Transactional
    public void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("錯(cuò)誤");
    }
}

再次訪問http://localhost:8080/test但金,雖然控制臺(tái)有輸出報(bào)錯(cuò)信息,但還是沒有回滾數(shù)據(jù)庫的操作郁季,這就納悶了冷溃,不是使用@Transactional注解就可以了嗎?

這就引申到下一個(gè)"坑"了

2.@Transactional注解標(biāo)記的方法不是Spring注入的bean調(diào)用

有點(diǎn)拗口梦裂,其實(shí)簡單理解為@Transactional注解標(biāo)記的方法應(yīng)該是Bean的調(diào)用似枕,而不是方法內(nèi)調(diào)用。例子中@Transactional注解標(biāo)記的方法是由Bean內(nèi)部方法的調(diào)用年柠,所以將@Transactional注解放到例子中的createUser方法就可以了凿歼。

@Service
public class UserService{

    @Autowired
    private UserDao userDao;

    /**
     * 創(chuàng)建用戶
     */
    @Transactional
    public void createUser() {
        insertUser();
    }

    public void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new RuntimeException("錯(cuò)誤");
    }
}

訪問http://localhost:8080/test,這次數(shù)據(jù)表就沒有新增用戶信息了,就證明事務(wù)回滾答憔。

小結(jié):使用@Transactional注解的方法味赃,訪問級(jí)別應(yīng)該是public,而且應(yīng)該是被Bean調(diào)用的方法

3.@Transactional注解沒有顯示聲明rollbackFor屬性

那我再對(duì)Service改一下虐拓,拋出的異常由原來的RuntimeException改為Exception

@Service
public class UserService{

    @Autowired
    private UserDao userDao;

    /**
     * 創(chuàng)建用戶
     */
    @Transactional
    public void createUser() throws Exception {
        insertUser();
    }

    public void insertUser() throws Exception {
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
        throw new Exception("錯(cuò)誤");
    }
}

訪問http://localhost:8080/test心俗,再次發(fā)現(xiàn)由新增用戶信息。My God侯嘀!這又是什么坑呀?

其實(shí)谱轨,這是由于不熟悉@Transactional注解的原因戒幔。

這是因?yàn)镾pring框架的事務(wù)管理默認(rèn)地只在發(fā)生不受控異常(RuntimeException和Error)時(shí)才進(jìn)行事務(wù)回滾。也就是說土童,當(dāng)事務(wù)方法拋出受控異常(Exception中除了RuntimeException及其子類以外的)時(shí)不會(huì)進(jìn)行事務(wù)回滾诗茎。

rollbackFor屬性的默認(rèn)值是 RuntimeException ,但是如果拋出的異常是 Exception 類型献汗,@Transactional注解無法捕獲異常敢订,所以也就無法回滾事務(wù)。阿里巴巴規(guī)范建議使用@Transactional注解的時(shí)候顯式地聲明rollbackFor屬性的值

// @Transactional注解 rollbackFor 屬性默認(rèn)值
@Transactional(rollbackFor = RuntimeException.class)

錯(cuò)誤使用:
@Transactional
public void test(){}

正確使用:
@Transactional(rollbackFor = Exception.class)
public void test(){}

ps.強(qiáng)烈建議大家在Idea上安裝阿里巴巴規(guī)范插件罢吃,插件掃描代碼楚午,發(fā)現(xiàn)有不規(guī)范的地方就回有提示,使咱們的代碼更加規(guī)范尿招、更加優(yōu)雅矾柜!

blog-插件提示.jpg

將原本使用 @Transactional 改為 @Transactional(rollbackFor = Exception.class)后,重新啟動(dòng)訪問http://localhost:8080/test后可以發(fā)現(xiàn)就谜,用戶信息沒有新增怪蔑,就證明事務(wù)回滾了!

小結(jié):使用 @Transactional 注解的時(shí)候丧荐,為了避免隱藏的bug缆瓣,一定要顯式聲明rollbackFor屬性的值!

4.@Transactional注解標(biāo)記的方法內(nèi)虹统,使用try...catch捕獲異常

接下來弓坞,模擬另外一個(gè)坑,這也是一個(gè)十分常見的事務(wù)失效問題

改動(dòng)使用 @Transactional 注解的方法车荔,將原本throw異常改為try...catch捕獲異常

    /**
     * 創(chuàng)建用戶
     */
    @Transactional(rollbackFor = Exception.class)
    public void createUser(){
        try {
            insertUser();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

訪問http://localhost:8080/test后可以發(fā)現(xiàn)昼丑,用戶信息新增了,就證明事務(wù)并沒有回滾夸赫!

這是因?yàn)楫惓P畔⒃诒籃Transactional捕獲之前被try...catch...捕獲了菩帝,相對(duì)于try...catch..."吃"掉了異常,@Transactional就無法捕獲異常,所以就無法回滾事務(wù)呼奢!

那我想通過使用try...catch...捕獲異常并做出一些補(bǔ)償機(jī)制宜雀,怎么辦?其實(shí)也是可以的握础,加上一行:TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

    /**
     * 創(chuàng)建用戶
     */
    @Transactional(rollbackFor = Exception.class)
    public void createUser(){
        try {
            insertUser();
        } catch (Exception e) {
            e.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            // 可以自定義出異常后的操作
        }
    }

小結(jié):使用@Transactional注解的時(shí)候辐董,要注意異常信息會(huì)不會(huì)被try...catch...捕獲。

5.@Transactional注解使用默認(rèn)的傳播機(jī)制

@Transactional注解中禀综,有個(gè)屬性propagation简烘,默認(rèn)的傳播級(jí)別為Propagation.REQUIRED

propagation屬性的值有以下幾種選擇

  • Propagation.REQUIRED(默認(rèn)):如果當(dāng)前存在事務(wù),則加入該事務(wù)定枷,如果當(dāng)前不存在事務(wù)孤澎,則創(chuàng)建一個(gè)新的事務(wù)
  • Propagation.SUPPORTS:如果當(dāng)前存在事務(wù),則加入事務(wù)欠窒,沒有則以非事務(wù)方式運(yùn)行
  • Propagation.MANDATORY:當(dāng)前存在事務(wù)覆旭,則加入事務(wù),不存在事務(wù)則拋出異常
  • Propagation.REQUIRES_NEW:創(chuàng)建一個(gè)新的事務(wù)岖妄,如果當(dāng)前存在事務(wù)型将,則把當(dāng)前事務(wù)掛起
  • Propagation.NOT_SUPPORTED:以非事務(wù)方式運(yùn)行,如果當(dāng)前存在事務(wù)荐虐,則把當(dāng)前事務(wù)掛起
  • Propagation.NEVER:以非事務(wù)方式運(yùn)行七兜,如果當(dāng)前存在事務(wù),則拋出異常
  • Propagation.NESTED:如果當(dāng)前存在事務(wù)福扬,則創(chuàng)建一個(gè)事務(wù)作為當(dāng)前事務(wù)的嵌套事務(wù)來運(yùn)行惊搏;如果當(dāng)前沒有事務(wù),則該取值等價(jià)于TransactionDefinition.PROPAGATION_REQUIRED

但要根據(jù)實(shí)際的業(yè)務(wù)場(chǎng)景選擇事務(wù)傳播級(jí)別忧换,不一定默認(rèn)的傳播級(jí)別適用恬惯!

假設(shè)現(xiàn)在的業(yè)務(wù)場(chǎng)景是,先創(chuàng)建用戶信息亚茬,然后根據(jù)用戶信息創(chuàng)建學(xué)生信息(Student表)酪耳,但如果由于某些原因,創(chuàng)建學(xué)生信息失敗刹缝,但不能影響用戶信息的創(chuàng)建碗暗。所以創(chuàng)建用戶信息和學(xué)生信息應(yīng)該在不同的事務(wù)內(nèi),這樣才不會(huì)相互影響梢夯,這樣的話言疗,使用@Transactional默認(rèn)的傳播級(jí)別就實(shí)現(xiàn)不了,但我們可以改變propagation屬性值颂砸,改為Propagation.REQUIRES_NEW

Student實(shí)體類

@Entity
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    private String name;
    private String classroom;
    
    // 省略getter噪奄、setter
}

StudentDao類

@Repository
public interface StudentDao extends JpaRepository<Student,Integer> {
}

StudentService實(shí)現(xiàn)類

@Service
public class StudentService {
    @Autowired
    private StudentDao studentDao;

    /**
     * 創(chuàng)建學(xué)生基本信息
     */
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW)
    public void createStudentInfo() throws Exception {
        Student student = new Student();
        student.setName("MuggleLee");
        student.setClassroom("高一一班");
        studentDao.save(student);
        throw new Exception("錯(cuò)誤");
    }
}

UserService實(shí)現(xiàn)類

@Service
public class UserService {

    @Autowired
    private UserDao userDao;

    @Autowired
    private StudentService studentService;

    /**
     * 創(chuàng)建用戶
     */
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public void createUser() {
        insertUser();
        try {
            studentService.createStudentInfo();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void insertUser(){
        User user = new User();
        user.setName("MuggleLee");
        userDao.save(user);
    }
}

重啟后訪問http://localhost:8080/test死姚,可以發(fā)現(xiàn)用戶信息可以正常新增,但學(xué)生信息卻沒有新增勤篮,就證明學(xué)生新增信息被事務(wù)回滾都毒,但不影響用戶信息新增。

以上都是常見的事務(wù)失效的場(chǎng)景碰缔,希望能夠諸位在開發(fā)的時(shí)候账劲,多加注意!

如果覺得文章不錯(cuò)的話金抡,麻煩點(diǎn)個(gè)贊哈瀑焦,你的鼓勵(lì)就是我的動(dòng)力!對(duì)于文章有哪里不清楚或者有誤的地方梗肝,歡迎在評(píng)論區(qū)留言~

參考資料:
極客時(shí)間——專欄:Java業(yè)務(wù)開發(fā)常見錯(cuò)誤100例

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末榛瓮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子统捶,更是在濱河造成了極大的恐慌榆芦,老刑警劉巖柄粹,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喘鸟,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡驻右,警方通過查閱死者的電腦和手機(jī)什黑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來堪夭,“玉大人愕把,你說我怎么就攤上這事∩” “怎么了恨豁?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爬迟。 經(jīng)常有香客問我橘蜜,道長,這世上最難降的妖魔是什么付呕? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任计福,我火速辦了婚禮,結(jié)果婚禮上徽职,老公的妹妹穿的比我還像新娘象颖。我一直安慰自己,他們只是感情好姆钉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布说订。 她就那樣靜靜地躺著抄瓦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪克蚂。 梳的紋絲不亂的頭發(fā)上闺鲸,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音埃叭,去河邊找鬼摸恍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛赤屋,可吹牛的內(nèi)容都是我干的立镶。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼类早,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼媚媒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起涩僻,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤缭召,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后逆日,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嵌巷,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年室抽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搪哪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡坪圾,死狀恐怖晓折,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兽泄,我是刑警寧澤漓概,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站病梢,受9級(jí)特大地震影響胃珍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜飘千,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一堂鲜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧护奈,春花似錦缔莲、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蛀骇。三九已至,卻和暖如春读拆,著一層夾襖步出監(jiān)牢的瞬間擅憔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國打工檐晕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留暑诸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓辟灰,卻偏偏與公主長得像个榕,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子芥喇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353