Spring Boot 事務(wù)管理

什么是事務(wù)冤议?

事務(wù)實(shí)際上是指一系列操作骄瓣,程序中的事務(wù)大多時(shí)候是指數(shù)據(jù)庫(kù)事務(wù)停巷,那么這一系列操作就是數(shù)據(jù)庫(kù)操作,通常包含多個(gè) SQL 語(yǔ)句榕栏。

為了保證完整性畔勤,事務(wù)需要滿足 ACID 原則:
(1) 原子性(Atomic)
一個(gè)事務(wù)無(wú)論包含多少操作,要么全部執(zhí)行扒磁,要么全部不執(zhí)行庆揪,若執(zhí)行期間某個(gè)操作失敗,則在其之前執(zhí)行的操作都要回滾到事務(wù)執(zhí)行前狀態(tài)妨托。
(2) 一致性(Consistency)
一個(gè)事務(wù)使系統(tǒng)從一個(gè)一致?tīng)顟B(tài)轉(zhuǎn)換到另一個(gè)一致?tīng)顟B(tài)嚷硫。
(3) 隔離性(Isolation)
事務(wù)執(zhí)行過(guò)程中的數(shù)據(jù)變化只存在于該事務(wù)中,對(duì)外界不產(chǎn)生影響始鱼,只有該事務(wù)正常執(zhí)行完畢后仔掸,其它事務(wù)才能獲取到這些變化的數(shù)據(jù)。
(4) 持久性(Durability)
事務(wù)正常執(zhí)行完畢后對(duì)數(shù)據(jù)的改變是永久性的医清。

本文重點(diǎn)在于 Spring Boot 中事務(wù)的使用起暮,不再贅述事務(wù)相關(guān)的技術(shù)細(xì)節(jié)。

本文示例基于之前已介紹過(guò)的代碼,如有不清楚還請(qǐng)參看:
Spring Boot 集成 MyBatis
Spring Boot 集成阿里巴巴 Druid 數(shù)據(jù)庫(kù)連接池

Spring 在之前版本中早已提供事務(wù)管理的能力负懦,Spring Boot 誕生后進(jìn)一步簡(jiǎn)化了事務(wù)配置工作筒捺。如果添加了 spring-boot-starter-jdbc 依賴,框架會(huì)默認(rèn)自動(dòng)注入 DataSourceTransactionManager 纸厉;如果添加了 spring-boot-starter-data-jpa 依賴系吭,框架會(huì)默認(rèn)自動(dòng)注入 JpaTransactionManager。無(wú)需其它額外配置颗品,直接在需要添加事務(wù)處理的方法上使用 @Transactional 注解肯尺。

1 定義 Service 接口

package demo.spring.boot.transaction.service;

import demo.spring.boot.transaction.domain.User;

public interface UserService {
    
    void addUser(User user);
}

2 定義 Service 接口實(shí)現(xiàn)類

package demo.spring.boot.transaction.service.impl;

import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    public void addUser(User user) {
        userDao.insert(user);
    }
}

3 編寫(xiě)單元測(cè)試

package demo.spring.boot.transaction;

import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.time.LocalDate;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTests {
    
    @Autowired
    private UserService userService;
    
    @Test
    public void testAddUser() {
        User user = new User();
        user.setAccount("test_account");
        user.setName("Test Name");
        user.setBirth(LocalDate.now());
        userService.addUser(user);
    }
}

執(zhí)行單元測(cè)試,測(cè)試通過(guò)躯枢,查詢數(shù)據(jù)庫(kù)可以看到剛插入的數(shù)據(jù)

mysql> select * from user \G;
*************************** 1. row ***************************
     id: 24
account: test_account
   name: Test Name
  birth: 2018-08-03
1 row in set (0.00 sec)

4 對(duì) Service 接口實(shí)現(xiàn)類的 addUser 方法稍作調(diào)整则吟,在插入數(shù)據(jù)后執(zhí)行一條肯定會(huì)拋出異常的語(yǔ)句,如下

@Override
public void addUser(User user) {
    userDao.insert(user);
    int x = 1 / 0;
}

刪除數(shù)據(jù)庫(kù) user 表中記錄(因?yàn)?account 字段添加了唯一性索引)锄蹂,再次執(zhí)行單元測(cè)試氓仲,測(cè)試失敗,失敗原因是被測(cè)試方法拋出了異常 java.lang.ArithmeticException: / by zero得糜。
但是因?yàn)閽伋霎惓T?DAO 執(zhí)行插入操作之后敬扛,所以數(shù)據(jù)庫(kù)中還是成功插入了數(shù)據(jù),數(shù)據(jù)庫(kù)查詢結(jié)果如下:

mysql> select * from user \G;
*************************** 1. row ***************************
     id: 25
account: test_account
   name: Test Name
  birth: 2018-08-03
1 row in set (0.00 sec)

5 在 Service 接口實(shí)現(xiàn)類的 addUser 方法中添加 @Transactional 注解實(shí)現(xiàn)事務(wù)管理

package demo.spring.boot.transaction.service.impl;

import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    @Transactional
    public void addUser(User user) {
        userDao.insert(user);
        int x = 1 / 0;
    }
}

