事务
数据库的事务,及事务的4种特性:原子性、一致性、持久性、隔离性 这里就不多解释了。主要记录下 事务的隔离性。
事务的隔离 ,我的理解是,表示 有多个事务在操作DB 时的一种 读原则吧。如果有其他事务正在操作DB时,我这个事务读取DB时的值是什么样的。
以mysql为例,事务的隔离有 串行、重复读、读已提交、读未提交;
- 串行: 非常好理解,就是 提交的事务会排队执行。
- 读已提交:T1会读取T2已提交的
- 读未提交:T1会读取T2还没有提交的
- 重复读:T1 读取的是 T1开始时 DB中的值,不管T2是提交了还是没有提交,都不影响。
常用的事务相关命令:
-- 查看事务隔离级别
select @@global.tx_isolation
select @@session.tx_isolation -- 当前会话
-- 修改事务隔离级别
set session transaction isolation level read uncommitted
set session transaction isolation level read committed
set session transaction isolation level repeatable read
set session transaction isolation level serializable
-- 查看自动提交
show variables like 'autocommit'
-- 开启自动提交
set autocommit = 1
-- 关闭自动提交
set autocommit = 0 -- 和 start transaction 类似
-- 事务就绪,执行第一个select语句时,才真正开始事务
start transaction
-- 事务立即开启
start transaction with consistent snapshot
-- 提交事务
commit
-- 回滚事务
ROLLBACK
开启事务:
第一种:设置后,只有执行了第一个 select 语句,事务才真正开启。
-- 关闭自动提交
set autocommit = 0
-- 显示开启
start transaction
第二种:设置后,事务立即开启
-- 显示开启
start transaction with consistent snapshot
一般,在编程过程中,都是使用 第一种 set autocommit = 0 的方式 ,关闭 事务的自动提交 来开启事务的,也就是 需要执行第一个 select 语句,当前的事务才真正的开启。如果使用的是mysql的默认的可重复读的隔离级别,当前事务如果还没有执行select语句,那么其他事务的一些commit之后的更新,当前事务依然可以看的到。
示例:可重复读、自动提交 验证 事务开启时机
示例1:
示例2:
通过示例1和示例2,可以说明,一个事务真正开启是在 执行了 select 之后,才会记录当前的快照。
事务隔离级别的行为控制,是针对当前事务 对 数据 读取的行为 控制
session A 设置了 读已提交,表示 session A 的事务 对 已提交的数据更改可读
session B 设置了 可重复读,表示 session B 的事务 对 数据是可重复读取,不论该数据是否被修改
session B 的修改,如果提交了,session A 就可以读到数据,
因为session A是 读(任何事务)已提交(的修改)
session A的修改,不论是否提交,session B 都读取不到
因为session B 是可重复读 当前事务内的数据,而不受其他事务对该数据的修改
Spring事务传播
MySql中的事务,是数据库层面支持的。在编程过程中,不同的业务代码 如何 操作事务?
事务 一般是指 一些列的 SQL 语句,事务的原子性要求 事务中的 所有SQL语句,要么全部成功,要不全部失败。 这一特性,如何在编程的时候实现呢?
在编程时,不同的SQL语句 很可能是遍布在不同的service中的,如果把这些SQL语句控制在同一个事务中呢?这就是 Spring的事务传播机制的用处
举一个场景 来理解一下 Spring的事务传播机制能给我们编程带来哪些好处。
比如一段业务逻辑:
执行一条 更新语句
其他业务代码
调用一个service方法,该service方法需要执行其他的更新语句
其他业务代码
调用另一个service方法:
该service方法需要执行其他的更新语句
调用另一个service方法,该service方法执行一些增删改查,远程调用等操作
其他业务代码
结束
要求:上述的业务逻辑处理过程中,有任何一个SQL执行异常或者代码抛异常,那么其他所有已执行的SQL需要全部回滚。 这时,如果没有spring的事务穿模,是不是有点不好办了?也许,在没有Spring框架时,有其他的办法来解决。不过,就目前Spring的项目来说,只有事务传播机制能帮我们来解决这个问题。
spring 事务传播(不同service 方法之间的调用):
第一类:严格在事务中运行
第二类:严格不在事务中运行
第三类:不严格 在或者不在 事务中运行,不自己新建事务
第一类:严格在事务中运行
1. 严格在事务中运行, 存在,则沿用,不存在,则新建 REQUIRED 必须的
3. 严格在事务中运行,存在,则沿用,不存在,则抛异常 MANDATORY 强制的
4.严格在事务中运行,存在,则挂起当前事务再新建事务,不存在,则新建事务 REQUIRED_NEW
7.严格在事务中运行,存在,则在当前事务中嵌套一个事务,不存在,则新建 NESTED 嵌套
第三类:不严格 在或者不在 事务中运行,不自己新建事务
2. 不严格在事务中运行,存在,则沿用,不存在,则 不新建 SUPPORTS 支持
第二类:严格不在事务中运行
5. 严格不在事务中运行,存在,则挂起当前事务,不存在,则不新建 NOT_SUPPORTED
6.严格不在事务中运行,存在,则抛异常,不存在,则不新建 NEVER 绝不
名词解释:
挂起事务: 类似于 把当前连接保存,使用 新的连接来处理后续的db操作
嵌套事务:使用 savepoint 机制
一般,常用的就是 REQUIRED 和 REQUIRED_NEW。
如果希望 运行在已有事务中,则使用 REQUIRED
如果希望 运行在 自己的 事务中,则使用 REQUIRED_NEW
Required,受外围方法的事务情况影响
required_new 不受外围方法的事务影响
默认传播方式:REQUIRED
spring boot 事务示例
使用注解:
@Transactional
该注解有几个属性值:
- 事务管理器
- 事务传播方式
- 事务隔离级别
- 回滚方式
- 不回滚方式
如果一个方法,没有加@Transactional注解,就没有使用 Spring 的事务管理功能。此时的数据库SQL操作,可以看成是用 控制台在执行SQL语句,即:SQL操作自动提交
事务传播规律:
在一个方法上加上事务注解:
方法内的多个SQL操作,在同一个事务内
方法内调用其他service的方法 或者 通过依赖自己 调用自己的方法:
其他service方法加事务注解:根据传播方式使用事务
其他service方法没加事务注解:和调用方法在同一个事务内
方法内调用自己的方法:
不论自己方法上有没有添加事务注解,都是和调用方法在同一个事务内
在一个方法上不加事务注解:
其他SQL或者其他service方法的事务,都跟当前方法没有关系
事务失效的几种情况:
1. 方法不是public
2. 类内的方法互相调用
解决:
1. 将方法拆分到不同的service
2. 自己注入自己
3. <aop:aspectj-autoproxy expose-proxy="true"/>或者 <aop:config expose-proxy="true">
代码调用:
((ServiceA ) AopContext.currentProxy()).insert()
3. 数据源没有配置事务管理器
4. 异常没cache住或者抛出的异常类型不回滚事务,那么事务不会回滚
并发操作的事务示例:
需求:对库存表进行更新操作,如果库存不为0,则库存减1
第一种:
查询之后,产品剩余数量可能被其他线程更新了,所以 用查询出来的结果判断,会不准确
public void reduceProduct(){
// 查询
ZProduct zProduct = zProductMapper.selectById(Integer.valueOf(1));
// 判断
if (zProduct.getProductCount() > 0){
// 更新库存
ZProduct updateZProduct = new ZProduct();
updateZProduct.setId(zProduct.getId());
updateZProduct.setProductCount(zProduct.getProductCount() - 1);
zProductMapper.updateById(updateZProduct);
// 生成订单
zOrderMapper.insert(new ZOrder("book"));
}
}
第二种:
这种写法也存在问题。
原因是:
第一个线程更新的事务,在释放锁之后,才提交。而在释放锁和事务提交之间,其他线程的事务看到的还是之前的库存值,因此,其他线程再进行 更新库存时,用的是旧数据进行更新,导致库存少减一次。
如果使用下面的语句更新库存,其实也存在问题,因为虽然库存值最终被更新为0,但是订单已经生成了啊
@Update("update z_product set product_count = product_count - 1 where id = ${id}")
int updateProduct(@Param("id") Integer id);
示例代码
ReentrantLock lock = new ReentrantLock();
@Transactional
public void reduceProduct(){
lock.lock();
System.out.println("lock");
try {
ZProduct zProduct = zProductMapper.selectById(Integer.valueOf(1));
System.out.println("count:" + zProduct.getProductCount());
if (zProduct.getProductCount() > 0){
ZProduct updateZProduct = new ZProduct();
updateZProduct.setId(zProduct.getId());
updateZProduct.setProductCount(zProduct.getProductCount() - 1);
zProductMapper.updateById(updateZProduct);
zOrderMapper.insert(new ZOrder("book"));
System.out.println("success");
}else {
System.out.println("没库存");
}
System.out.println("unlock");
}finally {
lock.unlock();
}
}
第三种:
对于第二种的问题处理,只需要将 业务的事务和 锁分开,满足 事务提交后,锁再释放就OK 了
---------------------------------------------------底线---------------------------------------------------------------