⭐表示重要。

第一章:准备工作

1.1 环境搭建

  • IDEA 2021+。
  • JDK 11。
  • Maven 3.8。
  • MySQL 5.7。

1.2 导入依赖

  • pom.xml
  1. <!-- Spring -->
  2. <dependency>
  3. <groupId>org.springframework</groupId>
  4. <artifactId>spring-context</artifactId>
  5. <version>5.3.12</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework</groupId>
  9. <artifactId>spring-aspects</artifactId>
  10. <version>5.3.12</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.springframework</groupId>
  14. <artifactId>spring-test</artifactId>
  15. <version>5.3.12</version>
  16. </dependency>
  17. <!-- Spring 持久化层支持jar包 -->
  18. <!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 -->
  19. <!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
  20. <dependency>
  21. <groupId>org.springframework</groupId>
  22. <artifactId>spring-orm</artifactId>
  23. <version>5.3.12</version>
  24. </dependency>
  25. <!-- MySQL驱动 -->
  26. <dependency>
  27. <groupId>mysql</groupId>
  28. <artifactId>mysql-connector-java</artifactId>
  29. <version>8.0.19</version>
  30. </dependency>
  31. <!-- junit单元测试 -->
  32. <dependency>
  33. <groupId>junit</groupId>
  34. <artifactId>junit</artifactId>
  35. <version>4.13.2</version>
  36. <scope>test</scope>
  37. </dependency>
  38. <!-- 数据库连接池 -->
  39. <dependency>
  40. <groupId>com.alibaba</groupId>
  41. <artifactId>druid</artifactId>
  42. <version>1.2.8</version>
  43. </dependency>

1.3 数据库属性文件

  • db.properties
  1. jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
  2. jdbc.driverClass=com.mysql.cj.jdbc.Driver
  3. jdbc.username=root
  4. jdbc.password=123456

1.4 SQL 脚本

  1. CREATE DATABASE IF NOT EXISTS `test` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
  2. USE `test`
  3. DROP TABLE IF EXISTS `emp`;
  4. CREATE TABLE `emp` (
  5. `emp_id` int(11) NOT NULL AUTO_INCREMENT,
  6. `emp_name` varchar(255) DEFAULT NULL,
  7. `emp_salary` double DEFAULT NULL,
  8. PRIMARY KEY (`emp_id`)
  9. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  10. INSERT INTO `emp` VALUES (1, '张三', 5000);
  11. INSERT INTO `emp` VALUES (2, '李四', 6000);
  12. INSERT INTO `emp` VALUES (3, '王五', 7000);

SQL脚本.png

1.5 实体类

  • Emp.java
  1. package com.github.fairy.era.bean;
  2. import java.io.Serializable;
  3. /**
  4. * @author 许大仙
  5. * @version 1.0
  6. * @since 2021-11-09 14:19
  7. */
  8. public class Emp implements Serializable {
  9. private Integer empId;
  10. private String empName;
  11. private Double empSalary;
  12. public Emp() {
  13. }
  14. public Emp(Integer empId, String empName, Double empSalary) {
  15. this.empId = empId;
  16. this.empName = empName;
  17. this.empSalary = empSalary;
  18. }
  19. public Integer getEmpId() {
  20. return empId;
  21. }
  22. public void setEmpId(Integer empId) {
  23. this.empId = empId;
  24. }
  25. public String getEmpName() {
  26. return empName;
  27. }
  28. public void setEmpName(String empName) {
  29. this.empName = empName;
  30. }
  31. public Double getEmpSalary() {
  32. return empSalary;
  33. }
  34. public void setEmpSalary(Double empSalary) {
  35. this.empSalary = empSalary;
  36. }
  37. @Override
  38. public String toString() {
  39. return "Emp{" +
  40. "empId=" + empId +
  41. ", empName='" + empName + '\'' +
  42. ", empSalary='" + empSalary + '\'' +
  43. '}';
  44. }
  45. }

1.6 Spring 的配置文件

  • applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns="http://www.springframework.org/schema/beans"
       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 http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.github.fairy.era"></context:component-scan>

    <!-- 导入数据库连接信息 -->
    <context:property-placeholder location="db.properties"></context:property-placeholder>

    <!-- 配置数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClass}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!-- 配置jdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 开启AOP的注解支持 -->
    <aop:aspectj-autoproxy/>
