我大抵是卷上瘾了,横竖都睡不着,坐起来身来打开Mac和外接显示器,这Bug没有由来,默然看着打印异常的屏幕,一个是我的,另外一个也是我的。
今天这个问题主要体现在大家平常用的Mybatis,在插入数据的时候,我们可以把库表索引的返回值通过入参对象返回回来。但是通过我自己手写的Mybatis,每次返回来的都是0,而不是最后插入库表的索引值。因为是手写的,不是直接使用Mybatis,所以我会从文件的解析、对象的映射、SQL的查询、结果的封装等一直排查下去,但竟然问题都不在这?!
- 就是这个 selectKey 的配置,在执行插入SQL后,开始执行获取最后的索引值。
- 通常只要配置的没问题,返回对象中也有对应的 id 字段,那么就可以正确的拿到返回值了。PS:问题就出现在这里,小傅哥手写的 Mybatis 竟然只难道返回一个0!
二、分析:诊断异常
可能大部分研发伙伴没有阅读过 Mybatis 源码,所以可能不太清楚这里发生了什么,小傅哥这里给大家画张图,告诉你发生了什么才让返回的结果为0的。
- Mybatis 的处理过程可以分为两个大部分来看,一部分是解析,另外一部分是使用。解析的时候把 Mapper XML 中的 insert 标签语句解析出来,同时解析 selectKey 标签。最终解析完成后,把解析的语句信息使用 MappedStatement 映射语句类存放起来。便于后续在 DefaultSqlSession 执行操作的时候,可以从 Configuration 配置项中获取出来使用。
- 那么这里有一个非常重要的点,就是执行 insert 插入的时候,里面还包含了一句查询的操作。那也就是说,我们会在一次 Insert 中,包含两条执行语句。重点:bug就发生在这里,为什么呢?因为最开始这两条语句执行的时候,在获取链接的时候,每一条都是获取一个新的链接,那么也就是说,insert xxx、select LAST_INSERT_ID() 在两个 connection 连接执行时,其实是不对的,没法获取到插入后的索引 ID,只有在一个链接或者一个事务下(一次 commit)才能有事务的特性,获取插入数据后的自增ID。
- 而因为这部分最开始手写 JdbcTransaction 实现 Transaction 接口获取连接的时候,每一次都是新的链接,代码块如下;
- 这里的链接获取,最开始没有 if null 的判断,每次都是直接获取链接,所以这种非一个链接下的两条 SQL 操作,所以必然不会获得到正确的结果,相当于只是单独执行SELECT LAST_INSERT_ID() 所以最终的查询结果为 0 了就!你可以测试把这条语句复制到 SQL查询工具中执行
三、震惊:同一个坑
但其实就这么一个链接的问题,在小傅哥手写Spring中也同样遇到过。
在 Spring 中有一部分是关于事务的处理,其实这些事务的操作也是对 JDBC 的包装操作,依赖于数据源获得的链接来管理事务。而我们通常使用 Spring 也是结合着 Mybatis 配置上数据源的方式进行使用,那么在一个事务下操作多个 SQL 语句的时候,是怎么获得同一个链接的呢。因为从上面👆🏻的案例中,我们得知保证事务的特性,需要在同一个链接下,即使是操作多条SQL
由于多个SQL的操作,已经是相当于每次都获取一个新的 Session 有一个新的链接从连接池中获得,但为了能达到事务的特性,所以在需要有事务操作下的多个 SQL 前需要开启事务操作,无论是手动还是注解。
而这个事务的开启动作处理做一些事务传播行为和隔离级别的限制,其实更重要的是让多个 SQL 的执行获取的链接,需要是同一个。所以这里就引入了 ThreadLocal 基于它在同一个线程操作下保存信息的同步特性,其实这里的从事务下获取的链接,其实就是保存到 TransactionSynchronizationManager#resources 属性中的。
虽然就这么一小块内容,但在小傅哥最开始手写Spring的时候,也是给漏下了。直到到测试的时候,才发现链接发现事务总是不成功,最初还以为是整个切面逻辑没有切进去或者是我的操作方式有误。直到逐步排查调试代码,发现原来多个SQL的执行竟然不是获得的同一个链接,所以也就没法让事务生效。
四、常见:事务失效
可能就是这么一个小小的链接问题,有时候就会引起一堆的异常,如果说我们没有学习过源码,那么可能也不知道这样的问题到底是如何发生的。所以往往深入的研究和探索,才能让你解释一个问题的时候,更加简单直接。
那么你说,事务失效的原因还有哪些?- 分享一些常见,如果你还有遇到其他的,可以发到评论区一起看看。
- 数据库引擎不支持事务:这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。https://dev.mysql.com/doc/refman/8.0/en/storage-en... 从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么搞都是白搭。
- 方法不是 public 的:来自 Spring 官方文档【When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.】@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
- 没有被 Spring 管理:// @Service - 这里被注释掉了 public class OrderServiceImpl implements OrderService { @Transactional public void placeOrder(Order order) { // ... } }。
- 数据源没有配置事务管理器:一般来自于自研的数据库路由组件@Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
- 异常被吞了。catch 后直接吃了,事务异常无法回滚。同时要配置上对应的异常@Transactional(rollbackFor = Exception.class)
五、总结:学习经验
很多类似这样的技术问题,都是来自于小傅哥对源码的学习,最开始是遇到问题的时候去翻看源码,虽然很多时候也很难把整个逻辑捋顺,但一点点的积累确实会让研发人员对技术有更加夯实的认知。
那么在现在我之所以去手写Spring、手写Mybatis,也是希望通过把这样的知识全部整理处理,从中学习复杂逻辑的设计方案、设计原则和如何运用设计模式解决复杂场景的问题。PS:通常我们的业务代码复杂度很难到这个程度,所以在见过”天“后,以后所承接的业务就很容易做设计了。
另外就是对各类技术细节的把控,以及积累于这样的经验把相关技术设计运用到一些类似 SpringBoot Starter 等的开发,只有类似这样的广度、高度、深度,才能真的把个人的研发能力提升起来。