Skip to content

CAS数据源事务管理与MyBatis跨版本演进:从commons-dbcp到dbcp2的三代配置变迁

作者: 必码 | bima.cc


前言

在企业级单点登录(SSO)基础设施的构建过程中,Apereo CAS(Central Authentication Service)的数据层配置往往是最容易被忽视、却又最容易出现问题的环节。当开发者将大量精力投入到认证流程定制、协议对接、票据管理等"高光"领域时,数据源连接池的参数调优、事务管理策略的选择、MyBatis集成方案的版本适配等"底层"工作,往往被简化为几行XML配置或YAML属性。然而,正是这些看似平凡的配置细节,决定了CAS系统在高并发登录场景下的稳定性、在版本升级过程中的兼容性、以及在生产环境中的可观测性。

纵观CAS从5.3.x到6.6.x再到7.3.x的版本演进历程,spring-common.xml数据源配置文件经历了三代根本性的变革。这不仅仅是连接池从commons-dbcp 1.4到commons-dbcp2的简单替换,更是一场涉及事务管理策略、MyBatis集成方案、Mapper扫描机制、Spring配置范式的全面演进。从5.3版本的"粗粒度AOP事务切面+全包扫描",到6.6版本的"精细化事务切面+精准Mapper扫描",再到7.3版本的"去XML化+组件扫描+连接池升级"——每一代变更都深刻反映了Spring生态的演进方向和CAS自身架构理念的升级。

为什么数据源事务管理这个话题值得深入探讨? 因为在实际生产环境中,以下问题频繁出现:

  • 连接泄漏导致的服务不可用:CAS在高并发登录场景下,如果连接池配置不当或事务管理存在缺陷,极易出现连接泄漏,最终导致数据库连接耗尽、认证服务完全不可用。
  • 版本升级导致的兼容性问题:从mybatis-spring 1.3.1升级到3.0.3,从commons-dbcp 1.4升级到commons-dbcp2,看似简单的版本号变更,实际上涉及大量属性名变更、API废弃和配置范式转换。如果缺乏对这些变更的深入理解,升级过程可能引入难以排查的隐性Bug。
  • 事务传播行为选择不当导致的数据不一致:CAS的票据注册表(Ticket Registry)操作需要在严格的事务保障下进行。如果事务传播行为配置不当,可能导致票据状态不一致,进而影响SSO会话管理。
  • Mapper扫描范围过大导致的启动缓慢:在大型项目中,不合理的Mapper扫描范围可能导致Spring容器启动时加载大量无关的Bean,严重影响启动速度和内存占用。

本文将基于真实的CAS Overlay项目源码(覆盖5.3.x、6.6.x、7.3.x三个版本线),从数据源连接池配置出发,逐一剖析每一代配置方案的设计理念、实现细节和演进逻辑。所有代码示例均经过教学化处理,只展示核心片段,旨在帮助读者建立对CAS数据层配置的系统性认知,而非提供可直接复制的模板。

无论你是正在评估CAS技术方案的技术决策者,还是负责CAS实施落地的开发工程师,亦或是需要将现有CAS系统从旧版本迁移到新版本的基础架构团队,本文都将为你提供有价值的参考。


第一章 spring-common.xml数据源配置三代演进

1.1 配置文件在CAS Overlay中的角色定位

在CAS Overlay项目中,spring-common.xml是Spring IoC容器的核心配置文件之一。它通常位于src/main/resources/目录下,负责定义数据源、事务管理器、MyBatis集成等基础设施工具Bean。CAS通过Spring的ContextLoaderListenerAnnotationConfigServletWebServerApplicationContext加载这个文件,将其中的Bean定义注册到Spring容器中。

理解spring-common.xml的加载机制,有助于我们理解为什么三个版本中的配置差异如此之大。在CAS 5.3.x中,Spring Boot的自动配置能力尚不成熟,大量基础设施Bean需要通过XML显式声明。到了CAS 6.6.x,虽然Spring Boot的自动配置已经相当完善,但CAS自身的一些特殊需求(如自定义事务管理器名称、特定的Mapper扫描策略)仍然需要XML配置来补充。而到了CAS 7.3.x,Spring Boot 3.x的自动配置能力已经非常强大,spring-common.xml的职责被大幅简化,许多配置项可以完全交给自动配置来处理。

三个版本中spring-common.xml的配置量变化也反映了这一趋势:5.3版本的配置文件通常在100-150行左右,6.6版本在80-120行左右,而7.3版本则缩减到30-50行左右。配置量的减少并不意味着功能的弱化,恰恰相反——它反映了框架层面对基础设施管理的成熟度提升。

1.2 第一代:CAS 5.3.x的"粗粒度"配置

CAS 5.3.x版本的数据源配置代表了"传统Spring XML配置"的典型风格。这一代配置诞生于Spring Boot 2.x早期阶段,彼时Spring Boot的自动配置能力尚不够成熟,大量基础设施Bean仍然需要通过XML显式声明。这一代配置的特点可以概括为:全包扫描、粗粒度事务切面、依赖Dubbo XML命名空间。在那个时代,XML配置是Java企业级开发的主流范式,开发者对XML的"繁琐"习以为常,甚至认为XML配置的显式性本身就是一种优势——所有Bean的定义和依赖关系都一目了然,不存在"魔法般"的自动装配带来的隐式行为。

然而,从今天的视角回看,5.3版本的配置方式存在几个显著的问题:配置量大、维护成本高、容易出错。一个典型的CAS 5.3 Overlay项目的spring-common.xml文件通常包含100到150行配置代码,涵盖了数据源、事务管理器、AOP切面、Mapper扫描、Dubbo引用等多个维度的配置。当项目规模增长、配置项增多时,这个文件会变得越来越臃肿,成为团队协作中的"热点文件"——多人同时修改时容易产生合并冲突,配置项之间的依赖关系也变得难以理清。

数据源配置

5.3版本使用Apache Commons DBCP 1.4作为连接池实现。DBCP 1.4是一个成熟稳定的连接池,虽然在功能上不如HikariCP等新一代连接池丰富,但在CAS 5.3的时代背景下,它是与Spring 4.x兼容性最好的选择之一。

xml
<!-- 教学示例 - CAS 5.3 数据源配置(简化版) -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
      destroy-method="close">
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <property name="maxActive" value="50" />
    <property name="maxIdle" value="20" />
    <property name="maxWait" value="10000" />
    <property name="defaultAutoCommit" value="false" />
    <property name="testWhileIdle" value="true" />
    <property name="validationQuery" value="SELECT 1" />
    <property name="removeAbandoned" value="true" />
    <property name="removeAbandonedTimeout" value="300" />
</bean>

这段配置展示了DBCP 1.4的典型参数设置。其中maxActive控制最大活动连接数,maxIdle控制最大空闲连接数,maxWait控制获取连接的最大等待时间(毫秒)。removeAbandonedremoveAbandonedTimeout的组合用于检测和回收被泄漏的连接——当某个连接被借出超过300秒仍未归还时,连接池会强制回收它。testWhileIdle配合validationQuery则确保空闲连接在被复用前仍然有效。

事务管理器配置

5.3版本的事务管理器配置相对简单直接:

xml
<!-- 教学示例 - CAS 5.3 事务管理器 -->
<bean id="transactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

这里使用的是Spring JDBC提供的DataSourceTransactionManager,它通过数据源获取数据库连接,并利用JDBC原生的提交/回滚机制来管理事务。这种事务管理器的特点是轻量级、与具体ORM框架解耦,适合与MyBatis等SQL映射框架配合使用。

AOP事务切面配置

5.3版本的事务切面配置采用了"一刀切"的策略——所有Service层方法都使用REQUIRED传播行为:

xml
<!-- 教学示例 - CAS 5.3 AOP事务切面 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" propagation="REQUIRED" />
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:advisor advice-ref="txAdvice"
                 pointcut="execution(* cc.bima.cas.service..*.*(..))" />
</aop:config>

这种配置的含义是:对cc.bima.cas.service包及其子包下所有类的所有方法,统一应用REQUIRED事务传播行为。REQUIRED的含义是:如果当前存在事务,则加入该事务;如果不存在事务,则新建一个事务。

这种"一刀切"策略的优点是配置简单、不容易遗漏。但缺点也非常明显:查询方法也会被包裹在事务中,虽然对数据一致性没有影响,但会增加数据库连接的占用时间,降低系统吞吐量。在低并发场景下,这种影响可以忽略不计;但在高并发登录场景下,每个查询操作都占用一个数据库连接,可能导致连接池耗尽。

Mapper扫描配置

5.3版本的Mapper扫描采用了最宽泛的范围:

xml
<!-- 教学示例 - CAS 5.3 Mapper扫描 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="cc.bima.cas" />
</bean>

basePackage设置为cc.bima.cas,意味着MyBatis会扫描这个包及其所有子包下的接口,尝试将它们注册为Mapper Bean。这种宽泛的扫描范围在项目初期可能不会带来问题,但随着项目规模的增长,cc.bima.cas包下可能包含大量非Mapper接口(如Service接口、工具类接口等),MyBatis会尝试将所有这些接口都作为Mapper来处理,虽然最终只有标注了特定注解或对应XML映射文件的接口才会被成功注册,但这个过程会消耗额外的启动时间和内存。

Dubbo XML命名空间

5.3版本的spring-common.xml中还包含了Dubbo的XML命名空间声明:

xml
<!-- 教学示例 - CAS 5.3 Dubbo命名空间声明 -->
<beans xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       ...>

这表明CAS 5.3版本的项目中集成了Apache Dubbo作为RPC框架,用于与其他微服务进行通信。Dubbo的XML命名空间声明虽然不直接参与数据源配置,但它的存在增加了配置文件的复杂度,也反映了当时项目架构的"微服务化"倾向。

1.3 第二代:CAS 6.6.x的"精细化"配置

CAS 6.6.x版本的数据源配置在5.3的基础上进行了显著的精细化改进。如果说5.3版本是"大刀阔斧"式的粗放配置,那么6.6版本就是"精雕细琢"式的精准配置。这一代配置的改进并非一蹴而就,而是基于5.3版本在生产环境中积累的大量运维经验和问题反馈。许多改进点都源于真实的线上故障——例如,全包扫描导致的启动缓慢在CI/CD流水线中成为瓶颈,粗粒度事务切面在高并发场景下暴露出的连接池压力问题等。

这一代配置的特点可以概括为:精准Mapper扫描、精细化事务切面、统一事务管理器命名。每一个改进点都对应着5.3版本中一个具体的痛点,体现了"问题驱动"的架构演进思路。

数据源配置

6.6版本仍然使用commons-dbcp 1.4作为连接池,数据源配置与5.3版本基本一致:

xml
<!-- 教学示例 - CAS 6.6 数据源配置(简化版) -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
      destroy-method="close">
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <property name="maxActive" value="50" />
    <property name="maxIdle" value="20" />
    <property name="maxWait" value="10000" />
    <property name="defaultAutoCommit" value="false" />
    <property name="testWhileIdle" value="true" />
    <property name="validationQuery" value="SELECT 1" />
    <property name="removeAbandoned" value="true" />
    <property name="removeAbandonedTimeout" value="300" />
</bean>