</beans>

1.7 测试类

package com.github.fairy.era.bean;

import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class SpringTest {

}

1.8 创建组件

1.8.1 持久层组件

  • EmpDao.java
package com.github.fairy.era.dao;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:23
 */
public interface EmpDao {

    void updateEmpNameById(Integer empId, String empName);

    void updateEmpSalaryById(Integer empId, double empSalary);

    String selectEmpNameById(Integer empId);
}
  • EmpDaoImpl.java
package com.github.fairy.era.dao.impl;

import com.github.fairy.era.dao.EmpDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:23
 */
@Repository
public class EmpDaoImpl implements EmpDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void updateEmpNameById(Integer empId, String empName) {
        jdbcTemplate.update(" UPDATE emp SET emp_name = ? WHERE emp_id = ? ", empName, empId);
    }

    @Override
    public void updateEmpSalaryById(Integer empId, double empSalary) {
        jdbcTemplate.update(" UPDATE emp SET emp_salary = ? WHERE emp_id = ? ", empSalary, empId);
    }

    @Override
    public String selectEmpNameById(Integer empId) {
        return jdbcTemplate.queryForObject(" SELECT emp_name FROM emp WHERE emp_id = ? ", String.class, empId);
    }
}

1.8.2 业务层组件

  • 在三层结构中,业务通常都是加到业务逻辑层,针对 Service 类使用事务。

  • EmpService.java

package com.github.fairy.era.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:45
 */
public interface EmpService {

    /**
     * 为了测试事务是否生效,执行两个数据库操作,看它们是否会在失败的时候一起回滚
     *
     * @param emp4EditNameId   修改员工姓名的id
     * @param name             员工姓名
     * @param emp4EditSalaryId 修改员工工资的id
     * @param salary           员工工资
     */
    void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary);
}
  • EmpServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) {
        empDao.updateEmpNameById(emp4EditNameId, name);

        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
    }
}

1.8.3 测试

package com.github.fairy.era.bean;

import com.github.fairy.era.service.EmpService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class SpringTest {

    @Autowired
    private EmpService empService;

    @Test
    public void test() {
        empService.update(1, "张三1", 2, 6000.34);
    }
}

第二章:应用最基本的事务控制

2.1 加事务前

2.1.1 搞破坏

  • 修改 EmpServiceImpl 中的 update() 方法:
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) {
        empDao.updateEmpNameById(emp4EditNameId, name);
        int i = 10 / 0;
        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
    }
}

2.1.2 测试 Service 方法

  • 测试:
package com.github.fairy.era.bean;

import com.github.fairy.era.service.EmpService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class SpringTest {

    @Autowired
    private EmpService empService;

    @Test
    public void testBaseTx() {
        empService.update(1, "张三1", 2, 6000.34);
    }
}
  • 结果:
java.lang.ArithmeticException: / by zero

    at com.github.fairy.era.service.impl.EmpServiceImpl.update(EmpServiceImpl.java:22)
    at com.github.fairy.era.bean.SpringTest.testBaseTx(SpringTest.java:24)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

加事务前执行结果.png

2.2 添加事务功能(⭐)

