Spring事务管理

Spring事务管理

数据库事务(Database Transaction)是指将一系列数据库操作当作一个逻辑处理单元的操作,这个单元中的数据库操作要么完全执行,要么完全不执行。通过将一组相关操作组合为一个逻辑处理单元,可以简化错误恢复,并使应用程序更加可靠。

事务的特性

一个逻辑处理单元要成为事务,必须满足ACID(原子性、一致性、隔离性和持久性)属性。所谓的ACID含义如下。
●原子性(Atomicity):一个事务内的操作,要么全部执行成功,要么全部不成功。
●一致性(Consistency):事务执行后,数据库状态与其他业务规则保持一致。如转账业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。
●隔离性(Isolation):每个事务独立运行。在并发环境中,并发的事务是互相隔离的,互不影响。
●持久性(Durability):事务一旦提交后,数据库中的数据必须被永久地保存下来。

JDBC事务

在Java基础的学习中可以知道JDBC事务通常通过Connection接口的方法来控制事务开始,事务提交以及事务回滚,下面,先通过简单的实例回顾JDBC事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package cn.bytecollege.util;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class DBUtil {
//省略加载驱动步骤
public void save() throws SQLException {
String url = "jdbc:mysql://localhost:3306?chapter05";
String username = "root";
String password = "root";
Connection connection = null;
try{
connection = DriverManager.getConnection(url,username,password);
//开启事务
connection.setAutoCommit(false);
//省略新增操作
//提交事务
connection.commit();
}catch (SQLException e){
e.printStackTrace();
connection.rollback();
}
}
}

直接使用JDBC的编程方式管理事务,虽然可以完成对应的功能,但这种编程方式对代码的复用性不高。为了解决通过直接适应JDBC的方式对事务进行控制,提高代码复用性的问题,Spring也提供了对事务进行控制的相关API,下面将介绍使用Spring的方式进行事务管理。

Spring事务管理

在正式学习Spring事务管理前,需要做一些准备工作,首先准备数据库,并初始化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE DATABASE SPRING_CHAPTER05;
USE SPRING_CHAPTER05;
CREATE TABLE STUDENT(
STUDENT_ID INT PRIMARY KEY AUTO_INCREMENT,
STUDENT_NAME VARCHAR(20),
STUDENT_AGE TINYINT,
STUDENT_GENDER CHAR(2)
);
CREATE TABLE STUDENT_INFO(
STUDENT_DETAIL_ID INT PRIMARY KEY AUTO_INCREMENT,
STUDENT_ID INT,
MAIL VARCHAR(30),
TELEPHONE VARCHAR(15)
)
INSERT INTO `spring_chapter05`.`student`(`STUDENT_ID`, `STUDENT_NAME`, `STUDENT_AGE`, `STUDENT_GENDER`) VALUES (1, '张三', 19, '男');
INSERT INTO `spring_chapter05`.`student`(`STUDENT_ID`, `STUDENT_NAME`, `STUDENT_AGE`, `STUDENT_GENDER`) VALUES (2, '李四', 18, '女');

INSERT INTO `spring_chapter05`.`student_info`(`STUDENT_DETAIL_ID`, `STUDENT_ID`, `MAIL`, `TELEPHONE`) VALUES (1, 1, 'zhangsan@163.com', '17765868611');
INSERT INTO `spring_chapter05`.`student_info`(`STUDENT_DETAIL_ID`, `STUDENT_ID`, `MAIL`, `TELEPHONE`) VALUES (2, 2, 'lisi@163.com', '18676543421');

为了便于操作数据库以及学习Spring事务,在本小节内整合Mybatis框架。

Spring整合Mybatis

  1. 创建Maven项目,并添加如下依赖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!-- mybatis-spring -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
<!-- druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<!--lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
<!-- spring-tx -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.3.9</version>
</dependency>
<!--spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.9</version>
</dependency>
<!--log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>

需要注意的是Mybatis为了同Spring整合,推出了Mybatis-Spring项目,因此需要添加该依赖,除此以外还需要添加Spring-jdbc及Spring-tx等依赖。

2.新建cn.bytecollege.entity包,并在包中创建实体类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.bytecollege.entity;
import lombok.Data;
@Data
public class Student {
private int studentId;
private String studentName;
private int studentAge;
private String studentGender;
}
package cn.bytecollege.entity;
import lombok.Data;
@Data
public class StudentInfo {
private int studentDetailId;
private int studentId;
private String mail;
private String telephone;
}

3.新建cn.bytecollege.mapper接口,添加Mybatis映射器绑定的接口:

1
2
3
4
5
6
7
8
package cn.bytecollege.mapper;
import cn.bytecollege.entity.Student;

import java.util.List;

public interface StudentMapper {
List<Student> findAllStudent();
}
1
2
3
4
5
6
package cn.bytecollege.mapper;
import cn.bytecollege.entity.StudentInfo;
import java.util.List;
public interface StudentInfoMapper {
List<StudentInfo> findAllStudentDetail();
}