虽然连接池实现没有变化,但6.6版本在连接池参数的调优上可能更加精细。例如,maxActive的值可能根据实际并发量进行了调整,removeAbandonedTimeout可能根据业务特点进行了优化。这些调优工作通常基于生产环境的监控数据和压测结果。

事务管理器配置——ticketTransactionManager

6.6版本最显著的变化之一是事务管理器的命名:

xml
<!-- 教学示例 - CAS 6.6 事务管理器 -->
<bean id="ticketTransactionManager"
      class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
</bean>

将事务管理器命名为ticketTransactionManager而非简单的transactionManager,这是一个非常有意义的命名策略。在CAS的架构中,票据(Ticket)是核心概念——TGT(Ticket Granting Ticket)、ST(Service Ticket)、PGT(Proxy Granting Ticket)等票据类型构成了SSO会话管理的基础。将事务管理器命名为ticketTransactionManager,明确表达了其职责范围:它是为票据相关操作提供事务保障的管理器。

这种命名策略在CAS多数据源场景中尤为重要。如果项目中同时存在多个数据源(例如CAS票据数据源和业务数据源),每个数据源都需要独立的事务管理器,清晰的命名可以避免Bean注入时的歧义。

精细化AOP事务切面

6.6版本对事务切面进行了精细化改进,根据方法名前缀区分读写操作:

xml
<!-- 教学示例 - CAS 6.6 精细化AOP事务切面 -->
<tx:advice id="txAdvice" transaction-manager="ticketTransactionManager">
    <tx:attributes>
        <tx:method name="save*" propagation="REQUIRED" />
        <tx:method name="update*" propagation="REQUIRED" />
        <tx:method name="delete*" propagation="REQUIRED" />
        <tx:method name="*" propagation="SUPPORTS" read-only="true" />
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:advisor advice-ref="txAdvice"
                 pointcut="execution(* cc.bima.cas.service..*.*(..))" />
</aop:config>

这段配置的核心思想是"写操作强事务,读操作弱事务":

  • save*update*delete*方法使用REQUIRED传播行为,确保写操作在事务的保护下执行。
  • 其余所有方法(通常是查询方法)使用SUPPORTS传播行为,并标记为read-onlySUPPORTS的含义是:如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。配合read-only标记,数据库驱动可以进行额外的优化(如MySQL的InnoDB引擎会跳过事务日志的写入)。

这种精细化配置相比5.3版本的"一刀切"策略,在高并发场景下可以显著减少数据库连接的占用时间。查询操作不再强制创建事务,数据库连接可以被更快地释放回连接池,从而提高系统的整体吞吐量。

精准Mapper扫描

6.6版本的Mapper扫描配置更加精准:

xml
<!-- 教学示例 - CAS 6.6 精准Mapper扫描 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="cc.bima.cas.dao" />
    <property name="annotationClass"
              value="org.apache.ibatis.annotations.Mapper" />
</bean>

相比5.3版本,6.6版本做了两个关键改进:

  1. 缩小扫描范围basePackagecc.bima.cas缩小到cc.bima.cas.dao,只扫描DAO层的包。这避免了MyBatis尝试处理Service层、Controller层等非Mapper接口,减少了启动时的无效扫描。

  2. 添加注解过滤annotationClass指定为@Mapper注解,意味着只有标注了@Mapper注解的接口才会被注册为Mapper Bean。这提供了双重保障——即使cc.bima.cas.dao包下存在非Mapper接口,只要它们没有标注@Mapper注解,就不会被MyBatis处理。

这种精准扫描策略在大型项目中尤为重要。假设cc.bima.cas包下有200个接口,其中只有30个是Mapper接口,5.3版本的宽泛扫描需要检查所有200个接口,而6.6版本的精准扫描只需要检查cc.bima.cas.dao包下标注了@Mapper的接口,启动效率的提升是显而易见的。

1.4 第三代:CAS 7.3.x的"极简化"配置

CAS 7.3.x版本的数据源配置代表了"Spring Boot 3.x时代"的配置范式。这一代配置的变革力度是三代中最大的——它不仅仅是参数调整或范围缩小,而是从根本上重新定义了数据源配置的哲学。如果说5.3版本是"我告诉你怎么做",6.6版本是"我帮你做得更好",那么7.3版本就是"框架已经知道怎么做,你只需要在需要的时候微调"。

这种哲学转变的背后,是Spring Boot 3.x自动配置能力的质的飞跃。Spring Boot 3.x的自动配置引擎经过了数年的打磨,已经能够智能地处理绝大多数常见的数据源配置场景——从连接池类型的选择、到事务管理器的创建、到Mapper接口的扫描注册,几乎都可以通过"约定优于配置"的原则自动完成。开发者只需要在少数特殊场景下进行显式配置,其余工作全部交给框架处理。

这一代配置的特点可以概括为:连接池升级、去AOP化、组件扫描替代显式配置。

数据源配置——commons-dbcp2

7.3版本最显著的变化是连接池从commons-dbcp 1.4升级到了commons-dbcp2:

xml
<!-- 教学示例 - CAS 7.3 数据源配置(简化版) -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
      destroy-method="close">
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
    <property name="maxTotal" value="50" />
    <property name="maxIdle" value="20" />
    <property name="maxWaitMillis" value="10000" />
    <property name="defaultAutoCommit" value="false" />
    <property name="testWhileIdle" value="true" />
    <property name="validationQuery" value="SELECT 1" />
    <property name="removeAbandonedOnBorrow" value="true" />
    <property name="removeAbandonedOnMaintenance" value="true" />
    <property name="removeAbandonedTimeout" value="300" />
</bean>

注意几个关键的属性名变化:

  • maxActive -> maxTotal:更准确地表达了"最大总连接数"的语义
  • maxWait -> maxWaitMillis:明确单位为毫秒,消除了歧义
  • removeAbandoned -> removeAbandonedOnBorrow + removeAbandonedOnMaintenance:将泄漏连接的回收时机细化为"借出时检测"和"维护时检测"两种

这些属性名变化虽然看似微小,但反映了DBCP2对连接池管理语义的精确化。在从DBCP 1.4迁移到DBCP2时,如果直接复制旧配置而不修改属性名,这些属性将被静默忽略,连接池将以默认参数运行——这可能导致生产环境的性能问题。

移除AOP事务切面

7.3版本最激进的变化是完全移除了AOP事务切面配置:

xml
<!-- CAS 7.3 中不再存在以下配置 -->
<!-- <tx:advice ... /> -->
<!-- <aop:config ... /> -->
<!-- <tx:annotation-driven ... /> -->

这意味着7.3版本不再使用XML AOP方式来管理事务。事务管理职责被完全转移到了注解驱动的声明式事务上——即通过@Transactional注解在Service方法上直接声明事务属性。

这种转变的背后有几个重要的驱动力:

  1. Spring Boot 3.x的自动配置能力:Spring Boot 3.x的DataSourceTransactionManagerAutoConfiguration可以自动检测数据源并创建对应的事务管理器,无需手动配置。
  2. 注解事务的精确性@Transactional注解可以精确到方法级别,比XML AOP切面更加灵活。开发者可以为每个方法单独指定传播行为、隔离级别、回滚规则等。
  3. 代码可读性:事务属性直接标注在方法上,阅读代码时可以一目了然地了解方法的事务需求,无需在XML文件和Java代码之间来回切换。

Mapper扫描简化

7.3版本的Mapper扫描配置回归了简洁风格:

xml
<!-- 教学示例 - CAS 7.3 Mapper扫描 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="cc.bima.cas.dao" />
</bean>

相比6.6版本,7.3版本移除了annotationClass属性。这并不意味着扫描精度降低了——在mybatis-spring 3.0.3中,MapperScannerConfigurer的默认行为已经足够智能,它会自动识别MyBatis的Mapper接口(通过检查接口是否有对应的XML映射文件或是否标注了@Mapper注解)。

新增组件扫描

7.3版本新增了Spring组件扫描配置:

xml
<!-- 教学示例 - CAS 7.3 组件扫描 -->
<context:component-scan base-package="cc.bima.cas" />

这个配置使得Spring可以自动发现和注册cc.bima.cas包下的所有@Component@Service@Repository@Controller等注解标注的Bean。在5.3和6.6版本中,这些Bean可能需要通过XML显式声明或通过其他方式注册。7.3版本引入组件扫描,进一步减少了XML配置量,使得配置更加"约定优于配置"。

1.5 三代配置对比总结

为了更直观地展示三代配置的演进,我们通过一张对比表来总结关键差异:

配置维度CAS 5.3.xCAS 6.6.xCAS 7.3.x
连接池commons-dbcp 1.4commons-dbcp 1.4commons-dbcp2
最大连接数属性maxActivemaxActivemaxTotal
等待超时属性maxWaitmaxWaitmaxWaitMillis
泄漏检测removeAbandonedremoveAbandonedremoveAbandonedOnBorrow + removeAbandonedOnMaintenance
事务管理器IDtransactionManagerticketTransactionManagerticketTransactionManager
事务切面全方法REQUIRED按方法名前缀区分读写移除,改用注解
tx:annotation-driven移除
Mapper扫描范围cc.bima.cascc.bima.cas.daocc.bima.cas.dao
Mapper注解过滤@Mapper
组件扫描cc.bima.cas
Dubbo命名空间

从这张表中可以清晰地看到三代配置的演进趋势:从"显式配置一切"到"精准配置关键项"再到"依赖自动配置"。这种演进不仅减少了配置量,更重要的是降低了配置错误的风险——配置越少,出错的可能性越低。


第二章 MyBatis集成方案跨版本差异

2.1 MyBatis版本矩阵

在CAS Overlay项目的三个版本中,MyBatis及其Spring集成模块的版本变化如下:

组件CAS 5.3.xCAS 6.6.xCAS 7.3.x
MyBatis3.5.63.5.63.5.16
mybatis-spring1.3.11.3.13.0.3
Spring Framework5.3.x5.3.x6.2.x
Spring Boot2.7.x2.7.x3.5.x

从版本矩阵中可以观察到两个关键事实:

  1. MyBatis核心版本跨度大:从3.5.6到3.5.16,跨越了10个小版本。虽然MyBatis的版本升级一向以向后兼容著称,但10个小版本的累积变化仍然值得关注。这些变化包括:SQL解析器的性能优化、动态SQL标签的增强、类型处理器的扩展、日志输出的改进等。每一个小版本可能只带来微小的改进,但10个小版本的累积效应是显著的——特别是在性能和稳定性方面。
  2. mybatis-spring版本跨越巨大:从1.3.1到3.0.3,跨越了两个大版本。这是一个Breaking Changes级别的升级,涉及命名空间变更、API重构、自动配置机制变更等。mybatis-spring 2.x引入了对Spring 5.x的原生支持,而3.0.x则完成了对Spring 6.x和Jakarta EE的全面适配。每一次大版本升级都伴随着大量的内部重构,虽然对外暴露的API变化相对有限,但底层实现机制已经焕然一新。

值得注意的是,CAS 5.3.x和6.6.x使用了完全相同的MyBatis版本组合(3.5.6 + mybatis-spring 1.3.1)。这说明在5.3到6.6的迁移过程中,MyBatis集成方案保持了稳定,开发者不需要在MyBatis层面做任何改动。这种稳定性大大降低了5.3到6.6的迁移成本,使得开发者可以将精力集中在CAS核心配置的调整上。而6.6到7.3的迁移则截然不同——mybatis-spring从1.3.1到3.0.3的跨越,要求开发者对MyBatis的Spring集成方案进行全面的审查和适配。

