乐趣区

关于shell:完蛋我的事务怎么不生效

前言

事务大家平时应该都有写,之前写事务的时候遇到一点坑,竟然不失效,起初排查了一下,温习了一下各种事务生效的场景,想着不如来一个总结,这样下次排查问题,就能有恃无恐了。那么先来温习一下事务相干常识,事务是指操作的最小工作单位,作为一个独自且不可切割的单元操作,要么全副胜利,要么全副失败。事务有四大个性(ACID):

  • 原子性(Atomicity):事务蕴含的操作,要么全副胜利,要么全副失败回滚,不会存在一半胜利一半失败的中间状态。比方 AB一开始都有 500 元,AB 转账 100,那么A 的钱少了 100B 的钱就必须多了 100,不能A 少了钱,B也没收到钱,那这个钱就不胫而走了,不合乎原子性了。
  • 一致性(Consistency):一致性是指事务执行之前和之后,放弃整体状态的统一,比方 AB一开始都有 500 元,加起来是 1000 元,这个是之前的状态,AB 转账 100,那么最初A400B600,两者加起来还是1000,这个整体状态须要保障。
  • 隔离性(Isolation): 后面两个个性都是针对同一个事务的,而隔离性指的是不同的事务,当多个事务同时在操作同一个数据的时候,须要隔离不同事务之间的影响,并发执行的事务之间不能互相烦扰。
  • 持久性(Durability): 指事务如果一旦被提交了,那么对数据库的批改就是永久性的,就算是数据库产生故障了,曾经产生的批改也必然存在。

事务的几个个性并不是数据库事务专属的,狭义上的事务是一种工作机制,是并发管制的根本单位,保障操作的后果,还会包含分布式事务之类的,然而个别咱们议论事务,不特指的话,说的就是与数据库相干的,因为咱们平时说的事务根本都基于数据库来实现。

事务不仅是实用于数据库。咱们能够将此概念扩大到其余组件,相似队列服务或内部零碎状态。因而,“一系列数据操作语句必须齐全实现或齐全失败,以统一的状态来到零碎”

测试环境

后面咱们曾经部署过了一些 demo 我的项目,以及用 docker 疾速搭建环境,本文基于的也是之前的环境:

  • JDK 1.8
  • Maven 3.6
  • Docker
  • Mysql

事务失常回滚的样例

失常的事务样例,蕴含两个接口,一个是获取所有的用户中的数据,另外一个更新的,是 update 用户数据,其实就是每个用户的年龄+1,咱们让一次操作完第一个之后,抛出异样,看看最初的后果:

@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource
    UserMapper userMapper;

    @Autowired
    RedisUtil redisUtil;

    @Override
    public List<User> getAllUsers() {List<User> users = userMapper.getAllUsers();
        return users;
    }

    @Override
    @Transactional
    public void updateUserAge() {userMapper.updateUserAge(1);
        int i= 1/0;
        userMapper.updateUserAge(2);
    }
}

数据库操作:

<?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.aphysia.springdocker.mapper.UserMapper">
    <select id="getAllUsers" resultType="com.aphysia.springdocker.model.User">
        SELECT * FROM user
    </select>

    <update id="updateUserAge" parameterType="java.lang.Integer">
        update user set age=age+1 where id =#{id}
    </update>
</mapper>

先获取 http://localhost:8081/getUserList 所有的用户看看:

在调用更新接口,页面抛出谬误了:

控制台也呈现了异样,意思是除以 0,异样:

java.lang.ArithmeticException: / by zero
    at com.aphysia.springdocker.service.impl.UserServiceImpl.updateUserAge(UserServiceImpl.java:35) ~[classes/:na]
    at com.aphysia.springdocker.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$c8cc4526.invoke(<generated>) ~[classes/:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123) ~[spring-tx-5.3.12.jar:5.3.12]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388) ~[spring-tx-5.3.12.jar:5.3.12]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119) ~[spring-tx-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.12.jar:5.3.12]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.12.jar:5.3.12]
    at com.aphysia.springdocker.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$25070cf0.updateUserAge(<generated>) ~[classes/:na]

而后咱们再次申请 http://localhost:8081/getUserList,看到数据两个都是11 阐明数据都没有发生变化,第一个操作完之后,异样,回滚胜利了:

[{"id":1,"name":"李四","age":11},{"id":2,"name":"王五","age":11}]

那什么时候事务不失常回滚呢?且听我细细道来:

