目录
一、事务概述
1、什么是事务
2、事务的四个特性(ACID)
二、搭建事务操作环境
1、dao、service 两层结构
2、示例
3、模拟异常(事务场景引入)
三、Spring 事务管理
1、事务管理介绍
2、声明式事务管理——注解方式
3、声明式事务管理——注解的参数配置
4、声明式事务管理——XML方式
5、声明式事务管理——完全注解开发
一、事务概述
1、什么是事务
(1)事务是数据库操作最基本单元:逻辑上对于一组操作,要么都成功,如果有一个失败所有操作都失败。
2、事务的四个特性(ACID)
(1)原子性
- 要么都成功,要么全失败
(2)一致性
- 事务只能把数据库从一个有效(正确)的状态“转移”到另一个有效(正确)的状态
(3)隔离性
- 多事务操作时,事务之间不会相互影响
(4)持久性
- 即使发生了异常情况,数据库中的数据也应该能够被恢复,并且不会丢失或损坏
二、搭建事务操作环境
通过模拟银行转账这一例子,来演示在 Spring5 中如何进行事务操作
1、dao、service 两层结构
JavaEE 中有三层结构,分别是:web、service、dao。其中数据库操作由 dao 实现,业务逻辑由 service 实现,因此我们主要关注这两层结构。
收入和支出的方法都执行后,在构成转账的方法,才是一个完整操作。
2、示例
(1)创建数据库、表,添加记录
(2)创建 UserDao 和 UserService,并完成注入
(3)在 dao 中创建两个方法:收入和支出
package com.demo.dao.impl;
import com.demo.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
@Repository
public class UserDaoImpl implements UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int income(String username, BigDecimal dif) {
String sql = "update \"BankUser\" set balance=balance+? where username=?";
return jdbcTemplate.update(sql, dif, username);
}
@Override
public int expense(String username, BigDecimal dif) {
String sql = "update \"BankUser\" set balance=balance-? where username=?";
return jdbcTemplate.update(sql, dif, username);
}
}
(4)在 service 中创建方法:转账
package com.demo.service.impl;
import com.demo.dao.UserDao;
import com.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public void remittance(String name1, String name2, BigDecimal dif) {
// name1 减少 dif
userDao.expense(name1, dif);
// name2 增加 dif
userDao.income(name2, dif);
}
}
(5)测试代码
import com.demo.pojo.User;
import com.demo.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.math.BigDecimal;
public class remittanceTest {
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("JdbcTemplate.xml");
UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.remittance("wyt", "gyt", new BigDecimal(25));
}
}
(6)运行结果
3、模拟异常(事务场景引入)
假设在转出一方转出后,出现了异常,那么按照事务的特性,就应当将操作回滚。
而在上面的代码中,就会使得转出一方出了钱,而接收一方没有收到钱。
三、Spring 事务管理
1、事务管理介绍
(1)由于异常一般出现在业务逻辑处理,所以一般将事务管理放在 service 层
(2)Spring 事务管理有两种方式:
- 编程式事务管理(一般不用),前面的模拟异常就是使用编程式;(编程式事务管理会造成代码冗余,一定要用编程式的话,可以使用 filter 来全局捕获异常)
- 声明式事务管理(一般使用);
(3)声明式事务管理有两种实现方式:
- 基于 xml 配置文件方式;(一般不用)
- 基于注解方式;(一般使用)
(4)在 Spring 进行声明式事务管理时,需要使用 AOP 原理
(5)Spring 事务管理 API
- 提供接口:PlatformTransactionManager,代表事务管理器,这个接口针对不同的框架提供不同的实现类
2、声明式事务管理——注解方式
(1)在 spring 配置文件中配置事务管理器
- 事务管理器同样使用 set 方法赋值 dataSource,因此也需要使用 <property>。
<!-- 创建事务管理器的对象 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
(2)在 spring 配置文件中开启事务注解
- 在配置文件中,引入名称空间 tx;
xmlns:tx="http://www.springframework.org/schema/tx"
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
- 开启事务注解,指定我们配置的事务管理器;
<!-- 开启事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
(3)为 service 类或 service 类方法添加事务注解 @Transactional
- 如果把 @Transactional 添加类上面,这个类里面所有的方法都添加事务;
- 如果把 @Transactional 添加类方法上面,仅为这个方法添加事务;
(4)输出结果
- 测试代码和前面示例的一样,而此次没有出现转出人少钱、接收人没有多钱的情况了,说明事务回滚成功。
3、声明式事务管理——注解的参数配置
在声明式的事务处理中,要配置一个切面,其中就用到了 propagation 等多个参数。需要明确的是,他们本质上都是对类方法被调用时的一种描述。
(1)propagation 事务传播行为
propagation 表示这些方法怎么使用事务,是用还是不用。其常用属性有 3 种:
- REQUIRED:支持(加入)当前事务,如果当前没有事务,就新建一个事务。(默认、常用)
- REQUIRED_NEW:必须新建事务,如果当前存在事务,把当前事务挂起。
- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
@Transactional(propagation = Propagation.REQUIRED)
(2)isolation 事务隔离级别
多事务的并发操作容易引发读问题,事务的隔离级别会产生不同的读问题:脏读、不可重复读、幻读。
(2-1)名词解释
- 脏读(脏数据):事务 A 修改了数据 D,但还未提交,就被事务 B 读取了,如果此时 A 进行了回滚,那么 B 读到的数据就是脏数据。依据脏数据所做的操作很可能不正确。
- 不可重复读:事务 A 需要多次读取数据 D,如果在 A 下一次读取 D 之前,事务 B 修改了 D 并提交,那么 A 前后读取到的数据就不一致了。此情况称为不可重复读。
- 幻觉读:一个未提交的事务 A,读取到了另一个已经提交的事务 B 所添加的数据 D。
(2-2)设置事务隔离级别解决读问题
- 默认一般是:REPEATABLE READ,可重复读。
(2-3)示例
@Transactional(isolation = Isolation.SERIALIZABLE)
(3)timeout 超时时间
- 事务需要在一个规定时间内完成提交,如果超时则会回滚。默认值是 -1 ,设置时间以秒单位进行计算。
(4)readOnly 是否只读
- 读:查询操作,写:添加修改删除操作;
- readOnly 默认值 false,表示可以查询,也可以进行添加修改删除操作;
- 设置 readOnly 值是 true 之后,只能查询;
(5)rollbackFor 回滚
- 设置出现哪些异常,则进行回滚
(6)noRollbackFor 不回滚
- 设置出现哪些异常,不进行回滚
4、声明式事务管理——XML方式
(1)配置事务管理器
与注解方式配置事务管理器一致。
<!-- 创建事务管理器的对象 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
(2)配置通知
在 AOP 中讲过,通知就是增强的逻辑部分,加上源代码部分,构成动态代理。
<!-- 配置通知,事务就是增强部分,就是通知 -->
<tx:advice id="txAdvice">
<!-- 配置事务参数 -->
<tx:attributes>
<!-- 将事务添加在符合规则的方法上 -->
<tx:method name="remittance" isolation="REPEATABLE_READ"/> <!-- 还可以设置隔离级别等属性 -->
<tx:method name="remit*"/> <!-- 以 remit 开头的方法,都符合规则 -->
</tx:attributes>
</tx:advice>
(3)配置切入点和切面
把事务加到哪些类、哪些方法。作用与注解方式的 @Transactional 一致。
<!-- 配置切入点和切面 -->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="pc" expression="execution(* com.demo.service.impl.UserServiceImpl.*(..))"/>
<!-- 配置切面 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pc"/>
</aop:config>
5、声明式事务管理——完全注解开发(详细)
前面的注解方式中,还是有一部分需要编写配置文件。
(1)创建数据库配置文件
prop.driverClassName = org.postgresql.Driver
prop.url = jdbc:postgresql://localhost:5432/MyDatabase
prop.username = postgres
prop.password = 123456
(2)创建 jdbc 配置类,替代 xml 配置文件中的数据库连接池
- @Bean:https://blog.csdn.net/lazy_zzzzzz/article/details/94464337
- @PropertySource:用于加载 Properties 属性配置文件,它需要指定配置文件所在的路径和文件名称。(classpath 详见:https://blog.csdn.net/y_chengbo/article/details/110118195)
- @Value:用于将基本数据类型的值注入到成员变量,它等价于 <property> 中的 value 属性。 在指定值时如果值是变量,则需要使用 ${变量名称} 的方式注入,如果不使用 ${} 的话则把字符赋值给变量
package com.demo.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.sql.Connection;
@PropertySource("classpath:dataSource.properties")
public class JdbcConfig {
@Value("${prop.driverClassName}")
private String driver;
@Value("${prop.url}")
private String url;
@Value("${prop.username}")
private String username;
@Value("${prop.password}")
private String password;
// 创建数据库连接池
@Bean
public DruidDataSource getDruidDataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}
// 创建 jdbcTemplate 对象
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
// 到 IOC 容器中,根据类型找到 dataSource
JdbcTemplate jdbcTemplate = new JdbcTemplate();
// 注入 dataSource
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
// 创建 connection 对象
@Bean // 写了 @Bean,那么就会到 IOC 容器中寻找对应对象
public Connection getConnection(@Autowired DataSource dataSource) {
Connection conn = null;
try {
conn = dataSource.getConnection();
} catch (Exception e) {
throw new RuntimeException(e);
}
return conn;
}
}
(3)创建 transaction 配置类,替代 xml 中的事务管理
- @EnableTransactionManagement:开启事务
package com.demo.config;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@EnableTransactionManagement // 开启事务
public class TransactionConfig {
// 创建事务管理器
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
(4)创建 Spring 配置类
- @Configuration: 表示当前的类是一个配置类,它就相当于 Spring 的 xml 配置文件
- @ComponentScan:注解扫描,用于扫描指定包下的哪些类添加了 @Component、@Service、@Repository、@Controller、@Autowired 等注解
- @Import: 导入别的配置类
package com.demo.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@ComponentScan({"com.demo"})
@Import({JdbcConfig.class})
public class SpringConfig {
}
(5)测试代码
import com.demo.config.transactionConfig;
import com.demo.service.UserService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.math.BigDecimal;
public class remittanceTest {
@Test
public void completeAnnotation() {
ApplicationContext context = new AnnotationConfigApplicationContext(transactionConfig.class);
UserService userService = context.getBean("userServiceImpl", UserService.class);
userService.remittance("wyt", "gyt", new BigDecimal(25));
}
}
(6)运行结果
原本两个 user 的 balance 都是 50,现在转账了 25:
若给转账功能手动引入一个异常,观察事务管理是否起作用:
显然遇到异常成功回滚,没有破坏一致性。