4.在resource目录下新建mapper路径,用于存放映射器文件。

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--1.namespace和接口的全限定名一致-->
<!--2.接口方法名和xml中sql语句ID一致-->
<!--3.sql语句resultType返回值类型和方法返回值类型一致-->
<!--4.输入参数类型与方法中一致-->
<mapper namespace="cn.bytecollege.mapper.StudentMapper">
<select id="findAllStudent" resultType="cn.bytecollege.entity.Student">
select STUDENT_ID, STUDENT_NAME, STUDENT_AGE, STUDENT_GENDER from student;
</select>
</mapper>
1
2
3
4
5
6
7
8
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.bytecollege.mapper.StudentInfoMapper">
<select id="findAllStudentDetail" resultType="cn.bytecollege.entity.StudentInfo">
select student_detail_id, student_id, mail, telephone from student_info
</select>
</mapper>

5.在resource目录下新建Mybatis配置文件:mybatis-config.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!--开启驼峰命名映射-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<!--扫描包下的类生成别名-->
<package name="cn.bytecollege.entity"/>
</typeAliases>
</configuration>

在Spring中整合Mybatis时,数据源和映射器位置可以在Spring配置文件中进行配置,因此在Mybatis配置文件中省略了数据源和映射器位置的配置。因此只配置了开启驼峰命名映射和生成实体类的别名。

6.在resource目录下新建Spring配置文件:spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!--配置扫描包-->
<context:component-scan base-package="cn.bytecollege"/>
<!--配置数据库信息文件-->
<context:property-placeholder location="classpath:data.properties"/>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 基本属性 url、user、password -->
<property name="url" value="${jdbc_url}" />
<property name="username" value="${jdbc_user}" />
<property name="password" value="${jdbc_password}" />
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="stat" />
<!-- 配置初始化大小、最小、最大 -->
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="6000" />
<property name="minIdle" value="1" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />

<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />

<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />

<property name="asyncInit" value="true" />
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置Mybatis sessionFactory-->
<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configLocation" value="classpath:mybatis-config.xml"></property>
<property name="mapperLocations" value="classpath:mapper/**/*.xml"></property>
</bean>
<!--配置扫描Mapper接口-->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="cn.bytecollege.mapper"></property>
</bean>
<!--使注解了@Transactional的Bean生效,以AOP的方式使事务生效-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

在Spring配置文件中需要配置以下信息:

  • 配置扫描注解包

  • 配置加载数据连接信息文件

  • 配置数据源,因为使用了Druid连接池,所以此处配置了DruidDataSource

  • 配置事务管理器

  • 配置Mybatis中SqlsessionFactoryBean,在Spring容器启动时创建SqlsessionFactory,SqlSessionFactoryBean需要配置以下3个属性:

    • dataSource:引用配置好的数据源
    • configLocation:Mybatis全局配置文件
    • mapperLocations:映射器所在位置
  • 配置扫描Mapper接口,MapperScannerConfigurer配置basePackage属性用于扫描同映射器绑定的接口,使用动态代理为其生成实现类代理

至此,Spring中已经成功整合Mybatis,新建测试类并调用StudentMapper中查询方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.bytecollege.test;

import cn.bytecollege.entity.Student;
import cn.bytecollege.mapper.StudentMapper;
import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;

public class Test {
public static void main(String[] args) {
Logger log = Logger.getLogger(Test.class);
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
StudentMapper mapper = context.getBean(StudentMapper.class);
List<Student> list = mapper.findAllStudent();
list.forEach(e->log.info(e));
}
}

Spring声明式事务

从数据库的设计上可以看出student表和student_info表存在一对一的关系,也就是说在添加学生基本信息时,需要同时添加学生详细信息,或者在学生详细信息中先添加一条占位数据,先不使用事务并在代码中抛出异常:

在接口中添加方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.bytecollege.mapper;
import cn.bytecollege.entity.Student;
import java.util.List;