试验

1. 引擎设置不对

咱们晓得,Mysql其实有一个数据库引擎的概念,咱们能够用 show engines 来查看 Mysql 反对的数据引擎:

能够看到 Transactions 那一列,也就是事务反对,只有 InnoDB,那就是只有InnoDB 反对事务,所以要是引擎设置成其余的事务会有效。

咱们能够用 show variables like 'default_storage_engine' 看默认的数据库引擎,能够看到默认是InnoDB:

mysql> show variables like 'default_storage_engine';
+------------------------+--------+
| Variable_name          | Value  |
+------------------------+--------+
| default_storage_engine | InnoDB |
+------------------------+--------+

那咱们看看咱们演示的数据表是不是也是用了InnoDB, 能够看到的确是应用InnoDB

那咱们把该表的引擎批改成 MyISAM 会怎么样呢?试试,在这里咱们只批改数据表的数据引擎:

mysql> ALTER TABLE user ENGINE=MyISAM;
Query OK, 2 rows affected (0.06 sec)
Records: 2  Duplicates: 0  Warnings: 0

而后再update,不出预料,还是会报错,看起来谬误没有什么不同:

然而获取全副数据的时候, 第一个数据更新胜利了,第二个数据没有更新胜利,阐明事务没有失效。

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

论断:必须设置为 InnoDB 引擎,事务才失效。

2. 办法不能是 private

事务必须是 public 办法,如果用在了 private 办法上,那么事务会主动生效,然而在 IDEA 中,只有咱们写了就会报错:Methods annotated with '@Transactional' must be overrideable,意思是事务的注解加上的办法,必须是能够重写的,private办法是不能够重写的,所以报错了。

同样的 final 润饰的办法,如果加上了注解,也会报错,因为用 final 就是不想被重写:

Spring中次要是用喷射获取 Bean 的注解信息,而后利用基于动静代理技术的 AOP 来封装了整个事务,实践上我想调用 private 办法也是没有问题的, 在办法级别应用 method.setAccessible(true); 就能够,然而可能 Spring 团队感觉 private 办法就是开发人员志愿上不违心公开的接口,没有必要毁坏封装性,这样容易导致凌乱。

Protected办法可不可以?不能够!

上面咱们为了实现,魔改代码构造,因为接口不能用 Portected,如果用了接口,就不可能用protected 办法,会间接报错,而且必须在同一个包外面应用,咱们把 controllerservice放到同一个包下:

测试后发现 事务不失效,后果仍然是一个更新了,另外一个没有更新:

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

论断:必须应用在 public 办法上,不能用在 private,finalstatic 办法上,否则不会失效。

3. 异样必须是运行期的异样

Springboot治理异样的时候,只会对运行时的异样(RuntimeException 以及它的子类)进行回滚,比方咱们后面写的i=1/0;,就会产生运行时的异样。

从源码来看也能够看到,rollbackOn(ex)办法会判断异样是 RuntimeException 或者Error

    public boolean rollbackOn(Throwable ex) {return (ex instanceof RuntimeException || ex instanceof Error);
    }

异样次要分为以下类型:

所有的异样都是 Throwable,而Error 是错误信息, 个别是程序产生了一些不可控的谬误, 比方没有这个文件,内存溢出,IO忽然谬误了。而 Exception 下,除了 RuntimeException,其余的都是CheckException, 也就是能够解决的异样,Java 程序在编写的时候就必须解决这个异样,否则编译是通不过来的。

由上面的图咱们能够看出,CheckedException, 我列举了几个常见的 IOException IO 异样,NoSuchMethodException 没有找到这个办法,ClassNotFoundException 没找到这个类,而 RunTimeException 有常见的几种:

  • 数组越界异样:IndexOutOfBoundsException
  • 类型转换异样:ClassCastException
  • 空指针异样:NullPointerException

事务默认回滚的是:运行时异样,也就是RunTimeException,如果抛出其余的异样是无奈回滚的, 比方上面的代码,事务就会生效:

    @Transactional
     public void updateUserAge() throws Exception{userMapper.updateUserAge(1);
        try{int i = 1/0;}catch (Exception ex){throw new IOException("IO 异样");
        }
        userMapper.updateUserAge(2);
    }

