事物:在一个业务流程中,通常需要多条DML(insert delete update)语句共同联合才能完成的,为了保证数据的安全,多条DML语句都必须同时成功,,或同时失败。
事物的四个处理过程:开启事务、执行核心业务代码、提交事务、回滚事务
事务的四个特性:
原子性:事务是最小工作单位,不可再分割
一致性:事务要么同时成功,要么同时失败。事务前和事务后的总量不变
隔离性:事务和事务之间有隔离性,互不干扰
持久性:持久性是事务结束的标志
Spring实现事务的两种方式
编程式事务:编写代码来实现事务的管理
声明式事务*:基于注解的方式、基于xml配置方式
以银行账户转账为案例
数据库表
spring6整合mybatis
pom.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring6-transaction-bank</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- 打包方式jar-->
<packaging>jar</packaging>
<!--配置多个仓库 -->
<repositories>
<!-- Spring6 -->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!-- 依赖-->
<dependencies>
<!-- @Resource注入注解-->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.0</version>
</dependency>
<!-- 引入Spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--引入Spring-jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--引入mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<!--引入mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<!--引入mybatis-spring依赖-->
<!-- -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.1</version>
</dependency>
<!-- 引入德鲁伊连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
<!-- Spring框架对Junit支持的依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.0-M2</version>
<!--6.0.0-M2既支持junit4又支持junit5-->
</dependency>
<!--junit依赖-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<!--所在的目录-->
<directory>src/main/java</directory>
<includes>
<!--包括目录下的.properties .xml文件都会扫描到-->
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
jdbc.properties文件
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://IP:3306/mysql
jdbc.user= root
jdbc.password= xxxxxx
mybatis-config.xml配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- logImpl设置mybatis输出日志-->
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<mappers>
<package name="com.qgs.dao"/>
</mappers>
</configuration>
SpringConfig.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 http://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.qgs"></context:component-scan>
<!--引入外部文件-->
<context:property-placeholder location="jdbc.properties"/>
<!--声明数据来源-->
<bean class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!--配置SqlSessionFactoryBean-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据源-->
<property name="dataSource" ref="dateSource"/>
<!--指定Mybatis-config.xml主配置文件-->
<property name="configLocation" value="mybatis-config.xml"/>
<!--注入别名-->
<property name="typeAliasesPackage" value="com.qgs.pojo"/>
</bean>
<!--扫描mapper配置器 SqlSession.Mapper()-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="factory"></property>
<property name="basePackage" value="com.qgs.dao"/>
</bean>
<!--事务管理器-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dateSource"/>
</bean>
<!--启动事务注解-->
<tx:annotation-driven transaction-manager="TransactionManager"/>
</beans>
BankDao接口
public interface BankDao {
/**
* 根据账号查询账户信息
*/
List<Bank> selectAll();
/**
* 根据actno查询账户信息
* @param actno
* @return
*/
Bank selectByActno(String actno);
/**
* 更新账户信息
* @param act
* @return
*/
int update(Bank act);
}
BankDao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qgs.dao.BankDao">
<select resultType="com.qgs.pojo.Bank">
select * from bank
</select>
<select resultType="com.qgs.pojo.Bank">
select actno,balance from bank where actno = #{actno}
</select>
<update >
update bank set balance=#{balance} where actno =#{actno}
</update>
</mapper>
pojo实体类
package com.qgs.pojo;
/**
* @author QGS
* @version 1.0.0
* @date 2023年03月24日 16:36:58
* @packageName com.qgs.pojo
* @className Bank
* @describe TODO
*/
public class Bank {
private String actno;
private Double balance;
public Bank() {
}
public Bank(String actno, Double balance) {
this.actno = actno;
this.balance = balance;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
@Override
public String toString() {
return "Bank{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
}
BankService 接口
public interface BankService {
/**
* 转账方法
* @param fromActno 转出账户
* @param toActno 转入账户
* @param money 金额
*/
void transfer(String fromActno ,String toActno ,double money);
}
BankService 接口实现类
package com.qgs.service;
import com.qgs.dao.BankDao;
import com.qgs.pojo.Bank;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author QGS
* @version 1.0.0
* @date 2023年03月25日 11:15:24
* @packageName com.qgs.service
* @className BankServiceImpl
* @describe TODO
*/
@Service("bankServiceImpl")
//@Transactional
public class BankServiceImpl implements BankService{
@Resource(name ="bankDao")
private BankDao bankDao;
@Override
//@Transactional默认propagation = Propagation.REQUIRED
@Transactional(propagation = Propagation.REQUIRED)
public void transfer(String fromActno, String toActno, double money) {
//查询转出账户余额
Bank fromBank = bankDao.selectByActno(fromActno);
if (fromBank.getBalance() <money){
throw new RuntimeException("余额不足");
}
//查询转入账户余额
Bank toBank = bankDao.selectByActno(toActno);
//将内存中转出与转入账户余额修改
fromBank.setBalance(fromBank.getBalance() - money);
toBank.setBalance(toBank.getBalance() + money);
//更新数据库
int count = bankDao.update(fromBank);
int count2 = bankDao.update(toBank);
count +=count2;
if (count !=2){
//回滚事务,转账失败
throw new RuntimeException("失败");
}
}
}
@Test
public void BankTest(){
ApplicationContext applicationContext =new ClassPathXmlApplicationContext("SpringConfig.xml");
BankService bankService = applicationContext.getBean("bankServiceImpl", BankService.class);
try {
bankService.transfer("act01","act2",300);
}catch (Exception e){
e.printStackTrace();
}
}
事务的传播行为
事务传播行为在spring框架中被定义为枚举类型。
事务的传播行为有7种:
REQUIRED:支持当前事务,如果不存在就新建一个(默认模式)
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。
MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常。
REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起。
NOT_SUPPORTED:以非事务方式运行,如果事务存在,挂起当前事务。
NEVER:以非事务方式运行,如果有事务存在,抛出异常。
NESTED:如果当前争优一个事务在进行中,则该方法应当运行一个嵌套事务中。被嵌套的事务可以单独于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED。
数据库中读取数据存在的三大问题
脏读:读取到没有提交的数据库的数据。
不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。
幻读:读到的数据是假的。(事务并发,一定存在幻读)
spring事务隔离级别
数据库中读取数据存在的三大读问题
脏读:读取到没有提交的数据库的数据。
不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。
幻读:读到的数据是假的。(事务并发,一定存在幻读)
事务隔离级别的四个级别:
读未提交:READ_UNCOMMITTEN
这种隔离级别:存在脏读问题,所谓的脏读(dirty read)表示能够读取到其他事务未提交的数据。
读提交:READ_COMMITTED (oracle)
解决了脏读问题,其他事务提交之后才能读到,但存在不可重复读问题
可重复读:REPEATABLE_READ (MYSQL)
解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据移植都是一样的。但存在幻读问题。
序列化:SERIALLZABLE
解决了幻读问题,事务排毒执行。不支持并发。
Spring事务超时
@Transactional(timeout = 10) 表示设置事务的超时时间为10秒。
超过10秒如果该事务所有的DML语句还没有执行完毕,最终选择回滚。
在当前事务中,最后一条DML语句执行之前的时间。最后一条DML语句后面还有很多业务逻辑(非DML语句),这些业务代码执行的时间不被记入超时时间
默认值为:timeout = -1 、表示没有时间限制。
Spring启动只读事务
@Transactional(readOnly = true) 作用:启动Spring优化策略,提高select语句执行效率
Spring事务设置异常回滚问题
设置RuntimeException异常时回滚
@Transactional(rollbackFor = RuntimeException.class)
设置NullPointerException异常不回滚
@Transactional(noRollbackFor = NullPointerException.class)