Spring核心之事务详解篇

Spring核心之事务详解篇

一、Spring整合MyBatis

第一步,引入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
63
64
65
66
67
<?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>com.huling</groupId>
<artifactId>MyBatisTest</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.18</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>

</project>

第二步,编写Spring配置文件:

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
<?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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/huling?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false&amp;allowPublicKeyRetrieval=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.huling.ibatis.entity"/>
<property name="mapperLocations">
<list>
<value>classpath:mapper/*Mapper.xml</value>
</list>
</property>
</bean>

<bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
<property name="basePackage" value="com.huling.ibatis.dao"/>
</bean>
</beans>

第三步,创建数据表t_user

1
2
3
4
5
6
7
8
9
mysql> desc t_user;
+----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(12) | YES | | NULL | |
| password | varchar(12) | YES | | NULL | |
+----------+-------------+------+-----+---------+----------------+
3 rows in set (0.04 sec)

第四步,创建实体类com.huling.ibatis.entity.User

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
public class User {
private Integer id;
private String name;
private String password;

public User() {
}

public User(Integer id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

第五步,创建数据访问层com.huling.ibatis.dao.UserDAO

1
2
3
public interface UserDAO {
public void save(User user);
}

第六步,创建Mapper映射文件resources/mapper/UserDAOMapper.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.huling.ibatis.dao.UserDAO">
<insert id="save" parameterType="User">
insert into t_user(name,password) values (#{name},#{password})
</insert>
</mapper>

第七步,编写测试方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* 测试Spring与Mybatis的整合
* @throws IOException
*/
@Test
public void test1() throws IOException {
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserDAO userDAO = ctx.getBean("userDAO", UserDAO.class);
User user = new User(null,"admin","123456");
userDAO.save(user);
}

image-20231031181129301

二、Spring的事务控制

事务(Transaction)是指作为一个逻辑工作单元执行的一系列操作,这些操作要么全部成功完成,要么全部失败回滚。事务是数据库管理系统中保证数据一致性和完整性的重要机制。

事务具有以下四个特性,通常被称为ACID特性:

  1. 原子性(Atomicity):事务是一个原子操作单位,不可分割。它要么全部执行成功,要么全部撤销,没有中间状态。如果事务的所有操作都成功执行,就会被提交;如果有任何一个操作失败,就会被回滚到事务开始前的状态。
  2. 一致性(Consistency):在事务开始之前和事务结束之后,数据库的完整性约束没有被破坏。事务执行过程中的中间状态不会被其他事务所见,只有在事务提交后才会对其他事务可见。
  3. 隔离性(Isolation):多个事务并发执行时,每个事务的执行都应该像是在独立的环境中执行,互相之间不会干扰。每个事务都应该感知不到其他事务的存在,避免出现读取未提交数据、脏读、不可重复读和幻读等并发问题。
  4. 持久性(Durability):一旦事务被提交,对数据库的修改将会永久保存,即使系统发生故障也不会丢失。持久性确保了数据的可靠性和持久可追溯性。

控制事务的底层都是通过连接对象(Connection)来控制事务的,Mybatis的SqlSession底层也是对Connection的封装。

第一步,引入依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>

第二步,编写业务逻辑层的UserService接口及其实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface UserService {
public void register(User user);
}

// 类中的所有方法都作为事务功能的切入点
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED)
public class UserServiceImpl implements UserService{
private UserDAO userDAO;

public UserDAO getUserDAO() {
return userDAO;
}

public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}

@Override
public void register(User user) {
userDAO.save(user);
throw new RuntimeException("测试Spring事务回滚");
}
}

@Transactional注解可以加在指定方法或者类上。

第三步,编写Spring配置文件:

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
<?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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/huling?serverTimezone=UTC&amp;characterEncoding=utf8&amp;useUnicode=true&amp;useSSL=false&amp;allowPublicKeyRetrieval=true"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</bean>

<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="typeAliasesPackage" value="com.huling.ibatis.entity"/>
<property name="mapperLocations">
<list>
<value>classpath:mapper/*Mapper.xml</value>
</list>
</property>
</bean>

<bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactoryBean"/>
<property name="basePackage" value="com.huling.ibatis.dao"/>
</bean>

<!--业务对象,需要进行业务运算和DAO操作,注入MyBatis提供的userDAO对象-->
<bean id="userService" class="com.huling.ibatis.service.UserServiceImpl">
<property name="userDAO" ref="userDAO"/>
</bean>

<!--增加事务的额外功能,需要通过连接池获得Connection连接对象控制事务-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>

<!--组装事务的切面,自动扫描Transactional注解作为切入点-->
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
</beans>

整个Spring事务控制的底层就是使用了AOP,tx:annotation-driven可以通过配置proxy-target-class="true" 强制底层在创建代理对象的时候使用CGLIB代理方式。

最后,编写测试程序:

1
2
3
4
5
6
7
8
9
/**
* 测试Spring事务处理
*/
@Test
public void test2() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("/applicationContext.xml");
UserService userService = ctx.getBean("userService", UserService.class);
userService.register(new User(null,"tom","123456"));
}

image-20231031182922711

三、Spring的事务属性

Spring的事务属性是用来设置事务管理器的行为特性,可以通过在方法上添加注解或XML配置文件中配置来定义事务属性。

  1. 传播行为(Propagation):指在嵌套事务中如何控制事务的传播,常用值包括:
    1. REQUIRED:如果当前已经存在一个事务,则加入该事务,否则新建一个事务,这是默认值,主要用于增删改的方法。
    2. SUPPORTS:如果当前已经存在一个事务,则加入该事务,否则以非事务的方式执行,主要应用在查询方法。
    3. REQUIRES_NEW:每次都会新建一个事务,如果当前已经存在一个事务,则将当前事务挂起(暂停),始终采用独立事务方法,主要用于日志记录的方法。
    4. NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务则挂起当前事务。
    5. NEVER:以非事务方式执行操作,如果当前存在事务就抛出异常。
    6. NESTED:如果当前已经存在一个事务,则在该事务内嵌套一个子事务,否则新建一个事务。如果子事务失败,则只回滚子事务,而不回滚父事务。如果父事务失败,则回滚所有事务。
  2. 隔离级别(Isolation):指并发事务之间如何隔离交错,常用值包括:
    1. DEFAULT:使用默认隔离级别。(数据库默认的隔离级别,例如MySQL默认为REPEATABLE READ)
    2. READ_UNCOMMITTED:未提交读,最低的隔离级别,允许读取其他事务未提交的数据,容易出现脏读、不可重复读和幻读等问题。
    3. READ_COMMITTED:已提交读,保证一个事务提交后另一个事务才能读取修改过的数据,避免脏读问题,但会出现不可重复读和幻读问题。
    4. REPEATABLE_READ:可重复读,保证一个事务在整个过程中多次读取数据时,数据的状态不发生变化,避免出现脏读和不可重复读问题,但依然会出现幻读问题。
    5. SERIALIZABLE:串行化,最高的隔离级别,完全隔离各个事务,一次只允许一个事务访问一个数据,也是最耗费资源的隔离级别。
  3. 读写规则(Read-only rules):指是否允许当前事务进行读写操作,默认值为false,表示该事务既可以进行读操作也可以进行写操作,当设置为true时表示只允许进行读操作。当一个事务方法中只包含读取数据库的操作,而没有任何修改数据的操作时,可以将该事务标记为只读事务。通过设置只读属性,可以告诉数据库引擎在执行这个事务期间采取一些优化措施,以提高性能和并发度。
    1. 提高性能:只读事务不需要对数据进行修改和锁定,因此可以减少数据库引擎的工作量和资源消耗。数据库引擎可以针对只读事务进行一些优化,例如跳过日志记录、减少锁的竞争等,从而提高查询性能。
    2. 增加并发度:只读事务不会对数据进行修改,因此多个只读事务可以并发地执行,而不会相互干扰。这可以提高系统的并发度和吞吐量,减少用户的等待时间。
    3. 减少风险:只读事务不会修改数据,因此不会引入数据一致性的风险。如果一个方法只需要读取数据而不需要修改数据,将其标记为只读事务可以确保数据的完整性和一致性。
  4. 超时时间(Timeout):指事务允许执行的最长时间,超时后事务将被回滚。默认值为-1,表示不设置超时时间。当一个事务方法执行时间过长,可能会占用数据库连接和其他资源,导致其他事务无法及时获取到资源(主要是数据库连接资源),从而产生阻塞和性能问题(长事务问题)。为了避免这种情况的发生,可以设置事务超时属性,告诉Spring事务管理器在一定时间内强制结束当前事务。具体来说,Spring的事务超时属性可以指定一个时间限制(以秒为单位,超出指定时间将抛出异常:TransactionTimedOutException),如果事务方法执行时间超过这个限制,则事务管理器将自动回滚该事务,并释放相关资源。这可以避免事务执行时间过长导致资源浪费和阻塞的问题,提高系统的可靠性和性能。
  5. 回滚规则(Rollback rules):事务管理器会捕获事务方法中抛出的异常,并根据异常类型和事务配置进行相应的处理。通过设置事务异常属性,可以指定哪些异常需要回滚事务,哪些异常需要忽略或转换为其他异常类型。

需要注意的是,较高的隔离级别可能会对并发性能产生一定的影响,因此在选择隔离级别时需要综合考虑应用程序的并发访问情况和性能需求。

image-20240818171526629