4. 配置不对导致

  1. 办法上须要应用 @Transactional 能力开启事务
  2. 多个数据源配置或者多个事务管理器的时候,留神如果操作数据库 A,不能应用B 的事务,尽管这个问题很童稚,然而有时候用错难查找问题。
  3. 如果在 Spring 中,须要配置 @EnableTransactionManagement 来开启事务,等同于配置 xml 文件 *<tx:annotation-driven/>*, 然而在Springboot 中曾经不须要了,在 springbootSpringBootApplication注解蕴含了 @EnableAutoConfiguration 注解,会主动注入。

@EnableAutoConfiguration主动注入了哪些货色呢?在 jetbrains://idea/navigate/reference?project=springDocker&path=~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.5.6/spring-boot-autoconfigure-2.5.6.jar!/META-INF/spring.factories 下有主动注入的配置:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
...

外面配置了一个TransactionAutoConfiguration,这是事务主动配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(PlatformTransactionManager.class)
@AutoConfigureAfter({ JtaAutoConfiguration.class, HibernateJpaAutoConfiguration.class,
        DataSourceTransactionManagerAutoConfiguration.class, Neo4jDataAutoConfiguration.class })
@EnableConfigurationProperties(TransactionProperties.class)
public class TransactionAutoConfiguration {
  ...
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnBean(TransactionManager.class)
    @ConditionalOnMissingBean(AbstractTransactionManagementConfiguration.class)
    public static class EnableTransactionManagementConfiguration {@Configuration(proxyBeanMethods = false)
        @EnableTransactionManagement(proxyTargetClass = false)   // 这里开启了事务
        @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
        public static class JdkDynamicAutoProxyConfiguration { }
    ...

    }

}

值得注意的是,@Transactional除了能够用于办法,还能够用于类,示意这个类所有的 public 办法都会配置事务。

5. 事务办法不能在同个类外面调用

想要进行事务管理的办法只能在其余类外面被调用,不能在以后类被调用,否则会生效,为了实现这个目标,如果同一个类有不少事务办法,还有其余办法,这个时候有必要抽取出一个事务类,这样分层会比拟清晰,防止后继者写的时候在同一个类调用事务办法,造成凌乱。

事务生效的例子:

比方咱们将 service 事务办法改成:

    public void testTransaction(){updateUserAge();
    }

    @Transactional
     public void updateUserAge(){userMapper.updateUserAge(1);
        int i = 1/0;
        userMapper.updateUserAge(2);
    }

controller 外面调用的是没有事务注解的办法,再间接调用事务办法:

    @RequestMapping("/update")
    @ResponseBody
    public int update() throws Exception{userService.testTransaction();
        return 1;
    }

调用之后,发现事务生效,一个更新另外一个没有更新:

[{"id":1,"name":"李四","age":12},{"id":2,"name":"王五","age":11}]

为什么会这样呢?

Spring用切面对办法进行包装,只对外部调用办法进行拦挡,外部办法没有进行拦挡。

看源码:实际上咱们调用事务办法的时候,会进入 DynamicAdvisedInterceptorpublic Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy)()办法:

外面调用了 AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice(), 这里是获取调用调用链。而没有@Transactional 注解的办法userService.testTransaction(),基本获取不到代理调用链,调用的还是原来的类的办法。

spring外面要想对一个办法进行代理,用的就是 aop,必定须要一个标识,标识哪一个办法或者类须要被代理,spring 外面定义了 @Transactional 作为切点,咱们定义这个标识,就会被代理。

代理的机会是什么时候呢?

Spring对立治理了咱们的 bean,代理的机会天然就是创立bean 的过程,看看哪一个类带了这个标识,就生成代理对象。

SpringTransactionAnnotationParser这个类有一个办法是用来判断 TransactionAttribute 注解的:

    @Override
    @Nullable
    public TransactionAttribute parseTransactionAnnotation(AnnotatedElement element) {
        AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(element, Transactional.class, false, false);
        if (attributes != null) {return parseTransactionAnnotation(attributes);
        }
        else {return null;}
  }

6. 多线程下事务生效

假如咱们在多线程外面像以下形式应用事务,那么事务是不能失常回滚的:

    @Transactional
    public void updateUserAge() {
        new Thread(new Runnable() {
                    @Override
                    public void run() {userMapper.updateUserAge(1);
                    }
                }
        ).start();
        int i = 1 / 0;
        userMapper.updateUserAge(2);
    }

因为不同的线程应用的是不同SqlSession, 相当于另外一个连贯,基本不会用到同一个事务:

2021-11-28 14:06:59.852 DEBUG 52764 --- [Thread-2] org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
2021-11-28 14:06:59.930 DEBUG 52764 --- [Thread-2] c.a.s.mapper.UserMapper.updateUserAge    : <==    Updates: 1
2021-11-28 14:06:59.931 DEBUG 52764 --- [Thread-2] org.mybatis.spring.SqlSessionUtils       : Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2e956409]

7. 留神正当应用事务嵌套

首先事务是有流传机制的:

  • REQUIRED(默认):反对应用以后事务,如果以后事务不存在,创立一个新事务,如果有间接应用以后的事务。
  • SUPPORTS:反对应用以后事务,如果以后事务不存在,就不会应用事务。
  • MANDATORY:反对应用以后事务,如果以后事务不存在,则抛出Exception, 也就是必须以后处于事务外面。
  • REQUIRES_NEW:创立新事务,如果以后事务存在,把以后事务挂起。
  • NOT_SUPPORTED:没有事务执行,如果以后事务存在,把以后事务挂起。
  • NEVER:没有事务执行,如果以后有事务则抛出Exception
  • NESTED:嵌套事务,如果以后事务存在,那么在嵌套的事务中执行。如果以后事务不存在,则体现跟 `REQUIRED

查不多。

默认的是REQUIRED,也就是事务外面调用另外的事务,实际上不会从新创立事务,而是会重用以后的事务。那如果咱们这样来写嵌套事务:

@Service("userService")
public class UserServiceImpl {
    @Autowired
    UserServiceImpl2 userServiceImpl2;
  
    @Resource
    UserMapper userMapper;
  
      @Transactional
    public void updateUserAge() {
        try {userMapper.updateUserAge(1);
            userServiceImpl2.updateUserAge();}catch (Exception ex){ex.printStackTrace();
        }
    }
}

调用的另外一个事务:

@Service("userService2")
public class UserServiceImpl2 {

    @Resource
    UserMapper userMapper;

    @Transactional
    public void updateUserAge() {userMapper.updateUserAge(2);
        int i = 1 / 0;
    }
}

会抛出以下谬误:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

咱们然而理论事务是失常回滚掉了,后果是对的,之所以呈现这个问题,是因为外面到办法抛出了异样,用的是同一个事务,阐明事务必须被回滚掉的,然而外层被 catch 住了,原本就是同一个事务,一个说回滚,一个 catch 住不让 spring 感知到 Exception, 那不是自圆其说么?所以spring 报错说:这个事务被标识了必须回滚掉,最终还是回滚掉了

怎么解决呢?

    1. 外层被动抛出谬误,throw new RuntimeException()
    1. 应用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); 被动标识回滚
    @Transactional
    public void updateUserAge() {
        try {userMapper.updateUserAge(1);
            userServiceImpl2.updateUserAge();}catch (Exception ex){ex.printStackTrace();
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }

8. 依赖内部网络申请回滚须要思考

有些时候,咱们不仅操作本人的数据库,还须要同时思考内部的申请,比方同步数据,同步失败,须要回滚掉本人的状态,在这种场景下,必须思考网络申请是否会出错,出错如何解决,错误码是哪一个的时候才胜利。

如果网络超时了,实际上胜利了,然而咱们断定为没有胜利,回滚掉了,可能会导致数据不统一。这种须要被调用方反对重试,重试的时候,须要反对幂等,屡次调用保留状态的统一,尽管整个主流程很简略,外面的细节还是比拟多的。

总结

事务被 Spring 包裹了复杂性,很多货色可能源码很深,咱们用的时候留神模仿测试一下调用是不是能失常回滚,不能天经地义,人是会出错的,而很多时候黑盒测试基本测试这种异样数据,如果没有失常回滚,前面须要手动解决,思考到零碎之间同步的问题,会造成很多不必要的麻烦,手动改数据库这流程就必须走。

【作者简介】
秦怀,公众号【秦怀杂货店 】作者,技术之路不在一时,山高水长,纵使迟缓,驰而不息。集体写作方向:Java 源码解析JDBCMybatisSpringredis 分布式 剑指 OfferLeetCode等,认真写好每一篇文章,不喜爱题目党,不喜爱花里胡哨,大多写系列文章,不能保障我写的都完全正确,然而我保障所写的均通过实际或者查找材料。脱漏或者谬误之处,还望斧正。

剑指 Offer 全副题解 PDF

2020 年我写了什么?

开源编程笔记

退出移动版