public interface StudentMapper {
/**
* 查询所有学生
* @return
*/
List<Student> findAllStudent();

/**
* 新增学生
* @param student
* @return
*/
int addStudent (Student student);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.bytecollege.mapper;
import cn.bytecollege.entity.StudentInfo;
import java.util.List;
public interface StudentInfoMapper {
/**
* 查询所有学生信息
* @return
*/
List<StudentInfo> findAllStudentDetail();

/**
* 新增学生信息
* @param studentInfo
* @return
*/
int addStudentInfo(StudentInfo studentInfo);
}

在映射器中添加对应的SQL语句

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.bytecollege.mapper.StudentMapper">
<select id="findAllStudent" resultType="cn.bytecollege.entity.Student">
select STUDENT_ID, STUDENT_NAME, STUDENT_AGE, STUDENT_GENDER from student;
</select>
<insert id="addStudent" useGeneratedKeys="true" keyProperty="studentId" parameterType="cn.bytecollege.entity.Student">
insert into student (student_name, student_age, student_gender)
values
(#{studentName},#{studentAge},#{studentGender});
</insert>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.bytecollege.mapper.StudentInfoMapper">
<select id="findAllStudentDetail" resultType="cn.bytecollege.entity.StudentInfo">
select student_detail_id, student_id, mail, telephone from student_info
</select>
<insert id="addStudentInfo" parameterType="cn.bytecollege.entity.StudentInfo">
insert into student_info (STUDENT_ID, MAIL, TELEPHONE)
values
(#{studentId},#{mail},#{telephone})
</insert>
</mapper>

新建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cn.bytecollege.test;

import cn.bytecollege.entity.Student;
import cn.bytecollege.entity.StudentInfo;
import cn.bytecollege.mapper.StudentInfoMapper;
import cn.bytecollege.mapper.StudentMapper;
import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;

public class Test {
public static void main(String[] args) {
Logger log = Logger.getLogger(Test.class);
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

StudentMapper studentMapper = context.getBean(StudentMapper.class);
StudentInfoMapper studentInfoMapper = context.getBean(StudentInfoMapper.class);

Student student = new Student();
student.setStudentAge(18);
student.setStudentName("李雷");
student.setStudentGender("男");

StudentInfo studentInfo = new StudentInfo();
studentInfo.setMail("lilei@163.com");
studentInfo.setTelephone("17765213454");

studentMapper.addStudent(student);
//1.抛出异常
System.out.println(5/0);
studentInfo.setStudentId(student.getStudentId());
studentInfoMapper.addStudentInfo(studentInfo);
}
}

在测试类中注释1处人为抛出异常,运行测试类可以看出student表中添加了数据,但是student_info表中并没有添加数据,显然这违背了事务的原子性。运行结果如下图:

为了方便使用Spring声明式事务,新建StudentService类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package cn.bytecollege.service;

import cn.bytecollege.entity.Student;
import cn.bytecollege.entity.StudentInfo;
import cn.bytecollege.mapper.StudentInfoMapper;
import cn.bytecollege.mapper.StudentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StudentService {
@Autowired
private StudentMapper studentMapper;
@Autowired
private StudentInfoMapper studentInfoMapper;
@Transactional
public void add(Student student,StudentInfo studentInfo){
studentMapper.addStudent(student);
//1.抛出异常
System.out.println(5/0);
studentInfo.setStudentId(student.getStudentId());
studentInfoMapper.addStudentInfo(studentInfo);
}
}

可以看出在add()中认为抛出了异常,并且在方法上添加了@Transactional注解,该注解将会在下一小节进行讲解。

新建测试类,进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package cn.bytecollege.test;

import cn.bytecollege.entity.Student;
import cn.bytecollege.entity.StudentInfo;
import cn.bytecollege.service.StudentService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

StudentService studentService = context.getBean(StudentService.class);

Student student = new Student();
student.setStudentName("李雷");
student.setStudentAge(18);
student.setStudentGender("男");

StudentInfo studentInfo = new StudentInfo();
studentInfo.setMail("lilei@163.com");
studentInfo.setTelephone("17765878821");

studentService.add(student,studentInfo);
}
}

运行测试类可以看出数据库中数据并没有添加,也就是说数据库事务回滚了,此时删除StudentService中注释1处的代码,再次运行可以看出数据成功插入。

@Transactional注解

基于@Transactional注解的拥有一组普适性很强的默认事务属性,往往可以直接使用这些默认的属性。


  • 事务传播行为∶PROPAGATION REQUIRED。
  • 事务隔离级别∶ISOLATION_DEFAULT。
  • 读写事务属性∶读/写事务。

  • 超时时间∶依赖于底层的事务系统的默认值。

  • 回滚设置∶任何运行期异常引发回滚,任何检查型异常不会引发回滚。

这些默认设置在大多数情况下都是适用的,所以一般不需要手工设置事务注解的属性。

属性 说明

| Propagation | 事务传播行为,通过以下枚举类提供合法值:
org.springframework.transaction.annotation.Propagation
。例如:@Transactional(propagation = Propagation.REQUIRES_NEW) |
| isolation | 事务隔离级别,通过一下枚举类提供合法值:org.springframework.transaction.annotation.Isolation
例如∶@Transactional(isolation=Isolation.READ_COMMITTED) |
| readOnly | 事务读写性,布尔型。例如∶@Transactional(readOnly=true) |
| timeout | 超时时间,int 型,以秒为单位。例如∶@Transactional(timeout=10) |
| rollbackFor | 一组异常类,遇到时进行回滚,类型为∶Class<?extends Throwable>[],默认值为{}。例如∶

@Transactional(rollbackFor={SQLException.class})。多个异常之间可用逗号分隔. |
| rollbackForClassName | 一组异常类名,遇到时进行回滚,类型为String[],默认值为{}。例如:

@Transactional(rollbackForClassName={SQLException}) |
| noRollbackFor | 一组异常类,遇到时不回滚,类型为Class<? extends Throwable>[],默认值为{} |
| noRollbackForClassName | 一组异常类名,遇到时不回滚,类型为String[],默认值为{} |

Spring事务隔离级别

Spring对事务的支持提供了5种隔离级别,各种隔离级别的含义如表所示。

Spring事务传播行为

事务传播行为是用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时候,事务的传播特性。Spring中定义了7种事务传播行为,如表所示。