作者:小傅哥
博客:https://bugstack.cn
积淀、分享、成长,让本人和别人都能有所播种!😄
一、前言:一个 Bug
没想到一个 Bug,居然搞我两次!
我大抵是卷上瘾了,横竖都睡不着,坐起来身来关上 Mac 和外接显示器,这 Bug 没有由来,默然看着打印异样的屏幕,一个是我的,另外一个也是我的。
最近可能是卷源码,卷上瘾了。先是《手写 Spring》,再是《手写 Mybatis》,但没想到一个小问题居然搞了我 2 次!
明天这个问题次要体现在大家平时用的 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 查问工具中执行
- 这里的链接获取,最开始没有 if null 的判断,每次都是间接获取链接,所以这种非一个链接下的两条 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 等的开发,只有相似这样的广度、高度、深度,能力真的把集体的研发能力晋升起来。PS:也是为了在技术的路上走的更远,无论是高级开发、架构师、CTO!