2.2.1 配置事务管理器以及开启基于注解的声明式事务功能

  • applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns="http://www.springframework.org/schema/beans"
       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 http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 事务管理器的bean只需要装配数据源,其他属性保持默认值即可 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 开启基于注解的声明式事务功能 -->
    <!-- 使用transaction-manager属性指定当前使用是事务管理器的bean -->
    <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>
  • 完整的 applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns="http://www.springframework.org/schema/beans"
       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 http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.github.fairy.era"></context:component-scan>

    <!-- 导入数据库连接信息 -->
    <context:property-placeholder location="db.properties"></context:property-placeholder>

    <!-- 配置数据库连接池 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driverClass}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>

    <!-- 配置jdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 开启AOP的注解支持 -->
    <aop:aspectj-autoproxy/>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 事务管理器的bean只需要装配数据源,其他属性保持默认值即可 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 开启基于注解的声明式事务功能 -->
    <!-- 使用transaction-manager属性指定当前使用是事务管理器的bean -->
    <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

2.2.2 在需要事务的方法上使用注解

  • EmpServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Transactional
    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) {
        empDao.updateEmpNameById(emp4EditNameId, name);
        int i = 10 / 0;
        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
    }
}

2.2.3 测试 Service 方法

  • 测试:
package com.github.fairy.era.bean;

import com.github.fairy.era.service.EmpService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class SpringTest {

    @Autowired
    private EmpService empService;

    @Test
    public void testBaseTx() {
        empService.update(1, "张三1", 2, 6000.34);
    }
}
  • 结果:
java.lang.ArithmeticException: / by zero

    at com.github.fairy.era.service.impl.EmpServiceImpl.update(EmpServiceImpl.java:24)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
    at com.sun.proxy.$Proxy21.update(Unknown Source)
    at com.github.fairy.era.bean.SpringTest.testBaseTx(SpringTest.java:24)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:69)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)

添加事务执行结果.png

2.3 @Transactional 注解的源码

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

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

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    String timeoutString() default "";

    boolean readOnly() default false;

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

    String[] rollbackForClassName() default {};

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

    String[] noRollbackForClassName() default {};
}

2.4 从日志角度查看事务效果

2.4.1 加入依赖

  • pom.xml
<!-- 加入日志 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

2.4.2 加入 logback 的配置文件

  • logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <!-- 指定日志输出的位置 -->
    <appender name="STDOUT"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 日志输出的格式 -->
            <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
            <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
        </encoder>
    </appender>

    <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
    <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
    <root level="INFO">
        <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
        <appender-ref ref="STDOUT" />
    </root>

    <!-- 根据特殊需求指定局部日志级别 -->
    <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" level="DEBUG"/>
    <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG" />

</configuration>

2.4.3 日志中事务相关内容