2.2 mybatis-spring 1.3.1到3.0.3的核心变化

mybatis-spring从1.3.1到3.0.3的升级,是整个MyBatis集成方案跨版本差异中最核心的部分。这一升级主要受Spring Framework从5.x到6.x的升级驱动。

命名空间变更

mybatis-spring 3.0.3最大的Breaking Change是Java包名的变更。在1.3.1中,核心类位于org.mybatis.spring包下:

java
// mybatis-spring 1.3.1 的包结构
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.mybatis.spring.SqlSessionTemplate;

在3.0.3中,这些类的包名没有变化,但内部实现进行了大量重构以适配Spring 6.x的API变更。例如,Spring 6.x要求对Null参数进行更严格的处理,mybatis-spring 3.0.3相应地调整了参数解析逻辑。

Jakarta命名空间迁移

mybatis-spring 3.0.3要求运行在Jakarta EE 9+环境中。这意味着所有javax.*的引用都需要替换为jakarta.*。对于CAS 7.3.x项目而言,这个迁移已经由Spring Boot 3.x的依赖管理自动完成,开发者通常不需要手动处理。但如果项目中存在自定义的MyBatis插件或拦截器,需要检查是否引用了javax.persistence等旧命名空间的类。

自动配置增强

mybatis-spring 3.0.3提供了更完善的Spring Boot自动配置支持。在1.3.1时代,开发者通常需要手动配置SqlSessionFactoryBeanMapperScannerConfigurer。而在3.0.3中,如果遵循Spring Boot的约定(如将mapper接口放在特定包下、将XML映射文件放在特定目录下),许多配置可以完全交给自动配置来处理。

当然,在CAS Overlay项目中,由于数据源配置的特殊性(如自定义连接池、特定的事务管理器命名),完全依赖自动配置可能不现实。但mybatis-spring 3.0.3的自动配置能力仍然可以作为兜底方案,减少手动配置的工作量。

2.3 SqlSessionFactory配置差异

SqlSessionFactory是MyBatis的核心对象,它负责创建SqlSession实例,而SqlSession是执行SQL映射语句的入口。三个版本中SqlSessionFactory的配置方式存在显著差异。

CAS 5.3.x的SqlSessionFactory配置

xml
<!-- 教学示例 - CAS 5.3 SqlSessionFactory配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="classpath:mybatis-config.xml" />
    <property name="mapperLocations"
              value="classpath*:mapper/**/*.xml" />
</bean>

5.3版本的配置非常经典:指定数据源、MyBatis全局配置文件位置、Mapper XML文件位置。classpath*:前缀表示在所有classpath路径下搜索匹配的资源,这确保了即使Mapper XML文件分布在多个JAR包中,也能被正确加载。

CAS 6.6.x的SqlSessionFactory配置

6.6版本的SqlSessionFactory配置与5.3基本一致,但可能在configLocation中增加了更多的MyBatis全局配置:

xml
<!-- 教学示例 - CAS 6.6 SqlSessionFactory配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="classpath:mybatis-config.xml" />
    <property name="mapperLocations"
              value="classpath*:mapper/**/*.xml" />
    <property name="typeAliasesPackage" value="cc.bima.cas.entity" />
</bean>

6.6版本新增了typeAliasesPackage属性,指定实体类的包路径。这使得在Mapper XML中可以直接使用类名(如User)而非全限定类名(如cc.bima.cas.entity.User)来引用实体类,简化了XML的编写。

CAS 7.3.x的SqlSessionFactory配置

7.3版本的SqlSessionFactory配置进一步简化:

xml
<!-- 教学示例 - CAS 7.3 SqlSessionFactory配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="mapperLocations"
              value="classpath*:mapper/**/*.xml" />
</bean>

注意7.3版本移除了configLocation属性。这意味着MyBatis的全局配置(如缓存、延迟加载、日志实现等)不再通过独立的mybatis-config.xml文件来管理,而是通过Spring Boot的配置属性(如mybatis.configuration.*)来设置。这种"配置集中化"的趋势与Spring Boot的设计理念一致——尽量减少散落在各处的配置文件,将配置集中在application.yml中。

2.4 Mapper接口与XML映射文件的演变

Mapper接口的演进

在三个版本中,Mapper接口的编写方式基本保持一致,都是定义Java接口并在方法上使用MyBatis的注解(可选)或通过XML映射文件来关联SQL语句。但mybatis-spring版本的变化对Mapper接口的注册方式产生了影响。

在mybatis-spring 1.3.1中,MapperScannerConfigurer默认会将包下所有接口都尝试注册为Mapper。如果某个接口没有对应的XML映射文件且没有使用注解,MyBatis会抛出异常(或根据配置跳过)。这就是为什么6.6版本引入了annotationClass过滤——通过要求Mapper接口标注@Mapper注解,可以明确区分哪些接口是Mapper、哪些不是。

在mybatis-spring 3.0.3中,MapperScannerConfigurer的默认行为更加智能。它会检查接口是否有对应的XML映射文件(通过MapperLocations中配置的路径模式),或者是否标注了@Mapper注解。只有满足至少一个条件的接口才会被注册为Mapper。因此,7.3版本可以安全地移除annotationClass属性。

XML映射文件的位置约定

三个版本中,Mapper XML文件的位置约定也经历了一些变化:

  • 5.3.x:通常放在src/main/resources/mapper/目录下
  • 6.6.x:同样放在src/main/resources/mapper/目录下,但可能增加了子目录来组织不同模块的Mapper
  • 7.3.x:在Spring Boot 3.x的标准约定下,Mapper XML文件可以放在src/main/resources/mapper/目录下,也可以放在src/main/resources/根目录下(通过mybatis.mapper-locations配置属性指定)

2.5 MyBatis全局配置的迁移路径

MyBatis的全局配置(如缓存开关、延迟加载、日志实现等)在三个版本中的配置方式有所不同:

5.3.x和6.6.x:mybatis-config.xml

xml
<!-- 教学示例 - mybatis-config.xml(5.3/6.6版本使用) -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="cacheEnabled" value="true" />
        <setting name="lazyLoadingEnabled" value="false" />
        <setting name="defaultExecutorType" value="REUSE" />
        <setting name="logImpl" value="SLF4J" />
    </settings>
</configuration>

7.3.x:application.yml

yaml
# 教学示例 - application.yml中的MyBatis配置(7.3版本)
mybatis:
  configuration:
    cache-enabled: true
    lazy-loading-enabled: false
    default-executor-type: REUSE
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
  mapper-locations: classpath*:mapper/**/*.xml

这种从独立XML文件到YAML配置的迁移,使得MyBatis的配置与其他Spring Boot组件的配置集中在一起,便于统一管理。同时,YAML格式天然支持层级结构,使得配置的组织更加清晰。

需要注意的是,mybatis-spring 3.0.3对配置属性的命名进行了规范化。在1.3.1中,MyBatis的配置项使用驼峰命名(如cacheEnabled),而在3.0.3的Spring Boot集成中,配置项使用kebab-case命名(如cache-enabled)。Spring Boot会自动进行驼峰到kebab-case的转换,因此两种命名方式都可以使用,但推荐使用kebab-case以保持一致性。


第三章 数据源连接池配置对比

3.1 commons-dbcp 1.4与commons-dbcp2的架构差异

Apache Commons DBCP(Database Connection Pool)是Java生态中最古老的连接池实现之一。从DBCP 1.4到DBCP2,虽然项目名称相似,但内部实现经历了根本性的重构。DBCP 1.4的最后正式发布可以追溯到2010年前后,而DBCP2的第一个稳定版发布于2014年。在这四年间,Java并发编程领域发生了翻天覆地的变化——java.util.concurrent包的成熟、JVM并发性能的持续优化、多核处理器的普及——这些变化为DBCP2的架构重构提供了技术基础。

理解DBCP 1.4与DBCP2的架构差异,不仅仅是为了完成版本迁移,更是为了理解连接池技术的演进方向,从而在生产环境中做出更明智的配置决策。

线程安全模型

DBCP 1.4使用synchronized关键字来实现线程安全。在高并发场景下,所有对连接池的操作(获取连接、归还连接、维护连接等)都需要获取同一把锁,这成为了一个性能瓶颈。

DBCP2则引入了更细粒度的并发控制机制。它使用java.util.concurrent.locks包中的锁实现(如ReentrantLock),对连接池的不同操作使用不同的锁,大大降低了锁竞争的概率。此外,DBCP2还引入了无锁的连接对象分配策略,在低竞争场景下可以完全避免锁的开销。

连接生命周期管理

DBCP 1.4的连接生命周期管理相对简单:创建连接、放入池中、借出连接、归还连接、销毁连接。连接的有效性检测依赖于testWhileIdlevalidationQuery的组合。

DBCP2在连接生命周期管理上增加了更多的精细控制:

  • 连接年龄限制:通过maxConnLifetimeMillis属性,可以设置连接的最大存活时间。超过这个时间的连接会被主动回收,即使它仍然有效。这可以防止长时间运行的连接积累内存泄漏或状态不一致问题。
  • 连接泄漏追踪:DBCP2提供了更强大的连接泄漏检测机制,可以记录泄漏连接的创建堆栈信息,便于问题排查。
  • 异步连接创建:DBCP2支持在后台异步创建连接,避免在请求高峰期因连接创建延迟导致的请求超时。

对象池实现

DBCP 1.4内部使用org.apache.commons.pool.impl.GenericObjectPool作为对象池实现。DBCP2则升级为org.apache.commons.pool2.impl.GenericObjectPool,后者提供了更丰富的池化策略,包括FIFO和LIFO两种驱逐策略、可配置的驱逐线程、JMX支持等。

3.2 连接池参数映射详解

从DBCP 1.4迁移到DBCP2,最直接的挑战是参数名的变更。以下是完整的参数映射关系:

DBCP 1.4 属性DBCP2 属性说明
maxActivemaxTotal最大活动连接数 -> 最大总连接数
maxIdlemaxIdle最大空闲连接数(名称不变)
minIdleminIdle最小空闲连接数(名称不变)
maxWaitmaxWaitMillis获取连接最大等待时间(明确单位为毫秒)
initialSizeinitialSize初始连接数(名称不变)
removeAbandonedremoveAbandonedOnBorrow借出时检测泄漏连接
removeAbandonedremoveAbandonedOnMaintenance维护时检测泄漏连接
removeAbandonedTimeoutremoveAbandonedTimeout泄漏连接超时时间(名称不变,单位为秒)
logAbandonedlogAbandoned记录泄漏连接的堆栈信息(名称不变)
testOnBorrowtestOnBorrow借出时验证连接有效性(名称不变)
testOnReturntestOnReturn归还时验证连接有效性(名称不变)
testWhileIdletestWhileIdle空闲时验证连接有效性(名称不变)
validationQueryvalidationQuery连接验证SQL(名称不变)
timeBetweenEvictionRunsMillistimeBetweenEvictionRunsMillis驱逐线程运行间隔(名称不变)
numTestsPerEvictionRunnumTestsPerEvictionRun每次驱逐检查的连接数(名称不变)
minEvictableIdleTimeMillisminEvictableIdleTimeMillis连接最小空闲时间(名称不变)

