本地事务

一个或一组SQL语句组成一个执行单元,这个执行单元要么全部执行,要么全部不执行

ACID 特性

  1. 原子性:一个事务的整体操作不可拆分,要么都成功,要么都失败
  2. 一致性:一个事务执行会使数据从一个一致状态切换到另一个一致状态
    以转账为例,转账之前 A 有 1000,B 有 1000, 如果 A 给 B 转 200,成功了,那么 A 就是 800,B 就是 1200,业务前后
    它们的总量都是 2000,不可能出现转完账之后,A 扣了 200,B 没加 200
  3. 隔离性:事务之间互相隔离
    假设有 100 个人都在下单,一个人下单失败了,他的数据回滚,不会影响其他人
  4. 持久性:一个事务一旦提交,则会永久的改变数据库的数据
    一旦事务保证了前3项特性,数据库通知事务提交成功了,那就一定会成功,就算数据库刚提示完成功,然后断电了,等再
    次启动数据库时,也能在数据库中发现数据的变化

隔离级别

  1. // java 中以注解的方式修改隔离级别
  2. @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方法,即可解决

  1. 导入 spring-boot-starter-aop 依赖,这个依赖引入了 aspectj
  2. 启动类开启 aspectj 动态代理功能,以后所有的动态代理都是 aspectj 创建的(即使没有接口也可以创建动态代理),对外暴露代理对象
    @EnableAspectJAutoProxy(exposeProxy=true)
  3. 用代理对象对本类互调
    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;,模拟积分服务出现异常,很容易就能复现这个问题

总结

本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚

产生分布式事务的最大原因,就是网络问题 + 分布式机器