本地事务
一个或一组SQL语句组成一个执行单元,这个执行单元要么全部执行,要么全部不执行
ACID 特性
- 原子性:一个事务的整体操作不可拆分,要么都成功,要么都失败
- 一致性:一个事务执行会使数据从一个一致状态切换到另一个一致状态
以转账为例,转账之前 A 有 1000,B 有 1000, 如果 A 给 B 转 200,成功了,那么 A 就是 800,B 就是 1200,业务前后
它们的总量都是 2000,不可能出现转完账之后,A 扣了 200,B 没加 200 - 隔离性:事务之间互相隔离
假设有 100 个人都在下单,一个人下单失败了,他的数据回滚,不会影响其他人 - 持久性:一个事务一旦提交,则会永久的改变数据库的数据
一旦事务保证了前3项特性,数据库通知事务提交成功了,那就一定会成功,就算数据库刚提示完成功,然后断电了,等再
次启动数据库时,也能在数据库中发现数据的变化
隔离级别
// java 中以注解的方式修改隔离级别
@Transactional(isolation = Isolation.XXX)
READ UNCOMMITTED(读未提交)
该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
READ COMMITTED(读已提交)
一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。
REPEATABLE READ(可重复读)
整个事务期间,只要事务没结束,第一次去数据库查询数据,假设: 1 号记录数据是 100,我们在整个事务期间,无论读多少次,1 号记录的值都是 100,即使其他人把这个数据都删了,或者修改了,我们读到的都是 100 。
该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL的 InnoDB 引擎可以通过 next-key locks 机制(参考下文”行锁的算法”一节)来避免幻读。
SERIALIZABLE(序列化)
在该隔离级别下整个数据库的事务都是串行顺序执行的,这就意味着数据库没有任何并发的能力,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
效果 | |
---|---|
脏读 | A事务未提交却被B事务读取到 |
不可重复读 | A事务做了修改提交了, 同时开启的B事务读取到了修改后的结果 |
幻读 | 同时开启的两个事务中,A事务进行了查询,发现表内有两条数据,此时B事务向表中插入了一条数据并提交,A事务打算修改这两条数据,结果修改了三条数据 |
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
READ UNCOMMITED(读未提交数据) | √ | √ | √ |
READ COMMITED(读已提交数据) | × | √ | √ |
REPEATABLE READ(可重复读) | × | × | √ |
SERIALIZABLE(串行化,性能最差) | × | × | × |
传播行为
1 、PROPAGATION_REQUIRED: :如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
2 、PROPAGATION_SUPPORTS: :支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
3 、PROPAGATION_MANDATORY: :支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
4 、PROPAGATION_REQUIRES_NEW::创建新事务,无论当前存不存在事务,都创建新事务。
5 、PROPAGATION_NOT_SUPPORTED::以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
6 、PROPAGATION_NEVER: :以非事务方式执行,如果当前存在事务,则抛出异常。
7 、PROPAGATION_NESTED: :如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。
示例
/*
a 事务的所有设置,会传播到跟它共用一个事务的方法,
这就意味着,如果 b 也设置 timeout 的话,不会有任何作用,
会因为它的传播行为,跟 a 保持一致
*/
@Transactional(timeout = 30)
public void a(){
// 传播行为指的就是,b、c这两个小事务要不要跟a共用一个事务
b(); // 相当于 a 跟 b 在同一条连接里执行
c(); // 不与 a b 共用一个
/*
情景1:
一旦出现了这个异常,a 方法就炸了,
因为 a、b 共用一个事务,所以它俩都会回滚
c 因为是一个新事务,所以不会回滚
*/
int i = 10 / 0;
}
// REQUIRED:单纯的需要一个事务,如果 a 已经有了,就会直接使用 a 的
@Transactional(propagation = Propagation.REQUIRED)
public void b(){
/*
情景2:
因为 a、b 共用一个事务,所以它俩都会回滚
c 不受影响
*/
int i = 10 / 0;
}
// REQUIRES_NEW:总是需要一个新的事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c(){
}
同一个对象内,事务方法互调默认失效
上述的这个示例,如果 a、b、c 方法全都在同一个 service 下,那么 b、c 做的传播行为配置,都不会起作用,也就是说b、c都会跟 a 共用一个事务
除非b、c在其他不同的 service,那样才能使它们自己的配置生效
原因
事务是用代理对象来控制的,如果在 a 里面调用的是同一个 service 的 b、c方法,相当于把 b、c 的代码复制、粘贴过来了,也就是跳过了代理
解决
使用代理对象调用b、c方法,即可解决
- 导入 spring-boot-starter-aop 依赖,这个依赖引入了 aspectj
- 启动类开启 aspectj 动态代理功能,以后所有的动态代理都是 aspectj 创建的(即使没有接口也可以创建动态代理),对外暴露代理对象
@EnableAspectJAutoProxy(exposeProxy=true)
- 用代理对象对本类互调
AopContext.currentProxy()
调用方法
@Transactional(timeout = 30)
public void a(){
// 直接强转,然后b、c的传播行为设置就能起作用了
OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
orderService.b();
orderService.c();
}
// REQUIRED:单纯的需要一个事务,如果 a 已经有了,就会直接使用 a 的
@Transactional(propagation = Propagation.REQUIRED)
public void b(){
}
// REQUIRES_NEW:总是需要一个新的事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c(){
}
本地事务在分布式下的问题
1、假失败
如果保存订单成功,远程锁库存假失败,那就会出现问题
假失败就是我们在订单服务调库存服务时, 库存锁定成功,然后由于服务器慢、卡顿、等故障原因,本地事务提交了之后,一直没返回到订单服务
此时再看订单服务,因为调用库存服务时间太长了,库存服务迟迟没有返回结果,可能就会触发 feign 的超时机制,在调用远程服务这里抛异常:read time out 读取超时,但是这个异常并不是我们手动抛的锁库存异常,而是 feign 的异常
并且订单服务,设计的回滚机制,是只要一出现异常就会全部回滚,
结果:库存锁定成功,订单服务因为 feign 的超时机制,出现异常,导致订单数据全部回滚,最终数据不一致
2、调用新服务出现异常之后,已经执行的服务不会回滚
假设库存锁定成功,将结果返回到了订单服务,我们根据结果又调用了积分服务,让它扣减积分,
结果积分服务内部出现异常,积分数据回滚
此时再看订单服务,订单服务感知到我们手动抛的积分异常,订单数据回滚,但是库存服务,却不会有任何感知,
结果:积分、订单数据全部回滚,库存给锁定了,也是数据不一致
只需要在订单服务的库存执行成功之后,添加一个 int i = 10 / 0;
,模拟积分服务出现异常,很容易就能复现这个问题
总结
本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚
产生分布式事务的最大原因,就是网络问题 + 分布式机器