15:49:16.042 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Creating new transaction with name [com.github.fairy.era.service.impl.EmpServiceImpl.update]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
15:49:16.143 [main] INFO com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
15:49:16.245 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@17ae98d7] for JDBC transaction
15:49:16.247 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@17ae98d7] to manual commit
15:49:16.248 [main] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL update
15:49:16.248 [main] DEBUG org.springframework.jdbc.core.JdbcTemplate - Executing prepared SQL statement [ UPDATE emp SET emp_name = ? WHERE emp_id = ? ]
15:49:16.261 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Initiating transaction rollback
15:49:16.261 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@17ae98d7]
15:49:16.301 [main] DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager - Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@17ae98d7] after transaction
15:49:16.302 [main] DEBUG org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate - Retrieved ApplicationContext [960733886] from cache with key [[MergedContextConfiguration@1757cd72 testClass = SpringTest, locations = '{classpath:applicationContext.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]]
15:49:16.302 [main] DEBUG org.springframework.test.context.cache - Spring test ApplicationContext cache statistics: [DefaultContextCache@2b62442c size = 1, maxSize = 32, parentContextCount = 0, hitCount = 4, missCount = 1]
15:49:16.303 [main] DEBUG org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate - Retrieved ApplicationContext [960733886] from cache with key [[MergedContextConfiguration@1757cd72 testClass = SpringTest, locations = '{classpath:applicationContext.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]]]
15:49:16.303 [main] DEBUG org.springframework.test.context.cache - Spring test ApplicationContext cache statistics: [DefaultContextCache@2b62442c size = 1, maxSize = 32, parentContextCount = 0, hitCount = 5, missCount = 1]
15:49:16.304 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - After test method: context [DefaultTestContext@2e377400 testClass = SpringTest, testInstance = com.github.fairy.era.bean.SpringTest@68e5eea7, testMethod = testBaseTx@SpringTest, testException = java.lang.ArithmeticException: / by zero, mergedContextConfiguration = [MergedContextConfiguration@1757cd72 testClass = SpringTest, locations = '{classpath:applicationContext.xml}', classes = '{}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{}', contextCustomizers = set[[empty]], contextLoader = 'org.springframework.test.context.support.DelegatingSmartContextLoader', parent = [null]], attributes = map['org.springframework.test.context.event.ApplicationEventsTestExecutionListener.recordApplicationEvents' -> false]], class annotated with @DirtiesContext [false] with mode [null], method annotated with @DirtiesContext [false] with mode [null].

第三章:事务属性:只读(⭐)

3.1 概述

  • 对一个查询操作来说,如果我们将其设置为只读,就能够明确的告诉数据库,这个操作不涉及到写操作,这样数据库就能够对查询操作来进行优化。
  • @Transactional 注解的 readOnly 属性的默认值为false。

3.2 设置方式

  • EmpService.java
package com.github.fairy.era.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:45
 */
public interface EmpService {

    /**
     * 根据id查询姓名
     *
     * @param empId
     * @return
     */
    String getEmpName(Integer empId);
}
  • EmpServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Service
public class EmpServiceImpl implements EmpService {

    // readOnly = true 将当前事务设置为只读
    @Transactional(readOnly = true)
    @Override
    public String getEmpName(Integer empId) {
        return empDao.selectEmpNameById(empId);
    }
}

3.3 针对增删改操作设置只读

  • 会抛出如下的异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:89)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:63)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1064)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1040)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1347)
    at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1025)
    at com.alibaba.druid.pool.DruidPooledPreparedStatement.executeUpdate(DruidPooledPreparedStatement.java:255)
    at org.springframework.jdbc.core.JdbcTemplate.lambda$update$2(JdbcTemplate.java:965)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:651)
    ... 50 more

3.4 @Transactional 注解标注在类上

3.4.1 生效原则

  • 如果一个类上的每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上,换言之, @Transactional 注解标注在类上,会影响到类中的每一个方法;同时,类级别标记的 @Transactional 注解中设置的属性也会延续到方法执行时的事务属性,除非在方法上又设置了 @Transactional 注解。
  • 对于一个方法来说,离它最近的 @Transactional 注解中的事务属性设置有效(就近原则)。

3.4.2 应用示例

  • 在类级别 @Transactional 注解中设置只读属性,这样类中的所有查询方法都不需要设置 @Transactional 直接了,因为对于查询操作来说,其他属性通常不需要设置,只需要使用公共属性即可。然后在这个基础上,对增删改方法设置 @Transactional 注解的 readOnly 属性为 false。

  • 示例:

  • EmpService.java
package com.github.fairy.era.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:45
 */
public interface EmpService {

    /**
     * 为了测试事务是否生效,执行两个数据库操作,看它们是否会在失败的时候一起回滚
     *
     * @param emp4EditNameId   修改员工姓名的id
     * @param name             员工姓名
     * @param emp4EditSalaryId 修改员工工资的id
     * @param salary           员工工资
     */
    void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary);

    /**
     * 根据id查询姓名
     *
     * @param empId
     * @return
     */
    String getEmpName(Integer empId);
}
  • EmpServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Transactional
    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) {
        empDao.updateEmpNameById(emp4EditNameId, name);
        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
    }

    @Override
    public String getEmpName(Integer empId) {
        return empDao.selectEmpNameById(empId);
    }
}

