Spring学习笔记12 面向切面编程AOP-CSDN博客
什么是事务:在一个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全.
多条DML要么同时成功,要么同时失败,叫做事务(Transaction)
事务四个处理过程:
1.开启事务(start transaction)
2.执行核心业务代码
3.提交事务(如果核心业务处理过程中没有出现异常)(commit transaction)
4.回滚事务(如果核心业务处理过程中出现异常)(rollabck transaction)
事务的四个特性:
A.原子性:事务是最小的工作单元,不可再分
C.一致性:事务要求要么同时成功,要么同时失败.事务前和事务后的总量不变.
I.隔离性:事务和事务之间因为有隔离,才可以保证互不干扰
D.持久性:持久性是事务结束的标志.
引入事务场景:
以银行账户转账为例学习事务.两个账户act-01和act-02.
act-01向act-02转账10000.
一个账户减10000,一个账户加10000,必须同时成功,或者同时失败
连接数据库的技术采用Spring框架的JdbcTemplate
新建maven项目或者模块
依赖
<dependencies><!--spring依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.0.10</version></dependency><!--jdbcTemplate依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>6.0.10</version></dependency><!--mysql驱动依赖--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.30</version></dependency><!--druid德鲁伊依赖--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.13</version></dependency><!--javaee的注解 @Resource依赖--><dependency><groupId>jakarta.annotation</groupId><artifactId>jakarta.annotation-api</artifactId><version>2.1.1</version></dependency><!--单元测试依赖--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency> </dependencies>
准备数据库表
项目结构
实体类
package com.example.pojo;import java.util.Objects;/*** @author hrui* @date 2023/9/26 15:02*/
public class Account {private Integer id;private String actno;private Double balance;public Account() {}public Account(Integer id, String actno, Double balance) {this.id = id;this.actno = actno;this.balance = balance;}@Overridepublic String toString() {return "Account{" +"id=" + id +", actno='" + actno + '\'' +", balance=" + balance +'}';}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Account account = (Account) o;return Objects.equals(id, account.id) && Objects.equals(actno, account.actno) && Objects.equals(balance, account.balance);}@Overridepublic int hashCode() {return Objects.hash(id, actno, balance);}public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}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;}
}
持久层
package com.example.dao;import com.example.pojo.Account;/*** 专门负责账户信息的CRUD操作* DAO中只执行SQL语句,没有任何业务逻辑.* 也就是说DAO不和业务挂钩* @author hrui* @date 2023/9/26 15:00*/
public interface AccountDao {Account selectByActNo(Integer id);int updateAct(Account account);
}
持久层实现类
package com.example.dao.impl;import com.example.dao.AccountDao;
import com.example.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;/*** @author hrui* @date 2023/9/26 15:04*/
@Repository
public class AccountDaoImpl implements AccountDao {@Resource(name="jdbcTemplate")private JdbcTemplate jdbcTemplate;@Overridepublic Account selectByActNo(Integer id) {String sql="select id,actno,balance from t_act where id=?";Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), id);return account;}@Overridepublic int updateAct(Account account) {String sql="update t_act set balance=? where id=?";int count = jdbcTemplate.update(sql, account.getBalance(),account.getId());return count;}
}
业务层接口
package com.example.service;/*** @author hrui* @date 2023/9/26 15:55*/
public interface AccountService {void transfer(Integer fid,Integer tid,double balance);
}
业务层实现类
package com.example.service.impl;import com.example.dao.AccountDao;
import com.example.pojo.Account;
import com.example.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;/*** @author hrui* @date 2023/9/26 15:57*/
@Service
public class AccountServiceImpl implements AccountService {@Resource(name="accountDaoImpl")private AccountDao accountDao;@Overridepublic void transfer(Integer fid, Integer tid, double balance) {//查询转出账户余额够不够Account fAccount = accountDao.selectByActNo(fid);if(fAccount.getBalance()<balance){throw new RuntimeException("余额不足");}//余额充足Account tAccount = accountDao.selectByActNo(tid);//修改内存中两个对象的值fAccount.setBalance(fAccount.getBalance()-balance);tAccount.setBalance(tAccount.getBalance()+balance);int count = accountDao.updateAct(fAccount);//模拟异常String str=null;System.out.println(str.toString());count+=accountDao.updateAct(tAccount);if(count!=2){System.out.println("转账失败,联系银行");}}
}
Spring配置文件
<?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:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><!--组件扫描--><context:component-scan base-package="com.example"></context:component-scan><!--配置数据源--><bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"><property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property><property name="url" value="1:3306/spring6"></property><property name="username" value="1"></property><property name="password" value="1"></property></bean><!--配置JdbcTemplate--><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><property name="dataSource" ref="dataSource"></property></bean>
</beans>
测试类
import com.example.service.AccountService;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;/*** @author hrui* @date 2023/9/26 16:14*/
public class Test {@org.junit.Testpublic void transfer(){BeanFactory beanFactory=new ClassPathXmlApplicationContext("spring-config.xml");AccountService accountServiceImpl = beanFactory.getBean("accountServiceImpl", AccountService.class);try {accountServiceImpl.transfer(1, 2, 50);System.out.println("转账成功");} catch (Exception e) {e.printStackTrace();}}
}
上面代码中间抛出异常就会导致,一遍转账了,而另一边没收到的情况
以代码逻辑的方式 需要在下面代码中执行1.开启事务 2.执行核心业务逻辑 3.无异常则提交事务
4.有异常则回滚事务
@Overridepublic void transfer(Integer fid, Integer tid, double balance) {//1.开启事务//2.执行核心业务逻辑//查询转出账户余额够不够Account fAccount = accountDao.selectByActNo(fid);if(fAccount.getBalance()<balance){throw new RuntimeException("余额不足");}//余额充足Account tAccount = accountDao.selectByActNo(tid);//修改内存中两个对象的值fAccount.setBalance(fAccount.getBalance()-balance);tAccount.setBalance(tAccount.getBalance()+balance);int count = accountDao.updateAct(fAccount);//模拟异常String str=null;System.out.println(str.toString());count+=accountDao.updateAct(tAccount);if(count!=2){System.out.println("转账失败,联系银行");}//3.如果执行业务流程过程中,没有异常.提交事务//4.如果执行业务流程过程中,有异常,回滚事务}
Spring对事务的支持
Spring实现事务的两种方式
编程式事务:通过编写代码的方式来实现事务的管理
声明式事务:1.基于注解方式 2.基于XML配置方式
Spring事务管理API
Spring对事务的管理底层实现方式是基于AOP实现的.采用AOP的方式进行了封装.所以Spring专门针对事务开发了一套API,API的核心接口如下
PlatformTransactionManager接口:Spring事务管理器的核心接口.在Spring6中它有两个实现
1.DataSourceTransactionManager:支持JdbcTemplate,Mybatis,Hibernate等事务管理
2.JtaTransactionManager:支持分布式事务管理
如果要在Spring6中使用JdbcTemplate,就要使用DataSourceTransactionManager来管理事务.(Srping内置写好了,可以直接使用)
声明式事务基于注解的实现方式
Spring配置文件里配置事务管理器,让SpringIOC容器管理 设置dataSource属性为druid的DataSource实现类
然后在方法上加@Transactional即可
好比有了1 2 3 4的步骤
写在类上,类里面所有方法都有事务控制
写在方法上,单个方法有事务控制
@Transactional注解
事务的传播行为
在a()方法中调用了b()方法,比如a()方法有事务,b()方法也有事务,那么事务是如何传递的?是合并到一个事务?还是另开启一个事务?这就是事务的传播行为
事务一共有七种传播行为:
REQUIRED:支持当前事务,如果不存在就新建一个(默认)[没有就新建,有就加入]
SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行[有就加入,没有就不管了]
MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常[有就加入,没有就抛异常]
REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起[不管有没有事务,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起]
NOT_SUPPORTED:以非事务方式运行,如果有事务在,挂起当前事务[不支持事务,存在就挂起]
NEVER:以事务方式运行,如果有事务存在,挂起当前事务[不支持事务,存在就挂起]
NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套事务中.被嵌套的事务可以独立于外层事务进行提交或回滚.如果外层事务不存在,行为就像REQUIRED一样[有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚,没有事务就和REQUIRED一样]
默认是@Transactional(Propagation=Propagation.REQUIRED)
下面两个是常用的:
REQUIRED:支持当前事务,如果不存在就新建一个(默认)[没有就新建,有就加入]
REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起[不管有没有事务,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起]
举例: A方法和B方法的方法上都有@Transactional(Propagation=Propagation.REQUIRED)默认
在A方法中调用B方法
A(){
保存操作XXXX;
B();
}
B方法
B(){
保存操作
}
那么A方法和B方法在同一个事务中, 即使你在A方法里对B方法进行try catch,无论哪个方法里报错,都会回滚
举例: A方法是@Transactional(Propagation=Propagation.REQUIRED)默认,B方法的方法上是@Transactional(Propagation=Propagation.REQUIRES_NEW)
A方法内进行保存操作且调用了B方法,假如B方法报错了,且A方法没有对B方法进行try catch那么两个都会回滚,假如B方法报错了,但是A方法内对B方法进行了try catch那么B方法会回滚,而A方法不会回滚,因为是两个事务
事务隔离级别:
数据库中读取数据的三大问题:(三大读问题)
脏读:读取到没有提交的数据,叫脏读(读的是缓存(内存里的东西))
不可重复读:在同一个事务当中,第一次和第二次读到的数据不一样
幻读:督导的数据是假的
事务隔离级别包括四个级别:
读未提交:READ_UNCOMMITTED.这种隔离级别,存在脏读问题.所谓脏读(dirty read)表示能够读取到其他事务还未提交的数据
读提交:READ_COMMITTED.解决了脏读问题,其他事务提交之后才能督导,但存在不可重复读问题.(Oracle数据库的默认级别)
可重复读:REPEATABLE_READ.解决了不可重复读,可以达到可重复读的效果,只要当前事务不结束,读取到的数据一直都是一样的.但存在幻读问题(Mysql数据库的默认级别)
序列化:SERIALIZABLE.解决了幻读问题,事务排队执行.不支持并发
读未提交,读已提交,可重复读都是多线程并发问题引起的,序列化就排队
事务超时问题 @Transactional(timeout=10)
以上代码表示设置事务的超时时间为10秒
表示超过10秒如果该事务中所有的DML(增删改)语句还没有执行完毕,最终结果会选择回滚
默认值-1,表示没有时间限制
这里有个坑,事务的超时时间指的是哪段时间?
在当前事务当中,最后一条DML(增删改)语句执行之前的时间,如果最后一条DML语句后面还有很多业务逻辑,这些业务代码执行的时间不会被计入超时时间
只读事务 代码 @Transactional(readOnly=true)
将当前事务设置为只读事务,在该事务执行过程中只允许select语句执行,delete insert update均不可以执行.
该特性的作用:启动Spring的优化策略,提高select语句执行效率.
如果该事务中确实没有增删改操作,建议设置为只读事务.
异常回滚事务:
代码 例 @Transactional(rollbackFor=NumberFormatException.class)
表示只有发生NumberFormatException异常或该异常的子类异常时才回滚
设置哪些异常不回滚事务:
代码 例 @Transactional(noRollbackFor=NullPointerException.class)
表示发生NullPointerException或该类子类异常不回滚,其他异常则回滚