什么是事務(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>