从这张映射表中可以看到,大部分参数名称保持不变,只有少数几个关键参数发生了重命名。其中最需要注意的是maxActivemaxTotal的变更——如果迁移时遗漏了这个属性名变更,连接池将以默认的maxTotal值(通常为8)运行,这在生产环境中几乎必然导致连接不足的问题。

新增参数

DBCP2还引入了一些DBCP 1.4中不存在的新参数:

  • maxConnLifetimeMillis:连接的最大存活时间。超过此时间的连接将被回收,无论其是否空闲。建议设置为数据库连接超时时间的80%左右,例如MySQL的wait_timeout默认为8小时(28800000毫秒),则maxConnLifetimeMillis可以设置为23040000毫秒(约6.4小时)。
  • connectionFactoryClassName:自定义连接工厂的类名。DBCP2允许开发者替换默认的连接工厂,以实现自定义的连接创建逻辑(如动态切换数据源、添加连接级拦截器等)。
  • fastFailValidation:快速失败验证。当设置为true时,如果连接验证失败,连接池会立即丢弃该连接,而不是尝试重新验证。这可以减少验证失败时的延迟。

3.3 连接泄漏检测机制

连接泄漏是数据库连接池管理中最常见也最危险的问题之一。当应用程序从连接池借出一个连接后,由于异常、逻辑错误或资源管理不当,未能正确归还连接,就会导致连接泄漏。随着泄漏的连接越来越多,连接池最终会耗尽,新的数据库请求将无法获取连接,导致服务不可用。

DBCP 1.4的泄漏检测

xml
<!-- 教学示例 - DBCP 1.4 连接泄漏检测 -->
<property name="removeAbandoned" value="true" />
<property name="removeAbandonedTimeout" value="300" />
<property name="logAbandoned" value="true" />

DBCP 1.4的泄漏检测机制相对简单:当removeAbandoned设置为true时,连接池会在每次借出连接时记录时间戳。当一个连接被借出超过removeAbandonedTimeout秒(示例中为300秒,即5分钟)仍未归还时,连接池会强制回收该连接。如果logAbandoned也设置为true,连接池还会记录该连接被借出时的堆栈信息,帮助开发者定位泄漏的源头。

DBCP2的泄漏检测

xml
<!-- 教学示例 - DBCP2 连接泄漏检测 -->
<property name="removeAbandonedOnBorrow" value="true" />
<property name="removeAbandonedOnMaintenance" value="true" />
<property name="removeAbandonedTimeout" value="300" />
<property name="logAbandoned" value="true" />

DBCP2将泄漏检测的触发时机细化为两种:

  1. removeAbandonedOnBorrow:当有新的连接请求到达时,检查是否有已泄漏的连接。这意味着泄漏连接的检测是"被动"的——只有在新请求需要连接时才会触发检测。
  2. removeAbandonedOnMaintenance:由连接池的后台维护线程定期检查。这意味着即使没有新的连接请求,泄漏连接也会被定期清理。

两种机制可以同时启用,也可以只启用其中一种。在生产环境中,建议同时启用两种机制,以确保泄漏连接能够被及时发现和清理。

泄漏检测的注意事项

连接泄漏检测虽然是一个有用的安全网,但它不应该被视为连接管理的"常规手段"。如果生产环境中频繁触发泄漏检测,说明应用程序存在连接管理方面的Bug,应该从根本上修复,而不是依赖连接池的泄漏回收机制。

此外,removeAbandonedTimeout的值需要根据业务特点谨慎设置。如果设置得太小(如30秒),可能会误杀正常的长时间运行的事务。如果设置得太大(如3600秒),泄漏连接占用连接池的时间过长,可能导致连接池耗尽。在CAS场景中,认证操作通常在毫秒级完成,300秒的超时设置已经非常宽松。

3.4 连接验证策略

连接验证是确保从连接池中取出的连接仍然有效的机制。数据库连接可能因为网络中断、数据库重启、防火墙超时等原因而失效。如果应用程序使用了一个已失效的连接,将会抛出异常,可能导致事务回滚或服务中断。

testOnBorrow vs testWhileIdle

xml
<!-- 教学示例 - 连接验证配置 -->
<property name="testOnBorrow" value="false" />
<property name="testWhileIdle" value="true" />
<property name="validationQuery" value="SELECT 1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />

testOnBorrow在每次借出连接时执行验证查询。这种方式可以确保每次取出的连接都是有效的,但性能开销较大——每次获取连接都需要执行一次数据库查询。在高并发场景下,这会成为性能瓶颈。

testWhileIdle在连接空闲时执行验证查询。这种方式不会影响正常请求的性能,但存在一个时间窗口——一个连接可能在空闲验证通过后、被借出前失效。不过,这个时间窗口通常非常短(几秒到几十秒),在实际生产中几乎不会造成问题。

推荐策略是关闭testOnBorrow,开启testWhileIdle,配合合理的timeBetweenEvictionRunsMillis(建议30-60秒)和validationQuery。这种策略在性能和可靠性之间取得了良好的平衡。

validationQuery的选择

validationQuery应该选择一条轻量级的SQL语句,能够快速验证连接的有效性。不同数据库的推荐值:

  • MySQL/MariaDB:SELECT 1
  • PostgreSQL:SELECT 1
  • Oracle:SELECT 1 FROM DUAL
  • SQL Server:SELECT 1

在DBCP2中,还可以使用validationQueryTimeout属性来设置验证查询的超时时间,防止验证查询本身因数据库问题而长时间阻塞。

3.5 连接池性能调优参数

除了基本的连接数和超时参数外,还有一些影响连接池性能的高级参数:

驱逐策略

xml
<!-- 教学示例 - DBCP2 驱逐策略配置 -->
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="numTestsPerEvictionRun" value="10" />
  • timeBetweenEvictionRunsMillis:后台驱逐线程的运行间隔。设置为60000毫秒(60秒)意味着每分钟检查一次连接池。
  • minEvictableIdleTimeMillis:连接在池中空闲的最小时间。超过此时间的空闲连接将被驱逐。设置为300000毫秒(5分钟)意味着空闲超过5分钟的连接将被回收。
  • numTestsPerEvictionRun:每次驱逐检查时检查的连接数。这个值不需要设置得太大,通常设置为maxTotal的1/5到1/3即可。

初始连接数

xml
<property name="initialSize" value="10" />

initialSize指定连接池启动时创建的初始连接数。在CAS场景中,服务启动后立即就会面临认证请求,如果初始连接数为0,第一批请求需要等待连接创建,可能导致启动后的短暂延迟。建议将initialSize设置为minIdle的值,确保启动时就有足够的连接可用。


第四章 事务管理策略对比

4.1 XML AOP事务切面 vs 声明式事务注解

CAS三个版本中,事务管理方式经历了从"XML AOP切面"到"注解驱动"的演进。理解这两种方式的差异,有助于在版本迁移时做出正确的选择,也有助于在新的项目中根据团队特点和项目规模选择最合适的事务管理策略。

事务管理是数据访问层的核心基础设施,它直接关系到数据的一致性和系统的可靠性。选择错误的事务管理方式,轻则导致性能下降,重则导致数据不一致、服务不可用。因此,深入理解XML AOP切面和声明式事务注解的优劣,是每个CAS开发者必备的知识。

XML AOP事务切面(5.3.x / 6.6.x)

XML AOP事务切面通过Spring的AOP(面向切面编程)机制,将事务管理逻辑"织入"到目标方法上。开发者不需要在Java代码中编写任何事务相关的代码,所有事务属性都在XML中声明。

xml
<!-- 教学示例 - XML AOP事务切面 -->
<tx:advice id="txAdvice" transaction-manager="ticketTransactionManager">
    <tx:attributes>
        <tx:method name="save*" propagation="REQUIRED" />
        <tx:method name="update*" propagation="REQUIRED" />
        <tx:method name="delete*" propagation="REQUIRED" />
        <tx:method name="*" propagation="SUPPORTS" read-only="true" />
    </tx:attributes>
</tx:advice>

<aop:config>
    <aop:advisor advice-ref="txAdvice"
                 pointcut="execution(* cc.bima.cas.service..*.*(..))" />
</aop:config>

<tx:annotation-driven
    transaction-manager="ticketTransactionManager" />

这种方式的优点:

  1. 集中管理:所有事务属性集中在一个XML文件中,便于全局查看和修改。
  2. 非侵入式:Java代码不需要引入Spring事务相关的注解,保持了代码的"纯净性"。
  3. 统一规则:通过方法名前缀匹配,可以方便地为一批方法应用相同的事务属性。

这种方式的缺点:

  1. 配置与代码分离:事务属性定义在XML中,但方法定义在Java代码中,两者之间的映射关系不够直观。开发者需要同时查看XML和Java代码才能理解某个方法的事务行为。
  2. 方法名约定依赖:事务属性的匹配依赖于方法名前缀(如save*update*)。如果方法命名不规范(如addUser而非saveUser),可能导致事务属性不匹配。
  3. 灵活性不足:XML AOP切面只能基于方法名模式进行匹配,无法为特定的方法设置特殊的事务属性。如果某个查询方法需要写事务,XML方式很难优雅地处理。

声明式事务注解(7.3.x推荐)

声明式事务注解通过@Transactional注解直接在Java方法上声明事务属性:

java
// 教学示例 - @Transactional注解声明事务
@Service
public class TicketServiceImpl implements TicketService {

    @Transactional(propagation = Propagation.REQUIRED)
    public void saveTicketGrantingTicket(TicketGrantingTicket tgt) {
        // 保存TGT的逻辑
    }

    @Transactional(propagation = Propagation.REQUIRED)
    public void updateTicketGrantingTicket(TicketGrantingTicket tgt) {
        // 更新TGT的逻辑
    }

    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public TicketGrantingTicket getTicketGrantingTicket(String ticketId) {
        // 查询TGT的逻辑
        return null;
    }
}

这种方式的优点:

  1. 直观清晰:事务属性直接标注在方法上,阅读代码时可以一目了然。
  2. 精确控制:每个方法可以独立配置事务属性,不受方法名约定的限制。
  3. IDE支持好:现代IDE可以识别@Transactional注解,提供代码提示和警告。

这种方式的缺点:

  1. 分散管理:事务属性分散在各个Service类中,全局查看需要搜索所有文件。
  2. 代码侵入:Java代码需要引入Spring事务相关的注解,增加了对Spring框架的耦合。
  3. 容易遗漏:新增方法时可能忘记添加@Transactional注解,导致方法在无事务保护的情况下执行。

混合模式(5.3.x / 6.6.x实际使用)

值得注意的是,5.3和6.6版本实际上采用了XML AOP切面和@Transactional注解的混合模式。XML中的<tx:annotation-driven>标签启用了注解驱动的事务管理,这意味着开发者可以在Java代码中使用@Transactional注解来覆盖XML中的默认事务属性。

这种混合模式在项目实践中非常实用:XML AOP切面提供默认的事务属性(如所有Service方法默认使用REQUIRED传播行为),而@Transactional注解则用于特殊场景的覆盖(如某个查询方法需要特殊的事务隔离级别)。

4.2 ticketTransactionManager的命名意义