執(zhí)行和第 4 步相同步驟的單元測(cè)試朝抖,注意還是需要?jiǎng)h除之前插入 user 表中記錄舔哪,依舊因?yàn)楫惓T驅(qū)е聠卧獪y(cè)試失敗,但是查詢數(shù)據(jù)庫(kù) user 表無(wú)數(shù)據(jù)記錄槽棍,說(shuō)明異常拋出前 DAO 插入的數(shù)據(jù)已被成功回滾(省略數(shù)據(jù)庫(kù)查詢結(jié)果)捉蚤。

注意:@Transactional 默認(rèn)只回滾 Unchecked Exception,即 RuntimeException炼七,所有 Checked Exception 默認(rèn)是不會(huì)滾的

示例:
首先缆巧,修改 addUser 方法,將 int x = 1 / 0; 替換成拋出 Checked Exception

package demo.spring.boot.transaction.service.impl;

import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    @Transactional
    public void addUser(User user)
        throws IOException {
        userDao.insert(user);
        throw new IOException();
    }
}

其次豌拙,修改單元測(cè)試方法

@Test
public void testAddUser()
    throws IOException {
    User user = new User();
    user.setAccount("test_account");
    user.setName("Test Name");
    user.setBirth(LocalDate.now());
    userService.addUser(user);
}

最后陕悬,運(yùn)行單元測(cè)試,測(cè)試失敗按傅,但是查詢數(shù)據(jù)庫(kù)仍看到拋出異常前插入的數(shù)據(jù)

mysql> select * from user \G;
*************************** 1. row ***************************
     id: 28
account: test_account
   name: Test Name
  birth: 2018-08-03
1 row in set (0.00 sec)

@Transactional 指定異匙匠回滾

查看 @Transactional 源碼,rollbackFor 屬性可以指定針對(duì)某些異澄ㄉ埽回滾(其它類似功能屬性請(qǐng)參考 API 文檔

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.METHOD, ElementType.TYPE})
@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 {};
}

修改 addUser 方法拼岳,添加 @Transactional 注解屬性 rollbackFor,指定當(dāng)拋出 java.io.IOException 異常時(shí)回滾

package demo.spring.boot.transaction.service.impl;

import demo.spring.boot.transaction.dao.UserDao;
import demo.spring.boot.transaction.domain.User;
import demo.spring.boot.transaction.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service
public class UserServiceImpl implements UserService {
    
    private final UserDao userDao;
    
    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    
    @Override
    @Transactional(rollbackFor = {IOException.class})
    public void addUser(User user)
        throws IOException {
        userDao.insert(user);
        throw new IOException();
    }
}

運(yùn)行單元測(cè)試况芒,測(cè)試失敗惜纸,但是查詢數(shù)據(jù)庫(kù) user 表無(wú)數(shù)據(jù)記錄,說(shuō)明異常拋出前 DAO 插入的數(shù)據(jù)已被成功回滾(省略數(shù)據(jù)庫(kù)查詢結(jié)果)。

如果賦予 rollbackFor 屬性其它異常類型耐版,既不是 java.io.IOException 又不是其父類祠够,則運(yùn)行單元測(cè)試后盡管測(cè)試失敗,但是異常拋出前的數(shù)據(jù)也會(huì)被插入數(shù)據(jù)庫(kù) user 表中粪牲,請(qǐng)自行測(cè)試古瓤。

項(xiàng)目工程目錄


POM文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>demo.spring.boot</groupId>
    <artifactId>demo-spring-boot-transaction</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo-spring-boot-transaction</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市腺阳,隨后出現(xiàn)的幾起案子落君,更是在濱河造成了極大的恐慌,老刑警劉巖舌狗,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叽奥,死亡現(xiàn)場(chǎng)離奇詭異扔水,居然都是意外死亡痛侍,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門魔市,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)主届,“玉大人,你說(shuō)我怎么就攤上這事待德【。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵将宪,是天一觀的道長(zhǎng)绘闷。 經(jīng)常有香客問(wèn)我,道長(zhǎng)较坛,這世上最難降的妖魔是什么印蔗? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮丑勤,結(jié)果婚禮上华嘹,老公的妹妹穿的比我還像新娘。我一直安慰自己法竞,他們只是感情好耙厚,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般窒悔。 火紅的嫁衣襯著肌膚如雪思劳。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,821評(píng)論 1 290
  • 那天泛豪,我揣著相機(jī)與錄音,去河邊找鬼。 笑死诡曙,一個(gè)胖子當(dāng)著我的面吹牛臀叙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播价卤,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼劝萤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了慎璧?” 一聲冷哼從身側(cè)響起床嫌,我...
    開(kāi)封第一講書(shū)人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎胸私,沒(méi)想到半個(gè)月后厌处,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡岁疼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年阔涉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片捷绒。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡瑰排,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出暖侨,到底是詐尸還是另有隱情椭住,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布字逗,位于F島的核電站京郑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏葫掉。R本人自食惡果不足惜些举,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望挖息。 院中可真熱鬧金拒,春花似錦、人聲如沸套腹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)电禀。三九已至幢码,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尖飞,已是汗流浹背症副。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工店雅, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贞铣。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓闹啦,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親辕坝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子窍奋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容