⭐表示重要。
第一章:准备工作
1.1 环境搭建
- IDEA 2021+。
- JDK 11。
- Maven 3.8。
- MySQL 5.7。
1.2 导入依赖
- pom.xml
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.12</version>
</dependency>
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.12</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
</dependency>
<!-- junit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
1.3 数据库属性文件
- db.properties
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
jdbc.driverClass=com.mysql.cj.jdbc.Driver
jdbc.username=root
jdbc.password=123456
1.4 SQL 脚本
CREATE DATABASE IF NOT EXISTS `test` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE `test`
DROP TABLE IF EXISTS `emp`;
CREATE TABLE `emp` (
`emp_id` int(11) NOT NULL AUTO_INCREMENT,
`emp_name` varchar(255) DEFAULT NULL,
`emp_salary` double DEFAULT NULL,
PRIMARY KEY (`emp_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `emp` VALUES (1, '张三', 5000);
INSERT INTO `emp` VALUES (2, '李四', 6000);
INSERT INTO `emp` VALUES (3, '王五', 7000);
1.5 实体类
- Emp.java
package com.github.fairy.era.bean;
import java.io.Serializable;
/**
* @author 许大仙
* @version 1.0
* @since 2021-11-09 14:19
*/
public class Emp implements Serializable {
private Integer empId;
private String empName;
private Double empSalary;
public Emp() {
}
public Emp(Integer empId, String empName, Double empSalary) {
this.empId = empId;
this.empName = empName;
this.empSalary = empSalary;
}
public Integer getEmpId() {
return empId;
}
public void setEmpId(Integer empId) {
this.empId = empId;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public Double getEmpSalary() {
return empSalary;
}
public void setEmpSalary(Double empSalary) {
this.empSalary = empSalary;
}
@Override
public String toString() {
return "Emp{" +
"empId=" + empId +
", empName='" + empName + '\'' +
", empSalary='" + empSalary + '\'' +
'}';
}
}
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)
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)
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);
}
}
- 示例:编译时异常不回滚
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);
}
}
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 隔离级别、传播行为对应事务之间的关系
6.3 测试的准备工作
6.3.1 思路
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);
}
}
- 测试结果:执行查询操作的事务读取了另一个尚未提交的修改。
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 事务的传播行为要研究的问题
- 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
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();
}
}
效果:内层方法 A 、内层方法 B 所做的修改都没有生效,总事务回滚了。
执行流程:
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);
}
}
- 执行流程:
7.4 实际开发场景
7.4.1 Service 层的方法应用了通知
7.4.2 过滤器或拦截器等类似组件
7.4.3 总结
在事务传播行为这里,使用
REQUIRES_NEW
属性,也可以说是让不同事务方法从事务的使用上解耦合
,不要互相影响。例如:如果事务的传播行为使用了
REQUIRED
属性,那么当日志出现异常引起事务回滚,进而导致用户无法下订单,这是非常严重的事情。