在CAS 6.6.x和7.3.x版本中,事务管理器被命名为ticketTransactionManager。这个命名并非随意为之,而是有着深刻的架构考量。

CAS的多数据源场景

在大型企业环境中,CAS可能需要同时连接多个数据源:

  1. 票据数据源:存储TGT、ST等票据数据
  2. 用户数据源:存储用户凭证、权限信息
  3. 审计数据源:存储登录日志、操作审计记录
  4. 配置数据源:存储服务注册信息、授权策略

每个数据源都需要独立的事务管理器。如果所有事务管理器都命名为transactionManager,Spring容器中会出现Bean名称冲突。通过将票据数据源的事务管理器命名为ticketTransactionManager,可以明确区分不同数据源的事务管理器。

@Transactional注解的事务管理器指定

当存在多个事务管理器时,@Transactional注解需要通过transactionManager属性指定使用哪个管理器:

java
// 教学示例 - 指定事务管理器
@Transactional("ticketTransactionManager")
public void saveTicketGrantingTicket(TicketGrantingTicket tgt) {
    // 使用ticketTransactionManager管理事务
}

如果只有一个事务管理器,Spring会自动使用它,无需显式指定。但显式指定可以增强代码的可读性,也便于后续扩展。

CAS内部组件的事务管理器引用

CAS框架内部的许多组件(如JpaTicketRegistryJdbcTicketRegistry等)也依赖事务管理器。这些组件通常通过@Qualifier("ticketTransactionManager")来引用特定的事务管理器。如果事务管理器的名称与CAS内部期望的名称不一致,可能导致自动注入失败。

4.3 事务传播行为的选择

Spring定义了7种事务传播行为(Propagation Behavior),CAS项目中主要使用以下几种:

REQUIRED(默认)

java
// 教学示例 - REQUIRED传播行为
@Transactional(propagation = Propagation.REQUIRED)
public void saveTicketGrantingTicket(TicketGrantingTicket tgt) {
    // 如果当前存在事务,则加入该事务
    // 如果不存在事务,则新建一个事务
}

REQUIRED是最常用的传播行为,也是CAS 5.3和6.6版本中XML AOP切面为写操作指定的传播行为。它的语义是"必须在一个事务中执行"——如果调用方已经开启了事务,则当前方法加入该事务;否则,当前方法会新建一个事务。

在CAS场景中,票据的创建、更新、删除操作都应该使用REQUIRED传播行为。例如,创建TGT时,可能需要同时写入票据表和审计日志表,这两个写操作应该在同一个事务中完成,以保证数据一致性。

SUPPORTS

java
// 教学示例 - SUPPORTS传播行为
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public TicketGrantingTicket getTicketGrantingTicket(String ticketId) {
    // 如果当前存在事务,则加入该事务(以只读方式)
    // 如果不存在事务,则以非事务方式执行
}

SUPPORTS是CAS 6.6版本中XML AOP切面为查询方法指定的传播行为。它的语义是"有事务就加入,没有也无所谓"——如果调用方已经开启了事务,则当前方法加入该事务(以只读方式);否则,当前方法以非事务方式执行。

在CAS场景中,票据查询操作通常不需要事务保护。查询操作不会修改数据,即使在没有事务的情况下执行,也不会影响数据一致性。使用SUPPORTS传播行为可以避免为查询操作创建不必要的事务,减少数据库连接的占用。

REQUIRES_NEW

java
// 教学示例 - REQUIRES_NEW传播行为
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logAuthenticationEvent(AuthenticationEvent event) {
    // 无论当前是否存在事务,都新建一个独立的事务
    // 新事务与调用方事务相互独立
}

REQUIRES_NEW的语义是"总是新建事务"——无论调用方是否已经开启了事务,当前方法都会新建一个独立的事务。如果调用方存在事务,调用方事务会被挂起,直到当前方法的事务提交或回滚后,调用方事务才恢复。

在CAS场景中,REQUIRES_NEW适用于审计日志记录等独立操作。即使认证事务回滚了,审计日志仍然需要被记录。通过使用REQUIRES_NEW,审计日志的写入在独立的事务中完成,不受认证事务状态的影响。

NOT_SUPPORTED

java
// 教学示例 - NOT_SUPPORTED传播行为
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public String generateTicketId() {
    // 以非事务方式执行
    // 如果调用方存在事务,调用方事务会被挂起
}

NOT_SUPPORTED的语义是"不需要事务"——当前方法总是以非事务方式执行。如果调用方存在事务,调用方事务会被挂起。

在CAS场景中,NOT_SUPPORTED适用于票据ID生成等纯计算操作。这些操作不涉及数据库访问,不需要事务保护。使用NOT_SUPPORTED可以避免不必要的数据库连接获取。

4.4 CAS票据注册表的事务保障

CAS的票据注册表(Ticket Registry)是SSO会话管理的核心组件,负责存储和管理TGT(Ticket Granting Ticket)、ST(Service Ticket)、PGT(Proxy Granting Ticket)等票据。票据注册表的所有写操作都必须在严格的事务保障下进行。

票据创建的事务需求

创建TGT是用户登录成功后的关键操作。这个过程通常涉及以下数据库操作:

  1. 在票据表中插入新的TGT记录
  2. 更新用户的最后登录时间
  3. 写入登录审计日志

这三个操作必须在同一个事务中完成。如果TGT插入成功但审计日志写入失败,事务应该回滚,确保数据一致性。如果使用REQUIRED传播行为,这三个操作会被包裹在同一个事务中,任何一步失败都会导致整个事务回滚。

票据验证的事务需求

验证ST是用户访问受保护资源时的关键操作。这个过程涉及:

  1. 读取ST记录
  2. 验证ST是否有效(未过期、未使用)
  3. 标记ST为已使用
  4. 关联ST与TGT

读取操作可以使用SUPPORTS传播行为,但标记ST为已使用的操作必须使用REQUIRED传播行为。在实际项目中,通常将整个验证流程放在一个REQUIRED事务中,以确保读取和写入的一致性。

票据清理的事务需求

CAS的后台清理任务定期清理过期的票据。这个过程涉及:

  1. 查询所有过期的票据
  2. 批量删除过期票据
  3. 更新统计信息

批量删除操作应该使用REQUIRED传播行为。如果清理过程中出现异常,事务回滚可以确保不会误删有效票据。对于大批量的清理操作,可以考虑分批处理,每批使用独立的REQUIRES_NEW事务,以避免单个事务过大导致的性能问题。

ticketTransactionManager与票据注册表的集成

在CAS的JdbcTicketRegistry中,事务管理器的注入方式如下:

java
// 教学示例 - JdbcTicketRegistry中的事务管理器注入
public class JdbcTicketRegistry implements TicketRegistry {

    private final PlatformTransactionManager transactionManager;

    @Autowired
    public JdbcTicketRegistry(
            @Qualifier("ticketTransactionManager")
            PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void addTicket(Ticket ticket) {
        TransactionTemplate txTemplate =
            new TransactionTemplate(transactionManager);
        txTemplate.execute(status -> {
            // 在事务中执行票据添加操作
            return doAddTicket(ticket);
        });
    }
}

通过TransactionTemplate编程式事务管理,JdbcTicketRegistry可以精确控制每个操作的事务边界。这种方式比声明式事务更加灵活,特别适合需要根据运行时条件动态决定事务行为的场景。


第五章 生产环境数据源配置建议

5.1 连接池参数调优

在生产环境中,连接池参数的调优需要基于实际的业务负载特征和数据库能力。这是一个需要反复迭代的过程——没有一套"万能参数"适用于所有场景。调优的起点是理解业务特征(并发量、读写比例、响应时间要求),然后通过监控数据验证调优效果,最终找到适合当前业务场景的最佳参数组合。

以下是基于CAS认证场景的调优建议,涵盖了从参数选择到验证方法的完整流程。

最大连接数(maxTotal)的确定

最大连接数的确定需要考虑以下因素:

  1. 并发认证请求数:CAS的认证请求通常呈现明显的波峰波谷特征——工作日的早晨和下午是登录高峰,夜间是低谷。最大连接数应该能够覆盖高峰期的并发需求。
  2. 数据库服务器的承载能力:每个数据库连接都会占用数据库服务器的内存和线程资源。MySQL默认的最大连接数为151,PostgreSQL默认为100。连接池的maxTotal不应超过数据库服务器的承载能力。
  3. CAS实例数量:如果部署了多个CAS实例(通过负载均衡),每个实例的maxTotal应该相应减少,确保所有实例的总连接数不超过数据库的限制。

推荐的计算公式:

maxTotal = max(并发认证请求数 * 1.5, minIdle * 2)

其中,1.5是安全系数,考虑了连接创建和验证的额外开销。minIdle * 2确保了即使在低负载时,连接池也有足够的缓冲空间。

对于中小型企业(并发认证请求<100),推荐maxTotal设置为30-50。对于大型企业(并发认证请求100-500),推荐maxTotal设置为50-100。

最小空闲连接数(minIdle)的确定

minIdle控制连接池中保持的最小空闲连接数。设置合理的minIdle可以避免请求高峰时因连接创建延迟导致的响应缓慢。

推荐将minIdle设置为maxTotal的20%-40%。例如,maxTotal为50时,minIdle可以设置为10-20。

初始连接数(initialSize)的确定

initialSize控制CAS启动时创建的初始连接数。推荐将initialSize设置为与minIdle相同的值,确保CAS启动后立即有足够的连接可用。

获取连接超时时间(maxWaitMillis)的确定

maxWaitMillis控制当连接池耗尽时,请求等待可用连接的最大时间。在CAS认证场景中,如果用户等待时间过长,可能会放弃登录或重试,导致更多请求涌入。

推荐将maxWaitMillis设置为3000-5000毫秒(3-5秒)。如果3秒内无法获取连接,说明系统已经处于过载状态,快速失败比长时间等待更有利于系统恢复。

5.2 事务隔离级别选择

数据库事务的隔离级别决定了并发事务之间的可见性规则。ANSI SQL定义了四个隔离级别,从低到高依次为:Read Uncommitted、Read Committed、Repeatable Read、Serializable。

CAS场景的隔离级别需求分析

在CAS认证场景中,主要的数据操作包括:

  1. 票据创建:插入新的TGT/ST记录。这是一个纯插入操作,不存在脏读或幻读的风险。
  2. 票据验证:读取ST记录并更新其状态。这涉及到"先读后写"的模式,需要防止两个并发请求同时验证同一个ST。
  3. 票据清理:批量删除过期票据。这是一个批量删除操作,需要防止误删有效票据。

推荐隔离级别:READ_COMMITTED

对于大多数CAS部署场景,READ_COMMITTED隔离级别已经足够。这是大多数数据库的默认隔离级别(Oracle、SQL Server、PostgreSQL),也是MySQL InnoDB引擎的默认隔离级别。

READ_COMMITTED可以防止脏读(一个事务读取到另一个事务未提交的数据),但不防止不可重复读(同一事务中两次读取同一行数据可能得到不同结果)和幻读(同一事务中两次执行相同查询可能得到不同行数)。

在CAS场景中,不可重复读和幻读的影响有限:

