Spring IoC和AOP看这里:Spring IoC和AOP–基于SpringBoot AOP开发
文章目录
事务简介
在JavaEE企业及开发的应用中,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的一部分。事务就是一组由于逻辑紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
事务特性(ACID):
- 原子性(Atomicity):”原子“的本意为”不可分割“,事务中的原子性就是一个事务中涉及多个操作在逻辑上缺一不可。事务的原子性要求事务中的操作要么都执行,要么都不执行。
- 一致性(Consistency):一致性是指数据一致,所有数据都满足业务规则的一致性状态。不管事务中有多少操作都必须保证执行前数据都是正确的,执行后数据仍是正确的。如果事务执行的过程中某一个操作失败了,必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
- 隔离性(Isolation):在应用程序中事务往往是并发的执行的,所有很有可能有多个事务处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。事务的隔离性要求多个事务在并发过程中不会相互干扰。
- 持久性(Durability):持久性要求事务执行完成后,对数据的修改要永久保存,不会因为各种系统错误或者其他意外情况而受到影响,事务对数据的修改通常应该被写入到持久化的存储器中。
Spring事务
Spring支持编程式事务管理和声明式事务管理两种方式。
编程式事务管理:是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager。
声明式事务管理:是基于AOP在方法前后对其拦截,在目标方法执行之前创建事务,并根据目标方法执行情况提交或者回滚。
编程式事务每次都要单独实现,在业务功能大的情况下使用很头疼;而声明式事务不同,它属于无侵入式,不会影响业务代码,只需要配置相关事务声明或者通过注解的方式,便可将事务应用在业务代码中。
基于SpringBoot注解事务示例
这里的演示基于SpringBoot,持久层使用MyBatis:
新建maven工程,pom.xml加入以下依赖,为了方便测试我们也引入test启动器:
<?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>test-aspect</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
创建启动类Application.java:
@SpringBootApplication
@MapperScan("com.**.mapper*") //扫描mapper接口
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
配置文件application.yml(数据库、用户和密码改成自己的):
spring:
datasource:
url: jdbc:mysql://localhost/test?characterEncoding=UTF-8&useUnicode=true&useSSL=false&autoReconnect=true&serverTimezone=UCT
username: root
password: root
driverClassName: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapping/*Mapping.xml
logging:
level:
com.spring.mapper: debug
建表语句:
DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
`id` int(11) NOT NULL,
`bname` varchar(100) DEFAULT NULL,
`price` int(11) DEFAULT NULL
) ;
INSERT INTO `book` VALUES (1,'坏蛋是怎么样炼成的',100),(2,'时间管理',100),(3,'Java虚拟机',100),(4,'数据库概述',100);
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL,
`name` varchar(100) DEFAULT NULL,
`balance` int(11) DEFAULT NULL
) ;
INSERT INTO `user` VALUES (1,'张三',10000),(2,'李四',10000),(3,'王五',10000),(4,'赵六',10000);
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
`id` int(11) NOT NULL,
`bid` int(11) DEFAULT NULL,
`stock` int(11) DEFAULT NULL
) ;
INSERT INTO `stock` VALUES (1,1,500),(2,2,500),(3,3,500),(4,4,500);
创建三个实体(需要Getter和Setter方法):
Book.java:
public class Book { //图书实体
private int id;
private String bName; //书名
private int price; //销售价格
}
Stock.java:
public class Stock { //库存实体
private int id;
private int bId; //书id
private int stock; //库存
}
User.java:
public class User { //用户实体
private Integer id;
private String name; //用户姓名
private Integer balance; //用户余额
}
创建三个mapper接口:
BookMapper.java:
@Mapper
public interface BookMapper {
Book selectPrice(@Param("id") int id); //用id获取图书信息
}
StockMapper.java:
@Mapper
public interface StockMapper {
int update(@Param("stock") int stock, @Param("bid") int bid); //修改库存
}
UserMapper.java:
@Mapper
public interface UserMapper {
int update(@Param("balance") int balance, @Param("name") String name); //修改余额
}
创建三个映射文件:
BookMapping.xml:
<?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.spring.mapper.BookMapper">
<select id="selectPrice" resultType="com.spring.entity.Book">
select * from book where id = #{id}
</select>
</mapper>
StockMapping.xml:
<?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.spring.mapper.StockMapper">
<update id="update">
update stock set stock = stock - #{stock} where bid = #{bid}
</update>
</mapper>
UserMapping.xml:
<?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.spring.mapper.UserMapper">
<update id="update">
update user set balance = balance - #{balance} where name = #{name}
</update>
</mapper>
创建service接口UserService.java:
public interface UserService {
void buyBook();
}
创建service实现类UserServiceImpl.java:
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
BookMapper bookMapper;
@Autowired
StockMapper stockMapper;
@Override
@Transactional
public void buyBook() {
//模拟张三买了一本书,需要减去库存,减去张三账户余额
Book book = bookMapper.selectPrice(1); //用id获取图书信息
stockMapper.update(1,book.getId()); //修改库存
userMapper.update(book.getPrice(),"张三"); //修改余额
}
}
新建测试类TestSpringTx.java:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class) //作用
public class TestSpringTx {
@Autowired
UserService userService;
@Test
public void test01(){
userService.buyBook();
}
}
工程结构:
执行测试方法,正常执行,查看表中数据:
: ==> Preparing: select * from book where id = ?
: ==> Parameters: 1(Integer)
: <== Total: 1
: ==> Preparing: update stock set stock = stock - ? where bid = ?
: ==> Parameters: 1(Integer), 1(Integer)
: <== Updates: 1
: ==> Preparing: update user set balance = balance - ? where name = ?
: ==> Parameters: 100(Integer), 张三(String)
: <== Updates: 1
这是我们改一下StockMapping.xml里面的SQL:
再次执行测试方法,报错,然后查看表数据:
: ==> Preparing: select * from book where id = ?
: ==> Parameters: 1(Integer)
: <== Total: 1
: ==> Preparing: update stock set stock = stock - ? where bid = ?
: ==> Parameters: 1(Integer), 1(Integer)
: <== Updates: 1
: ==> Preparing: updateaa user set balance = balance - ? where name = ?
: ==> Parameters: 100(Integer), 张三(String)
在执行更新库存表的时候没有异常,执行更新用户余额的时候报错,但是库存更新也回滚了。
@Transactional
@Transactional是声明式事务管理编程中使用的注解,用于接口实现类或接口实现方法上,只对public修饰的方法起作用。读取数据的接口尽量不要用此注解,注解底层采用AOP的动态代理拦截事务会影响系统性能。
这里看一下@Transactional的实现:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default -1;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
常用参数:
1. 超时 timeoutint
默认值-1(不设置超时),单位秒,设定的时间内没有执行完成会报事务超时异常。
@Transactional(timeout = 3)
public void buyBook() throws InterruptedException {
//模拟张三买了一本书,需要减去库存,减去张三账户余额
Book book = bookMapper.selectPrice(1);
stockMapper.update(1,book.getId());
Thread.sleep(3000);
userMapper.update(book.getPrice(),"张三");
}
执行输出:
2. 只读 readOnly
默认值false,设置事务是否只读,对查询事务设为只读可加快查询速度;有增删改的事务不可设置为true。
3. 异常回滚 rollbackFor和rollbackForClassName
默认值都是{},rollbackFor参数类型是Class<? extends Throwable>[],rollbackForClassName参数类型是String[ ]。
这里需要了解异常分类:
运行时异常(非检查异常):这种异常不用处理(例如除零异常),事务中发生运行时异常默认回滚。
编译时异常(检查异常):要么try-catch,要么throws(例如FileNotFoundException),事务中发生编译时异常默认不回滚。
事务中出现编译时异常哪些异常需要回滚:
这里我们用FileInputStream构造一个不存在的文件,然后抛出异常,把库存全部改为500,把用户余额全部改为10000,然后执行测试方法:
@Transactional
public void buyBook() throws FileNotFoundException {
//模拟张三买了一本书,需要减去库存,减去张三账户余额
Book book = bookMapper.selectPrice(1);
stockMapper.update(1,book.getId());
userMapper.update(book.getPrice(),"张三");
new FileInputStream("D://qezxadfasre.qerq");
}
这时发现虽然有异常发生了,但是事务并没有回滚,表中数据也更新了:
如果想在发生编译时异常回滚,使用noRollbackFor 或noRollbackForClassName 。
@Transactional(noRollbackFor = {FileNotFoundException.class})
// @Transactional(noRollbackForClassName = {"java.io.FileNotFoundException"})
public void buyBook() throws FileNotFoundException {
//模拟张三买了一本书,需要减去库存,减去张三账户余额
Book book = bookMapper.selectPrice(1);
stockMapper.update(1,book.getId());
userMapper.update(book.getPrice(),"张三");
new FileInputStream("D://qezxadfasre.qerq");
}
4. 异常不回滚 noRollbackFor和noRollbackForClassName
默认值都是{},noRollbackFor参数类型是Class<? extends Throwable>[],noRollbackForClassName参数类型是String[ ]。
事务中出现运行时异常哪些异常不需要回滚:
这里我们用除零触发运行时异常,把库存全部改为500,把用户余额全部改为10000,然后执行测试方法:
@Transactional
public void buyBook() {
//模拟张三买了一本书,需要减去库存,减去张三账户余额
Book book = bookMapper.selectPrice(1);
stockMapper.update(1,book.getId());
userMapper.update(book.getPrice(),"张三");
int a = 1/0;
}
发生运行时异常,回滚:
如果发生运行时异常不回滚,可以使用noRollbackFor或noRollbackForClassName。
@Transactional(noRollbackFor = {ArithmeticException.class})
// @Transactional(noRollbackForClassName = {"java.lang.ArithmeticException"})
public void buyBook() {
//模拟张三买了一本书,需要减去库存,减去张三账户余额
Book book = bookMapper.selectPrice(1);
stockMapper.update(1,book.getId());
userMapper.update(book.getPrice(),"张三");
int a = 1/0;
}
执行测试方法,并查看表:
5. 隔离级别 isolation
默认值Isolation.DEFAULT,参数类型Isolation。
事务隔离级别:一个事务和其他事务的隔离程度成为隔离级别,这是数据库具有隔离并发运行各个事务的能力,使它们不会互相影响,避免各种并发问题。SQL标准中定义了多种事务隔离级别,不同的隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,但并发越弱。
没有隔离级别的时候,事务并发会出现什么问题,假设现在有Tx1和Tx2两个事务并发执行:
- 脏读
(1)Tx1将某条记录的stock的值从500改成499;
(2)Tx2读取了Tx2更新后的值499;
(3)Tx1发生异常回滚,stock的值恢复到了500;
(4)Tx2读取到的499就是一个无效的脏数据。 - 不可重复读
(1)Tx1读取到stock的值为500;
(2)Tx2将stock的值从500改为400;
(3)Tx1再次读取stock的值为400。 - 幻读
(1)Tx1读取了book表中的一部分数据;
(2)Tx2向book表中插入了新的行;
(3)Tx1读取book表时,多出了一些行。
隔离级别:
读未提交
(READ UNCOMMITTED):允许Tx1读取Tx2未提交的数据。读已提交
(READ COMMITTED):要求Tx1只能读取Tx2已提交的数据。可重复读
(REPEATABLE READ):Tx2执行事务期间,确保Tx1多次从一个字段中取到相同值。串行化
(SERIALIZABLE):Tx2执行事务期间,确保Tx1多次从一个表读取到相同行,在Tx2执行期间,禁止其他事务对这个表进行添加、更新和删除操作,可以避免任何并发问题,但是性能十分低下。
各个隔离级别解决并发问题:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | ✔ | ✔ | ✔ |
READ COMMITED | ✘ | ✔ | ✔ |
REPEATABLE READ | ✘ | ✘ | ✔ |
SERIALIZABLE | ✘ | ✘ | ✘ |
Oracle和MySQL支持的隔离级别:
隔离级别 | Orcale | MySQL |
---|---|---|
READ UNCOMMITTED | ✘ | ✔ |
READ COMMITED | ✔ | ✔ |
REPEATABLE READ | ✘ | ✔ |
SERIALIZABLE | ✔ | ✔ |
@Transaction设置隔离级别用Isolation参数,参数类型是Isolation枚举:
public enum Isolation {
DEFAULT(-1), //不设置隔离级别
READ_UNCOMMITTED(1), //读未提交
READ_COMMITTED(2), //读已提交
REPEATABLE_READ(4), //可重复读
SERIALIZABLE(8); //串行化
}
MySQL数据库中事务隔离的现象:
查看MySQL全局隔离级别:
SELECT @@global.tx_isolation; -- 查询全局隔离级别
SELECT @@session.tx_isolation; -- 查询当前会话隔离级别
SELECT @@tx_isolation; -- 同上
打开cmd,执行mysql -uroot -proot(-u后面是数据库用户名,-p后面是密码;如果报’mysql’ 不是内部或外部命令,在mysql安装路径运行即可):
设置当前会话隔离级别:
SET [SESSIONI GLOBAL] TRANSACTION ISOLATION LEVEL (READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE )
-- 如:SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
1、读未提交的脏读问题
修改当前会话隔离级别为读未提交(READ UNCOMMITTED):
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
再打开一个cmd窗口,执行mysql -uroot -proot,模拟两个事务在读未提交下的现象,用start transaction;
在两个客户端都开启事务(蓝色为客户端1,灰色为客户端2):
- 客户端1执行
select price from book where bname ='Java虚拟机';
- 客户端2执行
update book set price=112 where bname = 'Java虚拟机';
- 客户端1执行
select price from book where bname ='Java虚拟机';
- 客户端2回滚,执行
rollback;
- 客户端1执行
select price from book where bname ='Java虚拟机';
那么在第三步读取到的数据112
就是脏数据。
2、读已提交避免脏读,没有避免不可重复读
修改客户端1会话隔离级别为读已提交(READ COMMITTED):
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
在两个客户端都开启事务:
1. 客户端1执行select price from book where bname ='Java虚拟机';
2. 客户端2执行update book set price=112 where bname = 'Java虚拟机';
3. 客户端1执行select price from book where bname ='Java虚拟机';
4. 客户端2提交,执行commit;
5. 客户端1执行select price from book where bname ='Java虚拟机';
在客户端1中,在一个事务中读取了三次,最后一次读取的值和其他数据不一样。
3、可重复读级别下,只要在一个事务中,读取的数据一直相同
修改客户端1会话隔离级别为读已提交(REPEATABLE READ)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
在两个客户端都开启事务:
1. 客户端1执行select price from book where bname ='Java虚拟机';
2. 客户端2执行update book set price=112 where bname = 'Java虚拟机';
3. 客户端1执行select price from book where bname ='Java虚拟机';
4. 客户端2执行delete from book where bname = 'Java虚拟机';
5. 客户端2提交,执行commit;
6. 客户端1执行select price from book where bname ='Java虚拟机';
客户端1开启事务后读取的每次数据都一致,即便数据被删掉了,在开启事务的时候会保存一个快照把数据定格。
并发修改
在两个客户端都开启事务,同时改修一条数据,先执行语句的事务不执行提交或者回滚,后执行的语句就一直在等待:
先执行语句的事务提交或者回滚,后执行的客户端才能执行语句。
无论哪种隔离级别,两个事务同时修改一行数据,先执行语句的提交或者回滚完成之后,后执行语句的事务才能执行语句,所以隔离级别是隔离读的。
6. 传播行为 ropagation
事务传播行为,默认值Propagation.REQUIRED,参数类型枚举Propagation:
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
}
事务的传播行为,当事务方法被另一个事务方法调用时,指定事务如何传播,也就是说两个事务方法是否共享同一个事务。例如:方法可继续在现有的事务中运行,也可开启一个新事务,在新启的事务中运行,Spring中定义了期七种传播类型:
传播类型 | 描述 |
---|---|
REQUIRED | 如果有事务运行,当前的方法就在这个事务运行;否则就启一个新的事务,并在新启的事务中运行 |
REQUIRES_NEW | 当前方法必须新启一个事务,并在新启的事务中运行 |
SUPPORTS | 如果有事务在运行,当前方法就在事务中运行;否则它不在事务中运行 |
NOT_SUPPORTED | 当前方法不再事务中运行,如果有运行事务就将它挂起 |
MANDATORY | 当前方法必须在运行的事务内部,如果没有正常运行的事务,就抛出异常 |
NEVER | 当前方法不应该运行在事务中,如果有运行的事务,就抛出异常 |
NESTED | 如果有事务在运行,当前方法就在这个事务的嵌套事务中运行;否则就启动一个新的事务,并在新启的事务中运行 |
被调用方法在运行的事务中运行时,事务的属性由运行中的事务决定,被调用方法的事务属性不起作用(例如timeout)。同一个类下面的事务方法调用,就只有一个事务,即使传播属性都为REQUIRES_NEW也不起作用。