Chapter5 事务操作
5.1 事务
5.1.1 事务简介
什么是事务
- 事务是数据库中的概念
- 事务是数据库操作最基本单元,逻辑上一组操作,要么都成功,如果有一个失败所有操作都失败
- 事务的四个特性:ACID(原子性、一致性、隔离性、持久性)
使用场景
- 当数据库操作涉及到多个表,或者是多个sql语句的insert,update,delete,需要保证这些语句都是成功才能完成我的功能,或者都失败,保证操作是符合要求的
jdbc,mybatis,hibernate各自的事务处理
- jdbc处理事务: Connection conn ; conn.commit(); conn.rollback();
- mybatis处理事务: SqlSession.commit(); SqlSession.rollback();
- hibernate处理事务: Session.commit(); Session.rollback();
问题
- 就是多种数据库的访问技术,有不同的事务处理的机制,对象,方法。
- spring提供一种处理事务的统一模型, 能使用统一步骤,方式完成多种不同数据库访问技术的事务处理
5.1.2 Spring事务管理
- 说明
- 事务原本是数据库中的概念,在 Dao 层。但spring中,一般将事务提升到业务层,即 Service 层,这样做是为了能够使用事务的特性来管理具体的业务。
- spring处理事务的模型,使用的步骤都是固定的。只需把事务使用的信息提供给spring
- spring事务操作方式
- 编程式事务管理:手动编写程序,较为繁琐一般不用
- 声明式事务管理,使用注解或xml配置文件(常用),底层使用AOP原理
5.1.3 环境搭建
创建数据库表
- 新建数据库spring5和表t_account
CREATE DATABASE `spring5` CHARACTER SET utf8; USE spring5; DROP TABLE IF EXISTS t_account; ( id VARCHAR(20) NOT NULL , # 主键 username VARCHAR(50) , # 姓名 money INT, # 存款 PRIMARY KEY (id) # 设置主键 );
- 新建MySQL用户并授权
CREATE USER 'Jacob' @'%' IDENTIFIED BY 'jacob12015229'; # 创建用户 GRANT ALL PRIVILEGES ON spring5.* TO 'Jacob'@'%'; # 授权
- 新建数据库spring5和表t_account
创建maven项目
- 新建空的Maven项目,JDK版本选择1.8
- pom.xml:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <!--单元测试--> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.7.0</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-test --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.2.5.RELEASE</version> <scope>test</scope> </dependency> <!--spring核心ioc--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!--做spring事务用到的--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.5.RELEASE</version> </dependency> <!--mybatis依赖--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.1</version> </dependency> <!--mybatis和spring集成的依赖--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> <!--mysql驱动--> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency> <!--阿里公司的数据库连接池--> <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.3</version> </dependency> </dependencies> <build> <!--目的是把src/main/java目录中的xml文件包含到输出结果中。输出到classes目录中--> <resources> <resource> <directory>src/main/java</directory><!--所在的目录--> <includes><!--包括目录下的.properties,.xml 文件都会扫描到--> <include>**/*.properties</include> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources> <!--指定jdk的版本--> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
创建类和相关接口
- 先看一下项目结构
src ├─main │ ├─java │ │ └─com │ │ └─atguigu │ │ ├─dao │ │ ├─domain │ │ └─service │ └─resources └─test └─java └─com └─atguigu
- dao下新建UserDao和mapper文件UserDao.xml
public interface UserDao { public void addMoney(@Param("name") String name, @Param("money") Integer money); public void reduceMoney(@Param("name") String name, @Param("money") Integer money); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--指定约束文件--> <mapper namespace="com.atguigu.dao.UserDao"> <update id="addMoney"> update t_account set money=money+#{money} where username=#{name} </update> <update id="reduceMoney"> update t_account set money=money-#{money} where username=#{name} </update> </mapper>
- service下新建UserService
@Service public class UserService { // 注入dao @Autowired private UserDao userDao; // 转账 public void accountMoney(String n1, String n2, Integer money) { userDao.addMoney(n1,money); userDao.reduceMoney(n2,money); } }
- 先看一下项目结构
配置spring和mybatis
- resources下新建applicationContext.xml文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 组件扫描 --> <context:component-scan base-package="com.atguigu"></context:component-scan> <context:property-placeholder location="classpath:jdbc.properties" /> <!--声明数据源DataSource, 作用是连接数据库的--> <bean id="myDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!--set注入给DruidDataSource提供连接数据库信息 --> <!-- 使用属性配置文件中的数据,语法 ${key} --> <property name="url" value="${jdbc.url}" /><!--setUrl()--> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}" /> <property name="maxActive" value="${jdbc.max}" /> </bean> <!--mybatis--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--set注入,把数据库连接池付给了dataSource属性--> <property name="dataSource" ref="myDataSource" /> <property name="configLocation" value="classpath:mybatis.xml" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--指定SqlSessionFactory对象的id--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> <property name="basePackage" value="com.atguigu.dao"/> </bean> </beans>
- resources下新建mybatis.xml文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--设置别名--> <typeAliases> <package name="com.atguigu.domain"/> </typeAliases> <!-- sql mapper(sql映射文件)的位置--> <mappers> <package name="com.atguigu.dao"/> </mappers> </configuration>
- resources下新建jdbc.properties文件(这里用的是mysql8.0)
jdbc.url=jdbc:mysql://localhost:3306/spring5?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=Jacob jdbc.password=jacob12015229 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.max=30
- resources下新建applicationContext.xml文件
测试
- 使用spring5+junit5的方式
- 在test/java/com/atguigu下新建测试类:
package com.atguigu; import com.atguigu.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @SpringJUnitConfig(locations = "classpath:applicationContext.xml") public class TestAccount { @Autowired private UserService userService; @Test public void testAccount(){ userService.accountMoney("rui","mei",100); } }
模拟编程式的事务处理
- 可以在UserService类中手动进行事务处理
- 代码:
// 事务操作 public void accountMoney(String n1, String n2, Integer money) { try { // 1. 开启事务 // 2. 进行事务操作 userDao.addMoney(n1,money); // 模拟异常 int i = money/0; userDao.reduceMoney(n2,money); // 3. 无异常,提交事务 } catch (Exception e) { // 4. 有异常,事务回滚 System.out.println("rollback"); }
5.2 事务管理API
5.2.1 事务管理器接口
Spring提供
PlatformTransactionManager
接口,代表事务管理器,这个接口针对不同的框架提供不同的实现类常用的实现类
DataSourceTransactionManager
:使用 JDBC 或 MyBatis 进行数据库操作时使用HibernateTransactionManager
:使用 Hibernate 进行持久化数据时使用。
spring回滚方式
- Spring 事务的默认回滚方式是: 发生运行时异常和 error 时回滚,发生受查(编译)异常时提交。
- 对于受查异常,程序员也可以手工设置其回滚方式。
java中的错误与异常
Throwable 类是 Java 语言中所有错误或异常的超类。只有当对象是此类(或其子类之一)的实例时, 才能通过 Java 虚拟机或者 Java 的 throw 语句抛出。
Error 是程序在运行过程中出现的无法处理的错误,比如 OutOfMemoryError、ThreadDeath、 NoSuchMethodError 等。当这些错误发生时,程序是无法处理(捕获或抛出)的, JVM 一般会终止线程
程序在编译和运行时出现的另一类错误称之为异常,分为运行时异常与受查异常
运行时异常,是 RuntimeException 类或其子类, 即只有在运行时才出现的异常。如,NullPointerException、 ArrayIndexOutOfBoundsException、 IllegalArgumentException 等均属于运行时异常。这些异常由 JVM 抛出,在编译时不要求必须处理(捕获或抛出)
受查异常,也叫编译时异常,即在代码编写时要求必须捕获或抛出的异常,若不处理,则无法通过编译。如 SQLException, ClassNotFoundException, IOException
RuntimeException 及其子类以外的异常,均属于受查异常。当然,用户自定义的 Exception的子类,即用户自定义的异常也属受查异常,所以自己手动创建抛出的异常不会使spring事务回滚
5.2.2 事务定义接口
TransactionDefinition
接口- 事务定义接口
TransactionDefinition
中定义了事务描述相关的三类常量:事务隔离级别、事务传播行为、事务默认超时时限,及对它们的操作 - 这些常量用于在
Transactional
中设置事务的属性
- 事务定义接口
多事务操作的问题
- 脏读:一个未提交事务读取到另一个未提交事务的数据(第二个事务回滚则会脏读)
- 不可重复读:一个未提交事务读取到另一提交事务修改数据
- 虚读/幻读:一个未提交事务读取到另一提交事务添加数据
隔离级别常量
DEFAULT: 采用 DB 默认的事务隔离级别。 MySql 的默认为 REPEATABLE_READ; Oracle默认为 READ_COMMITTED。
READ_UNCOMMITTED: 读未提交。未解决任何并发问题。
READ_COMMITTED: 读已提交。解决脏读,存在不可重复读与幻读。
REPEATABLE_READ: 可重复读。解决脏读、不可重复读,存在幻读
SERIALIZABLE: 串行化。不存在并发问题。
脏读 不可重复读 幻读 READ_UNCOMMITTED 有 有 有 READ_COMMITTED 无 有 有 REPEATABLE_READ 无 无 有 SERIALIZABLE 无 无 无
事务传播行为常量
PROPAGATION_REQUIRED
:指定的方法必须在事务内执行。若当前存在事务,就加入到当前事务中;若当前没有事务,则创建一个新事务。PROPAGATION_REQUIRES_NEW
:总是新建一个事务,若当前存在事务,就将当前事务挂起,直到新事务执行完毕。PROPAGATION_SUPPORTS
:指定的方法支持当前事务,但若当前没有事务,也可以以非事务方式执行。PROPAGATION_MANDATORY
PROPAGATION_NESTED
PROPAGATION_NEVER
PROPAGATION_NOT_SUPPORTED
默认事务超时时限
- 常量
TIMEOUT_DEFAULT
定义了事务底层默认的超时时限, sql 语句的执行时长。 - 事务的超时时限起作用的条件比较多,且超时的时间计算点较复杂,一般使用默认值即可
- 常量
5.3 使用注解的事务管理
5.3.1 @Transactional
注解
- 通过
@Transactional
注解方式, 可将事务织入到相应 public 方法中,实现事务管理。 @Transactional
注解可以加到service类上,表示类中所有方法都添加事务,也可以添加到具体的方法上- 属性
propagation
: 用于设置事务传播属性。该属性类型为Propagation
枚举,默认值为Propagation.REQUIRED
isolation
: 用于设置事务的隔离级别。该属性类型为Isolation
枚举,默认值为Isolation.DEFAULT
readOnly
: 用于设置该方法对数据库的操作是否是只读的。该属性为boolean
,默认值为false
。timeout
: 用于设置本操作与数据库连接的超时时限。单位为秒,类型为 int,默认值为-1,即没有时限。rollbackFor
: 指定需要回滚的异常类。类型为Class[]
,默认值为空数组。当然,若只有一个异常类时,可以不使用数组rollbackForClassName
: 指定需要回滚的异常类类名。类型为String[]
,默认值为空数组。当然,若只有一个异常类时,可以不使用数组。noRollbackFor
: 指定不需要回滚的异常类。类型为Class[]
,默认值为空数组。当然,若只有一个异常类时,可以不使用数组。noRollbackForClassName
: 指定不需要回滚的异常类类名。类型为String[]
,默认值为空数组。当然,若只有一个异常类时,可以不使用数组5.3.2 使用步骤
- 在applicationContext.xml文件中配置spring事务管理器
<!--使用spring的事务处理器--> <!--声明事务处理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--连接的数据库,指定数据源--> <property name="dataSource" ref="myDataSource"/> </bean>
- 在中配置文件,开启事务注解
<!--开启事务注解驱动--> <tx:annotation-driven transaction-manager="transactionManager"/>
- 在service类或其方法上添加事务注解
@Transactional
@Service @Transactional public class UserService { ...
- 测试
- 在service中添加异常
// 转账 public void accountMoney(String n1, String n2, Integer money) { userDao.addMoney(n1,money); int i = money/0; userDao.reduceMoney(n2,money); }
- 执行测试方法,刷新数据库表会发现两人的存款金额位发生变化。
- 在service中添加异常
5.4 使用配置文件的事务管理
5.4.1 说明
- 缺点:每个目标类都需要配置事务代理。当目标类较多,配置文件会变得非常臃肿
- 需要引入AspectJ的依赖:
<!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>5.3.2</version> </dependency>
5.4.2 步骤
- 修改配置文件applicationContext.xml
<!--使用spring的事务处理器--> <!--声明事务处理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--连接的数据库,指定数据源--> <property name="dataSource" ref="myDataSource"/> </bean> <!--配置通知--> <tx:advice id="txadvice" transaction-manager="transactionManager"> <!--配置事务参数--> <tx:attributes> <!--指定在哪种规则的方法上添加事务--> <tx:method name="accountMoney" propagation="REQUIRED"/> <!--<tx:method name="account*"/><!–表示给所有account开头的方法添加事务–>--> </tx:attributes> </tx:advice> <!--配置切入点和切面--> <aop:config> <!--配置切入点--> <aop:pointcut id="pt" expression="execution(* com.atguigu.service.UserService.*(..))"/> <aop:advisor advice-ref="txadvice" pointcut-ref="pt"/> </aop:config>
- 删除掉service中的
Transactional
注解@Service // @Transactional public class UserService { // 注入dao @Autowired private UserDao userDao; // 转账 public void accountMoney(String n1, String n2, Integer money) { userDao.addMoney(n1,money); int i = money/0; userDao.reduceMoney(n2,money); } }
- 测试
@SpringJUnitConfig(locations = "classpath:applicationContext.xml") public class TestAccount { @Autowired private UserService userService; @Test public void testAccount() { userService.accountMoney("rui","mei",100); } }
- 注意事项
- 在基于xml的声明式事务中,配置文件中事务属性的
tx:method
是必须配置的,如果没有配置则事务对这个方法不生效(即使配置了切入点表达式) tx:method
中可以通过rollback-for
属性配置回滚的异常,默认运行时异常,建议编译时异常+运行时异常
- 在基于xml的声明式事务中,配置文件中事务属性的
5.5 完全注解开发
5.5.1 说明
- 完全注解开发是指无需spring、mybatis配置文件,完全使用注解来开发
- 需要新建一个配置类,代替配置文件,并删除原来的xml配置文件
5.5.2 步骤
- 新建配置类TxConfig(com.atguigu.config包下)
@Configuration //配置类 @ComponentScan(basePackages = "com.atguigu") // 组件扫描 @EnableTransactionManagement //开启事务 @PropertySource("classpath:jdbc.properties") // 读取数据库连接信息 @MapperScan("com.atguigu.dao") // 扫描mapper映射文件 public class TxConfig { // 1. 读取连接连接信息 @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Value("${jdbc.max}") private String maxActive; @Value("${jdbc.driver}") private String driverClassName; // 2. 创建数据库连接池 @Bean public DruidDataSource getDruidDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setMaxActive(Integer.parseInt(maxActive)); dataSource.setDriverClassName(driverClassName); System.out.println("数据库连接池"); return dataSource ; } // 3. 创建mybatis的SqlSessionFactoryBean对象 @Bean public SqlSessionFactoryBean getSqlSessionFactoryBean(DataSource dataSource) throws IOException { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setTypeAliasesPackage("com.atguigu.domain"); // 设置别名 return sqlSessionFactoryBean; } // 4. 创建事务管理器 @Bean public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; } }
- 测试
@SpringJUnitConfig(TxConfig.class) public class TestAccount { @Autowired private UserService userService; @Test public void testAccount() { userService.accountMoney("rui","mei",100); } }