  • 票据验证操作通常通过数据库的行级锁(如MySQL的SELECT ... FOR UPDATE)来防止并发修改,不依赖事务隔离级别。
  • 票据清理操作通过时间条件过滤过期票据,即使出现幻读(清理过程中有新票据过期),下一轮清理会处理这些票据。

特殊场景下的隔离级别升级

在某些特殊场景下,可能需要更高的隔离级别:

  • REPEATABLE_READ:如果CAS需要在一个事务中多次读取同一票据的状态(如复杂的票据转移操作),可以使用REPEATABLE_READ来确保读取的一致性。
  • SERIALIZABLE:如果CAS需要执行涉及多张表的复杂操作(如票据迁移、数据修复),可以使用SERIALIZABLE来确保完全的事务隔离。但SERIALIZABLE的性能开销很大,应该谨慎使用。

在Spring中,可以通过@Transactional注解指定隔离级别:

java
// 教学示例 - 指定事务隔离级别
@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED
)
public void saveTicketGrantingTicket(TicketGrantingTicket tgt) {
    // 使用READ_COMMITTED隔离级别
}

5.3 监控与告警

生产环境中的数据源监控是保障CAS服务稳定性的关键环节。以下是基于commons-dbcp2的监控建议。

JMX监控

DBCP2内置了JMX(Java Management Extensions)支持。通过JMX,可以实时监控连接池的运行状态:

  • 活动连接数:当前正在使用的连接数。如果持续接近maxTotal,说明连接池可能不足。
  • 空闲连接数:当前空闲的连接数。如果持续为0,说明所有连接都在被使用,新请求需要等待。
  • 等待线程数:当前等待获取连接的线程数。如果大于0,说明连接池已经耗尽。
  • 连接创建总数:累计创建的连接数。如果持续快速增长,可能存在连接泄漏。
  • 连接泄漏回收数:被泄漏检测机制回收的连接数。如果大于0,说明应用程序存在连接泄漏问题。

在Spring Boot 3.x中,可以通过spring.jmx.enabled=true启用JMX,然后使用JConsole或VisualVM连接到CAS进程查看连接池的MBean。

日志监控

DBCP2可以配置连接池的日志输出,记录关键事件:

xml
<!-- 教学示例 - DBCP2 日志配置 -->
<property name="logAbandoned" value="true" />

logAbandoned设置为true时,DBCP2会在回收泄漏连接时输出警告日志,包含连接被借出时的堆栈信息。这些日志是排查连接泄漏问题的关键线索。

此外,建议在CAS的日志配置中为连接池相关的日志设置合适的级别:

xml
<!-- 教学示例 - Logback连接池日志配置 -->
<logger name="org.apache.commons.dbcp2" level="WARN" />
<logger name="org.apache.commons.pool2" level="WARN" />

在正常运行时,WARN级别可以捕获连接泄漏、连接验证失败等关键事件。在排查问题时,可以临时将日志级别调整为DEBUGTRACE,获取更详细的连接池运行信息。

告警规则建议

基于监控数据,建议配置以下告警规则:

  1. 连接池使用率告警:当活动连接数持续1分钟超过maxTotal的80%时触发告警。这通常意味着系统负载较高,可能需要扩容。
  2. 等待线程数告警:当等待线程数大于0时触发告警。这意味着连接池已经耗尽,新的请求正在排队等待。
  3. 连接泄漏告警:当连接泄漏回收数大于0时触发告警。这意味着应用程序存在连接管理Bug,需要立即排查。
  4. 连接验证失败告警:当连接验证失败次数持续增长时触发告警。这可能意味着数据库或网络存在问题。

5.4 高可用场景下的数据源配置

在CAS的高可用部署中,数据源的配置需要考虑数据库层面的高可用方案。

数据库读写分离

如果数据库采用了读写分离架构(如MySQL的主从复制),CAS的数据源配置需要适配:

xml
<!-- 教学示例 - 读写分离数据源配置 -->
<bean id="masterDataSource" class="org.apache.commons.dbcp2.BasicDataSource"
      destroy-method="close">
    <property name="url" value="${jdbc.master.url}" />
    <!-- 主库配置 -->
</bean>

<bean id="slaveDataSource" class="org.apache.commons.dbcp2.BasicDataSource"
      destroy-method="close">
    <property name="url" value="${jdbc.slave.url}" />
    <!-- 从库配置 -->
</bean>

在实际项目中,读写分离通常通过中间件(如MyCat、ShardingSphere)或Spring的AbstractRoutingDataSource来实现。CAS的票据写操作(创建、更新、删除)路由到主库,查询操作路由到从库。

需要注意的是,读写分离引入了主从复制延迟的问题。在极端情况下,用户刚创建的TGT可能尚未同步到从库,导致后续的ST验证失败。因此,对于需要强一致性的操作(如票据验证),应该强制路由到主库。

数据库连接故障转移