第四章:事务属性:超时

4.1 概述

  • 事务在执行过程中,可能因此遇到某些问题,导致程序假死,长时间占用数据库的资源,大概率是因为程序运行出了问题(可能是 Java 程序或 MySQL 数据库或网络连接等等),此时这个可能出现问题的程序应该被回滚,撤销它所做的操作,让其他正常的程序可以执行。
  • 换言之:超时回滚,释放资源。

4.2 设置超时

  • EmpService.java
package com.github.fairy.era.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:45
 */
public interface EmpService {

    /**
     * 为了测试事务是否生效,执行两个数据库操作,看它们是否会在失败的时候一起回滚
     *
     * @param emp4EditNameId   修改员工姓名的id
     * @param name             员工姓名
     * @param emp4EditSalaryId 修改员工工资的id
     * @param salary           员工工资
     */
    void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary);

    /**
     * 根据id查询姓名
     *
     * @param empId
     * @return
     */
    String getEmpName(Integer empId);
}
  • EmpServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    // 通过timeout设置超时时间
    @Transactional(timeout = 3)
    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) {
        empDao.updateEmpNameById(emp4EditNameId, name);
        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
    }

    @Override
    public String getEmpName(Integer empId) {
        return empDao.selectEmpNameById(empId);
    }
}

第五章:事务属性:回滚和不回滚的异常(⭐)

5.1 默认情况

  • 默认情况下,只针对运行时异常回滚,编译时异常不回滚。

  • 示例:运行时异常回滚

package com.github.fairy.era.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:45
 */
public interface EmpService {

    /**
     * 为了测试事务是否生效,执行两个数据库操作,看它们是否会在失败的时候一起回滚
     *
     * @param emp4EditNameId   修改员工姓名的id
     * @param name             员工姓名
     * @param emp4EditSalaryId 修改员工工资的id
     * @param salary           员工工资
     */
    void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary);
}
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    // 通过timeout设置超时时间
    @Transactional(timeout = 3)
    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) {
        empDao.updateEmpNameById(emp4EditNameId, name);
        int i = 10 / 0;
        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
    }
}
package com.github.fairy.era.bean;

import com.github.fairy.era.service.EmpService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class SpringTest {

    @Autowired
    private EmpService empService;

    @Test
    public void testBaseTx() {
        empService.update(1, "张三1", 2, 6000.34);
    }
}

事务属性:回滚和不回滚的异常默认情况1.png

  • 示例:编译时异常不回滚
package com.github.fairy.era.service;

import java.io.FileNotFoundException;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:45
 */
public interface EmpService {

    /**
     * 为了测试事务是否生效,执行两个数据库操作,看它们是否会在失败的时候一起回滚
     *
     * @param emp4EditNameId   修改员工姓名的id
     * @param name             员工姓名
     * @param emp4EditSalaryId 修改员工工资的id
     * @param salary           员工工资
     */
    void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) throws FileNotFoundException;

}
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.FileNotFoundException;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    // 通过timeout设置超时时间
    @Transactional(timeout = 3)
    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) throws FileNotFoundException {
        empDao.updateEmpNameById(emp4EditNameId, name);
        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
        throw new FileNotFoundException("编译时异常");
    }

}
package com.github.fairy.era.bean;

import com.github.fairy.era.service.EmpService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.FileNotFoundException;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class SpringTest {

    @Autowired
    private EmpService empService;

    @Test
    public void testBaseTx() throws FileNotFoundException {
        empService.update(1, "张三1", 2, 6000.34);
    }
}

事务属性:回滚和不回滚的异常默认情况2.png

5.2 设置回滚的异常

  • rollbackFor 属性(常用):需要设置一个 Class 类型的对象数组。
  • rollbackForClassName 属性:需要设置一个字符串类型的全类名数组。

  • 示例:

package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.FileNotFoundException;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    // 设置回滚的异常
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) throws FileNotFoundException {
        empDao.updateEmpNameById(emp4EditNameId, name);
        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
        throw new FileNotFoundException("编译时异常");
    }

}

5.3 设置不回滚的异常

  • 在默认设置和已有设置的基础上,再指定一个异常类型,让 Spring 遇到这个异常不回滚。
  • noRollbackFor 属性(常用):需要设置一个 Class 类型的对象数组。
  • noRollbackForClassName 属性:需要设置一个字符串类型的全类名数组。

  • 示例:

package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.FileNotFoundException;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    // rollbackFor:设置回滚的异常,noRollbackFor:设置不回滚的异常
    @Transactional(rollbackFor = Exception.class, noRollbackFor = FileNotFoundException.class)
    @Override
    public void update(Integer emp4EditNameId, String name, Integer emp4EditSalaryId, Double salary) throws FileNotFoundException {
        empDao.updateEmpNameById(emp4EditNameId, name);
        empDao.updateEmpSalaryById(emp4EditSalaryId, salary);
        throw new FileNotFoundException("编译时异常");
    }

}

5.4 回滚和不回滚异常同时设置

5.4.1 范围不同

  • 不管是那个设置范围大,都是在大范围内排除小范围的设置,例如:
@Transactional(rollbackFor = Exception.class, noRollbackFor = FileNotFoundException.class)
  • 意思是除了 FileNotFoundException 之外,其他的所有 Exception 范围的异常都回滚,但是遇到 FileNotFoundException 不回滚。

5.4.2 范围相同

  • 回滚和不回滚的异常设置了相同范围,例如:
@Transactional(rollbackFor = FileNotFoundException.class, noRollbackFor = FileNotFoundException.class)
  • 此时 Spring 采用了 rollbackFor 属性的设定,遇到 FileNotFoundException 异常会回滚。

第六章:事务属性:事务的隔离级别

6.1 事务回顾

6.1.1 事务的特性(ACID)

  • A(原子性):事务中包含的数据库操作缺一不可,整个事务是不可再分的。
  • C(一致性):事务执行之前,数据库中的数据整体是正确的;事务执行之后,数据库中的数据整体仍然是正确的。
    • 事务执行成功:提交(commit)。
    • 事务执行失败:回滚(rollback)。
  • I(隔离性):数据库系统同时执行很多事务时,各个事务之间基于不同隔离级别能够在一定程度上做到互不干扰。简单说就是:事务在并发执行过程中彼此隔离。
  • D(持久性):事务一旦提交,就永久保存到数据库中,不可撤销。

6.1.2 隔离级别

  • 并发问题: | 并发问题 | 问题描述 | | —- | —- | | 脏读 | 当前事务读取了其他事务尚未提交的修改 如果那个事务回滚,那么当前事务读取到的修改就是错误的数据 | | 不可重复读 | 当前事务读取同一个数据,第一次和第二次不一致 | | 幻读 | 当前事务在执行过程中,数据库表增减或减少了一些记录,感觉像是出现了幻觉 |

  • 隔离级别: | 隔离级别 | 描述 | 能解决的并发问题 | | —- | —- | —- | | 读未提交 | 允许当前事务读取其他事务尚未提交的修改 | 啥问题也解决不了 | | 读已提交 | 允许当前事务读取其他事务已经提交的修改 | 脏读 | | 可重复读 | 当前事务执行时锁定当前记录,不允许其他事务操作 | 脏读、不可重复读 | | 串行化 | 当前事务执行时锁定当前表,不允许其他事务操作 | 脏读、不可重复读、幻读 |

6.2 隔离级别、传播行为对应事务之间的关系

隔离级别、传播行为对应事务之间的关系.png

6.3 测试的准备工作

6.3.1 思路

事务属性:事务的隔离级别之测试的准备工作的思路.png

6.3.2 EmpService 参与测试的方法

  • EmpService.java
package com.github.fairy.era.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:45
 */
public interface EmpService {

    /**
     * 根据id查询姓名
     *
     * @param empId
     * @return
     */
    String getEmpName(Integer empId);

    /**
     * 根据id更新姓名
     *
     * @param empId
     * @param name
     */
    void updateEmpName(Integer empId, String name);
}
  • EmpServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Override
    public String getEmpName(Integer empId) {
        return empDao.selectEmpNameById(empId);
    }

    @Transactional
    @Override
    public void updateEmpName(Integer empId, String name) {
        empDao.updateEmpNameById(empId, name);
    }
}

6.3.3 Junit 中执行测试的代码

package com.github.fairy.era.bean;

import com.github.fairy.era.service.EmpService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class SpringTest {

    @Autowired
    private EmpService empService;

    @Test
    public void testTxReadOnly() {
        String empName = empService.getEmpName(2);
        System.out.println("empName = " + empName);

    }

    @Test
    public void testIsolation() {
        Integer empId = 2;
        String empName = "aaaaaaaa";
        empService.updateEmpName(empId, empName);

    }
}

6.3.4 搞破坏

  • 为了让事务B(执行修改操作的事务)能够回滚,在 EmpDaoImpl 中的对应方法中人为抛出异常。

  • EmpDaoImpl.java

package com.github.fairy.era.dao.impl;

import com.github.fairy.era.dao.EmpDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:23
 */
@Repository
public class EmpDaoImpl implements EmpDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void updateEmpNameById(Integer empId, String empName) {
        jdbcTemplate.update(" UPDATE emp SET emp_name = ? WHERE emp_id = ? ", empName, empId);
        int i = 10 / 0;
    }

    @Override
    public void updateEmpSalaryById(Integer empId, double empSalary) {
        jdbcTemplate.update(" UPDATE emp SET emp_salary = ? WHERE emp_id = ? ", empSalary, empId);
    }

    @Override
    public String selectEmpNameById(Integer empId) {
        return jdbcTemplate.queryForObject(" SELECT emp_name FROM emp WHERE emp_id = ? ", String.class, empId);
    }
}

6.4 执行测试

  • 在 @Transactional 注解中使用 isolation 属性设置事务的隔离级别,取值使用 org.springframework.transaction.annotation.Isolation 枚举类提供的数值。

6.4.1 测试读未提交

  • 修改事务的隔离级别:
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Transactional(isolation = Isolation.READ_UNCOMMITTED,readOnly = true)
    @Override
    public String getEmpName(Integer empId) {
        return empDao.selectEmpNameById(empId);
    }

    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    @Override
    public void updateEmpName(Integer empId, String name) {
        empDao.updateEmpNameById(empId, name);
    }
}

测试读未提交.png

  • 测试结果:执行查询操作的事务读取了另一个尚未提交的修改。

6.4.2 测试读已提交

  • 修改事务的隔离级别:
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Transactional(isolation = Isolation.READ_COMMITTED, readOnly = true)
    @Override
    public String getEmpName(Integer empId) {
        return empDao.selectEmpNameById(empId);
    }

    @Transactional(isolation = Isolation.READ_COMMITTED)
    @Override
    public void updateEmpName(Integer empId, String name) {
        empDao.updateEmpNameById(empId, name);
    }
}
  • 测试结果:执行查询操作的事务读取的是数据库中正确的数据。

第七章:事务属性:事务的传播行为(⭐)

7.1 事务的传播行为要研究的问题

  • 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

事务传播行为要研究的问题.png

7.2 propagation 属性

7.2.1 默认值

  • @Transactional 注解通过 propagation 属性设置事务的传播行为。它的默认值是:
Propagation propagation() default Propagation.REQUIRED;

7.2.2 可选值说明

  • propagation 属性的可选值由 org.springframework.transaction.annotation.Propagation 枚举类提供: | 名称 | 含义 | | —- | —- | | REQUIRED 默认值 | 当前方法必须工作在事务中。
    如果当前线程上有已经开启的事务可用,那么就在这个事务中运行 。
    如果当前线程上没有已经开启的事务,那么就自己开启新事务,在新事务中运行。
    所以当前方法有可能和其他方法共用事务。
    在共用事务的情况下:当前方法会因为其他方法回滚而受 连累 。 | | REQUIRES_NEW
    建议使用 | 当前方法必须工作在事务中,不管当前线程上是否有已经开启的事务,都要开启新事务 ,在新事务中运行。
    不会和其他方法共用事务, 避免 被其他方法 连累 。 |

7.3 测试

7.3.1 测试 REQUIRED 模式

  • EmpService.java
package com.github.fairy.era.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:45
 */
public interface EmpService {

    void updateEmpNameInner(Integer empId, String empName);

    void updateEmpSalaryInner(Integer empId, Double empSalary);
}
  • EmpServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public void updateEmpNameInner(Integer empId, String empName) {

        empDao.updateEmpNameById(empId, empName);
    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public void updateEmpSalaryInner(Integer empId, Double empSalary) {
        // 模拟异常
        int i = 10 / 0;
        empDao.updateEmpSalaryById(empId, empSalary);
    }
}
  • TopService.java
package com.github.fairy.era.service;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-10 13:46
 */
public interface TopService {

    void topTxMethod();
}
  • TopServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.service.EmpService;
import com.github.fairy.era.service.TopService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-10 13:46
 */
@Service
public class TopServiceImpl implements TopService {

    // 这里只是为了测试事务的传播行为,临时在Service中装配了另一个Service,实际开发中非常不建议这么做,因为这样做会严重破坏项目的结构
    @Autowired
    private EmpService empService;

    @Transactional
    @Override
    public void topTxMethod() {
        empService.updateEmpNameInner(1, "aa");

        empService.updateEmpSalaryInner(2, 6000.34);
    }
}
  • 测试:
package com.github.fairy.era.bean;

import com.github.fairy.era.service.TopService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-05 11:02
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class SpringTest {

    @Autowired
    private TopService topService;

    @Test
    public void testPropagation() {
        // 调用外层方法
        topService.topTxMethod();
    }

}

测试REQUIRED模式的结果.png

  • 效果:内层方法 A 、内层方法 B 所做的修改都没有生效,总事务回滚了。

  • 执行流程:

REQUIRED模式的执行流程.png

7.3.1 测试 REQUIRED_NEW 模式

  • 修改 EmpServiceImpl.java
package com.github.fairy.era.service.impl;

import com.github.fairy.era.dao.EmpDao;
import com.github.fairy.era.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author 许大仙
 * @version 1.0
 * @since 2021-11-09 14:46
 */
@Transactional(readOnly = true)
@Service
public class EmpServiceImpl implements EmpService {

    @Autowired
    private EmpDao empDao;

    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateEmpNameInner(Integer empId, String empName) {

        empDao.updateEmpNameById(empId, empName);
    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
    public void updateEmpSalaryInner(Integer empId, Double empSalary) {
        // 模拟异常
        int i = 10 / 0;
        empDao.updateEmpSalaryById(empId, empSalary);
    }
}

测试REQUIRED_NEW模式的结果.png

  • 执行流程:

REQUIRED_NEW模式的执行流程.png

7.4 实际开发场景

7.4.1 Service 层的方法应用了通知

Service层的方法应用了通知.png

7.4.2 过滤器或拦截器等类似组件

过滤器或拦截器等类似组件.png

7.4.3 总结

  • 在事务传播行为这里,使用 REQUIRES_NEW 属性,也可以说是让不同事务方法从事务的使用上 解耦合 ,不要互相影响。

  • 例如:如果事务的传播行为使用了 REQUIRED 属性,那么当日志出现异常引起事务回滚,进而导致用户无法下订单,这是非常严重的事情。