DBCP2本身不提供数据库连接的故障转移能力。如果需要支持数据库故障转移,可以考虑以下方案:

  1. 数据库中间件:使用ProxySQL、MaxScale等数据库中间件,在中间件层面实现故障转移。
  2. JDBC驱动层故障转移:某些JDBC驱动支持多主机URL(如MySQL的jdbc:mysql://host1,host2,host3/database),驱动会自动尝试连接下一个主机。
  3. Spring AbstractRoutingDataSource:通过自定义的数据源路由策略,在检测到主库不可用时自动切换到备用库。

5.5 容器化环境下的特殊考虑

在Docker/Kubernetes环境中部署CAS时,数据源配置需要考虑容器化带来的特殊挑战。

连接池参数与容器资源限制

在Kubernetes中,容器的CPU和内存资源通常会被限制(通过resources.limits)。连接池的maxTotal应该与容器的资源限制相匹配——如果容器只有1GB内存,设置maxTotal为200可能会导致OOM(Out of Memory)。

推荐的连接池内存估算:每个数据库连接大约占用1-5MB内存(取决于数据库驱动和配置)。如果maxTotal为50,连接池本身大约需要50-250MB内存。

健康检查与连接验证

Kubernetes的健康检查(Liveness Probe和Readiness Probe)需要考虑连接池的状态。如果数据库不可用,连接池可能处于"等待"状态,CAS的健康检查应该能够检测到这种情况并返回不健康状态。

建议在CAS的健康检查端点中包含数据源连接测试:

java
// 教学示例 - 数据源健康检查
@Component
public class DataSourceHealthIndicator implements HealthIndicator {

    @Autowired
    private DataSource dataSource;

    @Override
    public Health health() {
        try (Connection conn = dataSource.getConnection()) {
            if (conn.isValid(3)) {
                return Health.up().build();
            }
        } catch (SQLException e) {
            return Health.down(e).build();
        }
        return Health.down().build();
    }
}

优雅关闭与连接清理

在Kubernetes中,Pod终止时会发送SIGTERM信号,应用需要在收到信号后优雅关闭。对于数据源而言,优雅关闭意味着:

  1. 停止接受新的请求
  2. 等待正在执行的事务完成
  3. 关闭所有数据库连接
  4. 释放连接池资源

DBCP2的close()方法会等待所有活动连接归还后才关闭连接池。可以通过maxWaitMillis控制等待时间,避免关闭过程无限期阻塞。

xml
<!-- 教学示例 - DBCP2 优雅关闭配置 -->
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
      destroy-method="close">
    <!-- ... 其他配置 ... -->
    <property name="maxWaitMillis" value="5000" />
</bean>

第六章 版本迁移实战指南

6.1 从5.3.x迁移到6.6.x的数据源变更

从CAS 5.3.x迁移到6.6.x,数据源层面的变更相对温和。正如我们在版本矩阵中提到的,5.3和6.6使用了完全相同的MyBatis版本组合,连接池实现也没有变化。因此,这次迁移的核心工作集中在配置层面的优化和规范化上,而非底层技术的替换。

以下是迁移检查清单,每一项都标注了优先级和风险等级,帮助开发者合理规划迁移工作:

连接池配置

连接池实现没有变化(仍然是commons-dbcp 1.4),配置参数保持不变。但建议在迁移时审查以下参数:

  • maxActive:根据6.6版本的并发量特征重新评估
  • removeAbandonedTimeout:确保超时时间仍然合理
  • validationQuery:如果数据库版本升级了,验证SQL是否仍然有效

事务管理器

将事务管理器的ID从transactionManager改为ticketTransactionManager。这个变更需要注意:

  1. 检查所有@Transactional注解,确保没有通过transactionManager属性显式引用旧的事务管理器名称。
  2. 检查CAS内部组件的配置,确保它们能够正确引用ticketTransactionManager
  3. 如果项目中存在自定义的事务管理器引用(如TransactionTemplate),需要更新引用名称。

事务切面

将事务切面从"全方法REQUIRED"改为"按方法名前缀区分读写"。这个变更需要注意:

  1. 审查所有Service方法的方法名,确保写操作以saveupdatedelete开头,读操作不以这些前缀开头。
  2. 如果存在不遵循命名约定的方法(如addUserremoveTicket),需要重命名方法或使用@Transactional注解覆盖默认行为。
  3. 测试所有关键业务流程,确保事务行为符合预期。

Mapper扫描

将Mapper扫描范围从cc.bima.cas缩小到cc.bima.cas.dao,并添加@Mapper注解过滤。这个变更需要注意:

  1. 确保所有Mapper接口都位于cc.bima.cas.dao包或其子包下。如果存在位于其他包中的Mapper接口,需要将其移动。
  2. 确保所有Mapper接口都标注了@Mapper注解。在6.6版本中,没有@Mapper注解的接口不会被注册为Mapper Bean。
  3. 检查是否有非Mapper接口位于cc.bima.cas.dao包下。如果有,确保它们没有标注@Mapper注解。

6.2 从6.6.x迁移到7.3.x的数据源变更

从CAS 6.6.x迁移到7.3.x,数据源层面的变更最为剧烈。这次迁移涉及连接池实现替换、MyBatis Spring模块大版本升级、事务管理方式变更等多个维度的根本性变化。如果说5.3到6.6的迁移是"装修",那么6.6到7.3的迁移就是"重建"。

建议将这次迁移分为三个阶段:第一阶段完成连接池升级和属性名变更,确保基本的数据源功能正常;第二阶段完成MyBatis Spring模块升级,确保Mapper接口和SQL映射正常工作;第三阶段完成事务管理方式变更,从XML AOP切面迁移到注解驱动。每个阶段完成后都应进行充分的测试验证,确保上阶段的成果不会在下阶段被破坏。

以下是迁移检查清单:

连接池升级

从commons-dbcp 1.4升级到commons-dbcp2,需要修改以下内容:

  1. Bean类名变更org.apache.commons.dbcp.BasicDataSource -> org.apache.commons.dbcp2.BasicDataSource
  2. 属性名变更maxActive -> maxTotalmaxWait -> maxWaitMillis
  3. 泄漏检测属性变更removeAbandoned -> removeAbandonedOnBorrow + removeAbandonedOnMaintenance
  4. 依赖变更:在Gradle/Maven中,将commons-dbcp:commons-dbcp替换为org.apache.commons:commons-dbcp2
groovy
// 教学示例 - Gradle依赖变更
// 旧版本(6.6)
// implementation 'commons-dbcp:commons-dbcp:1.4'

// 新版本(7.3)
implementation 'org.apache.commons:commons-dbcp2:2.10.0'

MyBatis Spring模块升级

从mybatis-spring 1.3.1升级到3.0.3,需要修改以下内容:

  1. 依赖变更:在Gradle/Maven中更新mybatis-spring的版本号
  2. Jakarta命名空间:确保所有javax.*的引用已替换为jakarta.*
  3. 自动配置:检查mybatis-spring 3.0.3的自动配置是否与手动配置冲突
groovy
// 教学示例 - Gradle依赖变更
// 旧版本(6.6)
// implementation 'org.mybatis:mybatis:3.5.6'
// implementation 'org.mybatis:mybatis-spring:1.3.1'

// 新版本(7.3)
implementation 'org.mybatis:mybatis:3.5.16'
implementation 'org.mybatis:mybatis-spring:3.0.3'

事务管理方式变更

从XML AOP事务切面迁移到注解驱动事务,需要修改以下内容:

  1. 移除XML事务配置:删除<tx:advice><aop:config><tx:annotation-driven>等XML配置
  2. 添加@Transactional注解:在所有需要事务保护的Service方法上添加@Transactional注解
  3. 确保事务管理器自动配置:确认Spring Boot能够自动创建ticketTransactionManager,或者通过@Bean方法手动注册
java
// 教学示例 - 7.3版本手动注册事务管理器
@Configuration
public class DataSourceConfig {

    @Bean("ticketTransactionManager")
    public PlatformTransactionManager ticketTransactionManager(
            DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

Mapper扫描简化

移除annotationClass属性,简化Mapper扫描配置。这个变更通常不需要额外的代码修改,因为mybatis-spring 3.0.3的默认行为已经足够智能。但建议在迁移后验证所有Mapper接口是否被正确注册。

mybatis-config.xml迁移

如果5.3/6.6版本使用了独立的mybatis-config.xml文件,需要将其中的配置迁移到application.yml中:

yaml
# 教学示例 - mybatis-config.xml到application.yml的迁移
# 旧方式(5.3/6.6):
# <setting name="cacheEnabled" value="true" />
# <setting name="lazyLoadingEnabled" value="false" />

# 新方式(7.3):
mybatis:
  configuration:
    cache-enabled: true
    lazy-loading-enabled: false

6.3 迁移验证清单

完成迁移后,建议按照以下清单进行验证:

  1. 连接池验证

    • [ ] 确认数据源Bean正确创建,无报错日志
    • [ ] 确认连接池参数正确加载(通过JMX或日志验证)
    • [ ] 执行认证操作,确认数据库连接正常获取和归还
    • [ ] 模拟连接泄漏场景,确认泄漏检测机制正常工作
  2. 事务验证

    • [ ] 确认事务管理器Bean正确创建
    • [ ] 执行写操作,确认事务正常提交
    • [ ] 模拟写操作异常,确认事务正常回滚
    • [ ] 执行读操作,确认不创建不必要的事务
  3. MyBatis验证

    • [ ] 确认所有Mapper接口正确注册
    • [ ] 执行CRUD操作,确认SQL正确执行
    • [ ] 检查MyBatis日志,确认SQL语句和参数符合预期
  4. 性能验证

    • [ ] 执行压力测试,确认连接池参数合理
    • [ ] 监控数据库连接数,确认无连接泄漏
    • [ ] 对比迁移前后的认证响应时间,确认无性能退化

第七章 深入理解连接池与事务的协作机制

7.1 Spring事务管理器与连接池的交互原理

Spring的DataSourceTransactionManager是连接数据库连接池和事务管理的桥梁。理解它的内部工作机制,有助于排查复杂的事务问题——例如"为什么事务回滚了但数据仍然被修改"、"为什么同一个事务中的两次查询返回了不同的结果"等看似诡异的问题。

在CAS项目中,DataSourceTransactionManager(具体来说是命名为ticketTransactionManager的实例)是使用最广泛的事务管理器实现。它的设计简洁而高效:通过数据源获取连接、通过线程本地变量(ThreadLocal)绑定连接到当前线程、通过JDBC原生的提交和回滚机制管理事务。这种设计使得它既轻量级又与具体ORM框架解耦,非常适合与MyBatis等SQL映射框架配合使用。

事务开始时的连接获取

当一个@Transactional方法被调用时,Spring的事务管理器会执行以下步骤:

  1. 从数据源获取一个数据库连接
  2. 将连接的autoCommit属性设置为false(如果配置要求)
  3. 将连接绑定到当前线程(通过ThreadLocal
  4. 在整个事务执行期间,当前线程的所有数据库操作都使用这个连接
java
// 教学示例 - 事务管理器获取连接的简化逻辑
public class DataSourceTransactionManager {

    protected void doBegin(Object transaction, TransactionDefinition definition) {
        // 1. 从数据源获取连接
        Connection con = dataSource.getConnection();

        // 2. 设置autoCommit为false
        if (con.getAutoCommit()) {
            con.setAutoCommit(false);
        }

        // 3. 绑定到当前线程
        TransactionSynchronizationManager.bindResource(
            dataSource, con);
    }
}

事务执行期间的连接复用

在事务执行期间,MyBatis通过SpringManagedTransaction获取数据库连接。SpringManagedTransaction不会从连接池中获取新连接,而是从当前线程绑定的连接中获取:

java
// 教学示例 - SpringManagedTransaction获取连接的简化逻辑
public class SpringManagedTransaction implements Transaction {

    private Connection connection;

    public Connection getConnection() throws SQLException {
        if (this.connection == null) {
            // 从当前线程获取事务绑定的连接
            this.connection = DataSourceUtils
                .getConnection(this.dataSource);
        }
        return this.connection;
    }
}

这种机制确保了同一个事务中的所有数据库操作使用同一个连接,从而保证了事务的原子性。

事务结束时的连接释放

当事务完成(提交或回滚)时,Spring的事务管理器会执行以下步骤:

  1. 执行提交或回滚操作
  2. 将连接的autoCommit属性恢复为原始值
  3. 将连接从当前线程解绑
  4. 将连接归还到连接池
java
// 教学示例 - 事务管理器释放连接的简化逻辑
protected void doCleanupAfterCompletion(Object transaction) {
    // 1. 解绑当前线程的连接
    Connection con = (Connection) TransactionSynchronizationManager
        .unbindResource(dataSource);

    // 2. 恢复autoCommit
    try {
        if (con.getAutoCommit() != originalAutoCommit) {
            con.setAutoCommit(originalAutoCommit);
        }
    } finally {
        // 3. 归还连接到连接池
        DataSourceUtils.releaseConnection(con, dataSource);
    }
}

7.2 连接泄漏与事务超时的关联

连接泄漏和事务超时是两个经常同时出现的问题,它们之间存在密切的关联。

场景分析

考虑以下场景:一个@Transactional方法执行时间过长(例如由于外部服务调用超时),导致事务超时。如果事务超时后连接没有被正确释放,就会产生连接泄漏。

java
// 教学示例 - 事务超时导致连接泄漏
@Transactional(timeout = 30) // 30秒超时
public void complexAuthentication(UserCredential credential) {
    // 1. 数据库操作(正常)
    userDao.updateLoginTime(credential.getUsername());

    // 2. 外部服务调用(可能超时)
    externalService.verifyIdentity(credential);

    // 3. 数据库操作(可能因超时而无法执行)
    auditLogDao.insertLog("login_success");
}

如果步骤2的外部服务调用耗时超过30秒,Spring会标记事务为"rollback-only"。但外部服务调用本身不会被中断——它会继续执行直到完成或抛出异常。在此期间,数据库连接一直被占用。如果外部服务调用最终抛出异常,Spring会回滚事务并释放连接;但如果外部服务调用挂起(如TCP连接半开),连接可能永远不会被释放。

防御措施

针对这种场景,建议采取以下防御措施:

  1. 设置事务超时:通过@Transactional(timeout = 30)设置合理的事务超时时间。
  2. 设置外部调用超时:对外部服务调用设置独立的超时时间,且外部调用超时应小于事务超时。
  3. 连接泄漏检测:启用DBCP2的removeAbandonedOnBorrowremoveAbandonedOnMaintenance,作为最后的安全网。
  4. 避免在事务中调用外部服务:将外部服务调用移到事务外部,只在事务内执行数据库操作。

7.3 多数据源场景下的事务管理

在CAS项目中,如果存在多个数据源,事务管理会变得更加复杂。每个数据源需要独立的事务管理器,而跨数据源的操作需要特殊的事务管理策略。

单数据源事务(简单场景)

java
// 教学示例 - 单数据源事务
@Transactional("ticketTransactionManager")
public void saveTicket(Ticket ticket) {
    ticketDao.insert(ticket);
    auditDao.insertLog("ticket_created");
    // 两个DAO操作使用同一个数据源,在同一个事务中
}

多数据源事务(复杂场景)

java
// 教学示例 - 多数据源事务
@Transactional("ticketTransactionManager")
public void saveTicketWithAudit(Ticket ticket) {
    // 操作票据数据源
    ticketDao.insert(ticket);

    // 操作审计数据源(不同的事务管理器)
    auditTransactionTemplate.execute(status -> {
        auditDao.insertLog("ticket_created");
        return null;
    });
}

在这个示例中,票据操作在ticketTransactionManager管理的事务中执行,审计操作在auditTransactionManager管理的事务中执行。两个事务相互独立——票据事务回滚不会影响审计事务,反之亦然。

如果需要跨数据源的原子性操作(即要么全部成功,要么全部回滚),需要引入分布式事务管理方案,如JTA(Java Transaction API)或Saga模式。但在CAS场景中,跨数据源的原子性需求通常不强烈——票据数据和审计数据之间不需要严格的原子性保证。

7.4 MyBatis一级缓存与事务的关系

MyBatis的一级缓存(SqlSession级别的缓存)与Spring事务管理之间存在微妙的关系。理解这种关系有助于避免缓存导致的数据不一致问题。

MyBatis一级缓存的工作原理

MyBatis的一级缓存默认开启,它在SqlSession的生命周期内缓存查询结果。如果在同一个SqlSession中执行相同的查询,MyBatis会直接从缓存中返回结果,而不会再次查询数据库。

在Spring集成的场景中,MyBatis的SqlSession通过SqlSessionTemplate管理。默认情况下,SqlSessionTemplate为每个查询创建一个新的SqlSession,这意味着一级缓存在查询之间不会生效。

然而,在事务范围内,SqlSessionTemplate的行为会发生变化——它会在整个事务期间使用同一个SqlSession,使得一级缓存在事务范围内生效。

一级缓存与事务的交互

java
// 教学示例 - 一级缓存在事务中的行为
@Transactional
public Ticket getTicketAndVerify(String ticketId) {
    // 第一次查询:访问数据库
    Ticket ticket1 = ticketDao.selectById(ticketId);

    // 第二次查询:命中一级缓存,不访问数据库
    Ticket ticket2 = ticketDao.selectById(ticketId);

    // ticket1 == ticket2(同一对象引用)
    return ticket2;
}

在事务范围内,两次相同的查询会命中一级缓存。这在大多数情况下是有益的——减少了不必要的数据库查询。但在某些场景下,一级缓存可能导致问题:

  1. 在事务中更新后查询:如果在事务中先更新了一条记录,然后查询该记录,一级缓存可能返回更新前的旧数据(取决于MyBatis的flush策略)。
  2. 在事务中调用存储过程:如果存储过程修改了数据,MyBatis的一级缓存可能不知道数据已经变化。

解决方案

如果需要在事务中强制刷新缓存,可以调用SqlSession.clearCache()

java
// 教学示例 - 手动清除一级缓存
@Autowired
private SqlSessionTemplate sqlSessionTemplate;

@Transactional
public void updateAndQuery(String ticketId) {
    ticketDao.updateStatus(ticketId, "USED");

    // 清除缓存,确保下次查询访问数据库
    sqlSessionTemplate.clearCache();

    Ticket ticket = ticketDao.selectById(ticketId);
    // 现在可以获取到更新后的数据
}

或者,在Mapper XML中使用flushCache="true"属性:

xml
<!-- 教学示例 - 更新时刷新缓存 -->
<update id="updateStatus" flushCache="true">
    UPDATE ticket SET status = #{status} WHERE id = #{id}
</update>

第八章 常见问题与故障排查

8.1 连接池耗尽的排查

症状

CAS服务在运行一段时间后,所有认证请求开始超时,日志中出现"Cannot get a connection, pool error"或"Connection is not available, request timed out"等错误信息。用户在前端看到登录页面无响应或返回503错误,整个单点登录体系陷入瘫痪。这是生产环境中最严重的数据源问题之一,需要立即响应和排查。

连接池耗尽通常不是突然发生的,而是经过一个渐进的过程:连接泄漏逐渐积累、并发量突然增加、数据库响应变慢导致连接占用时间延长——这些因素叠加在一起,最终导致连接池耗尽。因此,排查连接池耗尽问题时,不仅要关注当前的状态,还要追溯历史趋势,找出问题的根源。

排查步骤

  1. 检查活动连接数:通过JMX查看连接池的活动连接数。如果活动连接数等于maxTotal,且等待线程数大于0,说明连接池已耗尽。

  2. 检查连接泄漏:查看DBCP2的泄漏连接回收日志。如果日志中频繁出现"Connection has been abandoned"信息,说明存在连接泄漏。

  3. 检查事务超时:查看是否有长时间运行的事务。通过数据库的SHOW PROCESSLIST(MySQL)或pg_stat_activity(PostgreSQL)查看当前活跃的数据库连接及其执行时间。

  4. 检查连接归还:确认所有数据库操作都正确关闭了连接。在Spring集成的场景中,连接的关闭由事务管理器自动处理,但如果使用了手动的JDBC操作(如JdbcTemplate在非事务方法中使用),可能需要手动关闭连接。

解决方案

  • 如果是连接泄漏:修复代码中的连接管理Bug,确保所有连接都能正确归还。
  • 如果是连接不足:增加maxTotal的值,或优化数据库查询以减少连接占用时间。
  • 如果是事务超时:优化长时间运行的事务,将非数据库操作移到事务外部。

8.2 事务不生效的排查

症状

数据库操作执行成功,但在异常发生时数据没有回滚;或者@Transactional注解似乎没有生效。

常见原因与解决方案

  1. 方法非public@Transactional注解只对public方法生效。如果方法被标记为private或protected,事务不会生效。

  2. 同类内部调用:Spring AOP基于代理机制实现事务管理。如果在一个类内部,方法A调用方法B(方法B标注了@Transactional),事务不会生效,因为调用不经过代理对象。

java
// 教学示例 - 同类内部调用导致事务不生效
@Service
public class TicketService {

    public void processTicket(String ticketId) {
        // 直接调用,不经过代理,事务不生效
        this.updateTicket(ticketId);
    }

    @Transactional
    public void updateTicket(String ticketId) {
        ticketDao.updateStatus(ticketId, "USED");
    }
}

解决方案:将updateTicket方法移到另一个Service类中,或者通过AopContext.currentProxy()获取代理对象来调用。

  1. 异常类型不匹配:默认情况下,@Transactional只在RuntimeExceptionError时回滚,不回滚checked Exception。如果方法抛出了IOException等checked Exception,事务不会回滚。
java
// 教学示例 - 指定回滚异常类型
@Transactional(rollbackFor = Exception.class)
public void saveTicketWithFile(Ticket ticket) throws IOException {
    ticketDao.insert(ticket);
    writeFile(ticket); // 可能抛出IOException
    // rollbackFor = Exception.class 确保IOException也会触发回滚
}
  1. 事务管理器未正确配置:如果Spring容器中存在多个事务管理器,且@Transactional注解没有指定使用哪个,可能导致使用了错误的事务管理器。

8.3 MyBatis Mapper绑定失败的排查

症状

CAS启动时报错:"Invalid bound statement (not found)",表示MyBatis无法找到Mapper接口对应的SQL映射。

常见原因与解决方案

  1. Mapper XML文件位置不正确:确认Mapper XML文件位于mapperLocations配置指定的路径下。

  2. Mapper XML的namespace不正确:确认Mapper XML文件中的namespace属性与Mapper接口的全限定类名一致。

xml
<!-- 教学示例 - Mapper XML namespace -->
<mapper namespace="cc.bima.cas.dao.TicketMapper">
    <!-- namespace必须与接口全限定类名一致 -->
</mapper>
  1. Mapper接口未被扫描:确认Mapper接口位于basePackage配置指定的包下,且(在6.6版本中)标注了@Mapper注解。

  2. 编译时XML文件未包含:在Gradle构建中,确认src/main/resources目录下的XML文件被正确包含在构建输出中。如果使用了资源过滤(resource filtering),确认XML文件没有被过滤掉。

8.4 版本升级后的兼容性问题

mybatis-spring 3.0.3的Breaking Changes

从mybatis-spring 1.3.1升级到3.0.3时,可能遇到以下兼容性问题。这些问题在实际项目中并不罕见,建议在迁移前逐一排查,避免上线后才发现问题。

  1. SqlSessionTemplate的行为变化:mybatis-spring 3.0.3中,SqlSessionTemplate的默认行为有所调整。如果项目中直接使用了SqlSessionTemplate,需要测试其行为是否仍然符合预期。特别是在批量操作和自定义拦截器的场景下,SqlSession的生命周期管理可能存在差异。
  2. MapperScannerConfigurer的默认值变化:某些属性的默认值可能发生了变化。建议在迁移时显式设置所有关键属性,而不是依赖默认值。例如,lazyInitialization属性的默认值在不同版本中可能不同,影响Mapper代理对象的创建时机。
  3. 类型处理器的变更:mybatis-spring 3.0.3对某些Java类型的处理方式可能有所调整。如果项目中使用了自定义的类型处理器,需要验证其兼容性。特别是Java 8日期时间类型(LocalDateTime、LocalDate等)的处理,在不同版本中可能使用不同的类型处理器。

commons-dbcp2的属性名变更

从commons-dbcp 1.4迁移到commons-dbcp2时,最容易被忽略的属性名变更:

  1. maxActive -> maxTotal:如果遗漏,连接池将以默认值8运行。在一个中等规模的CAS部署中,8个连接远远不够,会导致严重的性能问题。
  2. maxWait -> maxWaitMillis:如果遗漏,连接池将以默认值-1(无限等待)运行。这意味着当连接池耗尽时,请求线程将无限期阻塞,最终导致线程池耗尽和服务不可用。
  3. removeAbandoned -> removeAbandonedOnBorrow + removeAbandonedOnMaintenance:如果遗漏,泄漏检测将不生效。在生产环境中,缺少泄漏检测意味着连接泄漏会持续累积,直到连接池耗尽。

建议在迁移后通过JMX验证所有连接池参数是否正确加载。可以编写一个简单的健康检查接口,在启动时输出所有连接池参数的当前值,与预期值进行比对。这种"启动时自检"的机制可以在问题暴露给用户之前就发现配置错误。

Spring Boot 3.x的自动配置冲突

在CAS 7.3.x中,Spring Boot 3.x的数据源自动配置可能与手动配置的spring-common.xml产生冲突。例如,Spring Boot可能自动创建一个DataSource Bean,与XML中定义的dataSource Bean冲突。为了避免这种冲突,可以在application.yml中排除数据源的自动配置:

yaml
# 教学示例 - 排除数据源自动配置
spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

但这种做法也有副作用——排除自动配置后,一些依赖自动配置的功能(如健康检查中的数据源检测)可能需要手动实现。因此,建议在排除自动配置后,全面测试CAS的所有功能,确保没有遗漏。


总结与展望

本文基于真实的CAS Overlay项目源码,系统性地梳理了从CAS 5.3.x到6.6.x再到7.3.x三个版本中,数据源事务管理与MyBatis集成方案的演进历程。通过对spring-common.xml配置文件的逐代分析,我们揭示了以下关键趋势:

配置范式的演进:从5.3版本的"全量XML显式配置",到6.6版本的"精准化XML配置",再到7.3版本的"最小化XML+注解驱动+自动配置",配置的复杂度不断降低,而框架的自动化程度不断提升。这种演进趋势与Spring Boot生态的发展方向高度一致——让框架做更多的事情,让开发者专注于业务逻辑。

连接池的技术升级:从commons-dbcp 1.4到commons-dbcp2,不仅仅是属性名的变更,更是连接池内部架构的根本性重构。DBCP2在并发性能、连接生命周期管理、泄漏检测等方面都带来了显著的提升。对于生产环境而言,这次升级的价值不容忽视。

事务管理的精细化:从"一刀切"的全方法REQUIRED事务,到按方法名前缀区分读写的精细化切面,再到方法级别的@Transactional注解,事务管理的粒度越来越精细。这种精细化使得系统可以在保证数据一致性的同时,最大限度地提升并发性能。

MyBatis集成的现代化:从mybatis-spring 1.3.1到3.0.3,MyBatis的Spring集成方案完成了对Spring 6.x和Jakarta EE的适配。虽然升级过程中存在Breaking Changes,但mybatis-spring 3.0.3带来的自动配置增强和API现代化,为后续的维护和升级奠定了更好的基础。

展望未来,CAS的数据层配置将继续朝着以下方向演进:

  1. 连接池的进一步优化:随着虚拟线程(Virtual Threads)在Java 21中的引入,传统的连接池模型可能面临重构。虚拟线程使得"一个请求一个连接"的模型变得更加可行,连接池的配置策略可能需要相应调整。

  2. 响应式数据访问:Spring WebFlux和R2DBC正在推动Java生态向响应式编程模型迁移。虽然CAS目前仍以同步模型为主,但响应式数据访问可能在未来的CAS版本中成为可选方案。

  3. 云原生数据源管理:在Kubernetes环境中,数据源配置的动态化(如通过ConfigMap和Secret管理数据库凭证、通过Service Mesh实现数据库流量管理)将成为标准实践。CAS的数据源配置需要更好地适配这些云原生基础设施。

  4. 可观测性的深度融合:OpenTelemetry等可观测性标准正在统一分布式系统的监控、追踪和日志。未来的数据源配置需要原生支持分布式追踪(如将数据库连接的获取和释放纳入追踪链路),以便在微服务架构中实现端到端的问题排查。

无论技术如何演进,数据源事务管理的核心原则不会改变:确保数据一致性、保障系统可用性、优化资源利用率。理解本文所阐述的三代配置演进逻辑,将帮助读者在面对未来的技术变革时,能够从容应对、做出正确的技术决策。


版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。

本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。

文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc