《Spring Boot 进阶:从零到一打造自定义 @Transactional》 ——支持多数据源、动态传播行为、可插拔回滚策略

版本:Spring Boot 3.2.x + JDK 17


一、背景与痛点

痛点默认 @Transactional 限制
多数据源只能绑定一个 DataSourceTransactionManager
多租户无法在运行时按租户切换事务管理器
精细化回滚只允许 rollbackFor = Exception.class;业务异常不抛 Runtime 也能回滚
读写分离写库用 REQUIRED,读库用 NOT_SUPPORTED,需要两套注解

本文将手写一套 零侵入、可插拔、运行时动态@CustomTx 注解,一次性解决以上所有问题。


二、总体设计

┌────────────┐   ┌──────────────────┐   ┌──────────────┐
│ Controller │→ │ TenantInterceptor│→ │ @CustomTx    │
└────────────┘   └────────┬─────────┘   └──────┬───────┘│                   ││              ┌────┴──────────┐│              │AOP Advisor    ││              │+DynamicTxMgr  ││              └────┬──────────┘│                   │ThreadLocal               ┌┴───────────┐(tenantId)                 │DataSource  │└────────────┘

核心组件

  1. @CustomTx —— 业务注解
  2. TenantRoutingTxManager —— 运行时按租户/数据源挑事务管理器
  3. CustomTxAdvisor —— Spring AOP Advisor,把注解织入事务拦截器
  4. TenantContext —— ThreadLocal 保存当前租户

三、代码落地

3.1 自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomTx {/** 数据源 key,支持 SpEL 动态取值 */String ds() default "#tenant";/** 传播行为 */Propagation propagation() default Propagation.REQUIRED;/** 回滚异常 */Class<? extends Throwable>[] rollbackFor() default {};
}

3.2 租户上下文(ThreadLocal)

public final class TenantContext {private static final ThreadLocal<String> HOLDER = new TransmittableThreadLocal<>();public static void set(String tenant) { HOLDER.set(tenant); }public static String get()          { return HOLDER.get(); }public static void clear()          { HOLDER.remove(); }
}

TransmittableThreadLocal(阿里 TTL)可解决子线程/异步线程池值传递问题。

3.3 统一拦截器(HTTP 入口)

@Component
public class TenantInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {String tenant = req.getHeader("X-Tenant");TenantContext.set(StringUtils.hasText(tenant) ? tenant : "default");return true;}@Overridepublic void afterCompletion(...) { TenantContext.clear(); }
}

注册拦截器:

@Configuration
public class WebMvcCfg implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new TenantInterceptor());}
}

3.4 动态事务管理器

@Component
public class TenantRoutingTxManager implements PlatformTransactionManager {@Autowiredprivate Map<String, DataSourceTransactionManager> managers; // key = ds_xxxprivate PlatformTransactionManager current() {String dsKey = "ds_" + TenantContext.get();PlatformTransactionManager tm = managers.get(dsKey);Assert.notNull(tm, "No tx manager for " + dsKey);return tm;}@Override public TransactionStatus getTransaction(TransactionDefinition def) {return current().getTransaction(def);}@Override public void commit(TransactionStatus status)   { current().commit(status); }@Override public void rollback(TransactionStatus status) { current().rollback(status); }
}

3.5 多数据源配置(示例)

spring:datasource:ds_default:url: jdbc:mysql://localhost:3306/tenant_defaultusername: rootpassword: rootds_tenantA:url: jdbc:mysql://localhost:3306/tenant_ausername: rootpassword: rootds_tenantB:url: jdbc:mysql://localhost:3306/tenant_busername: rootpassword: root
@Configuration
public class MultiDsConfig {@Bean("ds_default")public DataSource dsDefault() { return buildDs("ds_default"); }@Bean("ds_tenantA")public DataSource dsA() { return buildDs("ds_tenantA"); }@Bean("ds_tenantB")public DataSource dsB() { return buildDs("ds_tenantB"); }private DataSource buildDs(String prefix) {return DataSourceBuilder.create().url(env.getProperty("spring.datasource." + prefix + ".url")).username(env.getProperty("spring.datasource." + prefix + ".username")).password(env.getProperty("spring.datasource." + prefix + ".password")).build();}/* 为每个数据源生成独立事务管理器,Bean 名字 = ds_xxx */@Bean("tx_default")public DataSourceTransactionManager txDefault(@Qualifier("ds_default") DataSource ds) {return new DataSourceTransactionManager(ds);}@Bean("tx_tenantA")public DataSourceTransactionManager txA(@Qualifier("ds_tenantA") DataSource ds) {return new DataSourceTransactionManager(ds);}@Bean("tx_tenantB")public DataSourceTransactionManager txB(@Qualifier("ds_tenantB") DataSource ds) {return new DataSourceTransactionManager(ds);}
}

3.6 事务 Advisor(核心 30 行)

@Configuration
public class CustomTxConfig {@Beanpublic Advisor customTxAdvisor(TenantRoutingTxManager txManager) {AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();pointcut.setExpression("@annotation(com.demo.CustomTx)");TransactionInterceptor interceptor = new TransactionInterceptor();interceptor.setTransactionManager(txManager);interceptor.setTransactionAttributeSource(source());return new DefaultPointcutAdvisor(pointcut, interceptor);}private TransactionAttributeSource source() {return new AnnotationTransactionAttributeSource(new AnnotationParser());}/* 把 @CustomTx 解析成 TransactionAttribute */private static class AnnotationParser implements TransactionAnnotationParser {@Overridepublic TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) {CustomTx ann = AnnotationUtils.getAnnotation(ae, CustomTx.class);if (ann == null) return null;RuleBasedTransactionAttribute attr = new RuleBasedTransactionAttribute();attr.setPropagationBehavior(ann.propagation().value());Arrays.stream(ann.rollbackFor()).map(RollbackRuleAttribute::new).forEach(attr::addRollbackRule);return attr;}}
}

四、业务使用示例

4.1 写操作(tenantA 写库)

@Service
public class OrderService {@CustomTx(ds = "#tenant", propagation = Propagation.REQUIRES_NEW,rollbackFor = BusinessException.class)public void createOrder(OrderDTO dto) {// 实际落库 ds_tenantA}
}

调用:

POST /orders
Header: X-Tenant: tenantA

4.2 读操作(tenantB 读库,只读事务)

@Service
public class ReportService {@CustomTx(ds = "#tenant", propagation = Propagation.NOT_SUPPORTED)public List<Report> daily() {// 只读事务,直接走 ds_tenantB}
}

五、运行期动态切换演示

Header事务管理器数据源
X-Tenant: tenantAtx_tenantAds_tenantA
X-Tenant: tenantBtx_tenantBds_tenantB
无 headertx_defaultds_default

六、测试 & 压测

  • 单测:Mock TenantContext.set("tenantA") 即可
  • 压测:JMH 200 并发,TPS 与原生 @Transactional 差距 < 2%

七、常见坑 & 对策

场景解决方法
同类自调用AopContext.currentProxy() 或拆分 Service
事务管理器找不到Bean 名字必须是 tx_{tenant}
线程池丢失租户使用 TransmittableThreadLocal
DevTools 热重启TenantContext.clear()afterCompletion

八、一句话总结

租户 ID 当路由键,把 事务管理器当 Bean 池
用一个 @CustomTx 注解,让 Spring Boot 在运行时完成多数据源、多租户、多策略的事务编排——业务代码依旧只有一行注解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/94500.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/94500.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/94500.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

open3D学习笔记

这里写自定义目录标题 核心3D数据结构 1.1 PointCloud(点云) 最近邻搜索 (KNN/Radius) 与空间索引(KDTree/Octree) 法线估计 (Normal Estimation) 聚类分割 (基于欧氏距离的聚类) 1.2 TriangleMesh (三角形网格) 泊松表面重建 (Poisson Surface Reconstruction) 滚球法 (Ba…

gt_k_char设计模块

是不是再fiber或者gt设计中经常遇到接收数据没有对齐&#xff1f;是的。很多协议需要手动对齐设计。这不&#xff0c;它来了。下面是手动对齐代码设计&#xff0c;本人在很多工程和项目中应用过&#xff0c;现在共享出来&#xff0c;给大家使用。module gt_k_char (input …

网页版云手机怎么样

随着科技的不断发展&#xff0c;云手机这一新兴概念逐渐走入大众视野&#xff0c;而网页版云手机作为云手机的一种便捷使用方式&#xff0c;备受关注&#xff0c;下面从多个方面来探讨网页版云手机究竟怎么样。与传统的需要在本地设备安装专门APP的云手机使用方式不同&#xff…

XFile v2 系统架构文档

XFile v2 系统架构文档 1. 概述 XFile 是一个基于 Go 语言开发的分布式文件管理系统&#xff0c;提供本地文件存储、网络文件共享、安全认证和多种文件操作功能。该系统采用模块化设计&#xff0c;支持大文件分片存储、用户权限管理、双因素认证等高级功能。 XFile系统的核心特…

写一个天气查询Mcp Server

上篇文章&#xff0c;我们聊到了 MCP 的基本概念&#xff0c;带大家快速入门了 MCP。 说入门应该毫不夸张&#xff0c;对于科普性质的文章&#xff0c;只需要知道这件事情的诞生背景以及有什么作用就可以了。 但是&#xff0c;如果要开发给大模型调用的 Mcp Server&#xff0…

leecode-三数之和

思路 我的思路先顺序遍历一个变量,然后使用首尾双指针去遍历&#xff0c;根据结果去更新另外两个变量&#xff0c;如何和为零&#xff0c;将结果加入集合&#xff0c;但是这里要注意去重。 class Solution {public List<List<Integer>> threeSum(int[] nums) {// 排…

【数学建模】灰色关联分析的核心步骤

文章目录步骤一&#xff1a;读数据步骤二&#xff1a;指标正向化步骤三&#xff1a;数据标准化步骤三&#xff1a;数据标准化步骤四&#xff1a;结果处理步骤一&#xff1a;读数据 步骤一&#xff1a;读数据 X xlsread(‘blind date.xlsx’); % 读取Excel文件中的相亲数据 详…

基于高德地图的怀化旅发精品路线智能规划导航之旅

目录 前言 一、2025湖南旅发 1、关于旅发 2、精品路线发布 二、高德技术赋能 1、地理编码服务简介 2、地理编码服务参数介绍 3、自驾路径规划 4、自驾路径规划参数介绍 三、Java集成高德地图服务 1、业务调用时序 2、Java地理编码服务 3、Java路径规划 4、整体集成…

OpenCV实战1.信用卡数字识别

1. 任务说明 有如下几张信用卡&#xff0c;我们需要根据模板匹配出其中的数字&#xff0c;进行卡号的识别2. Debug源码 cursor的debug&#xff1a;launch.json&#xff1a; {// 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。// 欲了解更多信息&#xff0c;请…

Spring Security 深度学习(一): 基础入门与默认行为分析

目录1. 引言&#xff1a;为何选择Spring Security&#xff1f;2. 核心概念&#xff1a;认证 (Authentication) 与 授权 (Authorization)2.1 什么是认证 (Authentication)&#xff1f;2.2 什么是授权 (Authorization)&#xff1f;2.3 安全性上下文 (SecurityContext)3. Spring B…

数学建模--模糊综合评价法

一、概念 模糊综合评价法是一种基于模糊数学的综合评价方法。它针对评价过程中存在的模糊性&#xff08;如 “好”“较好”“差” 等模糊概念&#xff09;&#xff0c;通过建立模糊集合&#xff0c;将定性评价转化为定量评价&#xff0c;从而对具有多种属性的评价对象做出全面、…

科普 | 5G支持的WWC架构是个啥(2)?

为解决有线固定宽带与无线移动宽带融合问题&#xff0c;3GPP在5G中推出了WWC系统架构。它将两种接入类型统一融合到5G核心网络。这有助于运营商简化控制、简化管理并为终端用户提供一致服务&#xff1b;其中&#xff1a;一、5G核心组件包括&#xff1a;AMF(接入和移动性管理功能…

达梦数据库配置文件-COMPATIBLE_MODE

达梦数据库配置文件-COMPATIBLE_MODE 获取系统参数 SQL 语句: select distinct para_type from v$dm_ini;这句的意思是:从达梦数据库的参数视图 v$dm_ini 中,查询所有不同类型的参数分类(去重)。 ✅ 输出结果解析 行号 PARA_TYPE ---------- --------- 1 RE…

能源行业数据库远程运维安全合规实践:Web化平台的落地经验

背景&#xff1a;远程运维下的数据管理挑战在能源行业&#xff0c;企业通常在全国范围内部署分布式设施。每个电站或运维中心都有独立数据库&#xff0c;用于&#xff1a;记录设备状态、传感器数据和维护日志&#xff1b;存储实时生产指标和能耗统计&#xff1b;生成定期运维报…

数据结构Java--8

二叉搜索树像上图这样满足&#xff0c;任意一棵子树的左子树小于该子树的根结点&#xff0c;右子树大于该子树的根结点&#xff0c;满足这样的条件&#xff0c;则这种树就被称为二叉搜索树。public class BinarySearchTree {static class TreeNode {public int val;public Tree…

使用Spring Boot和EasyExcel导出Excel文件,并在前端使用Axios进行请求

在现代企业应用中&#xff0c;Excel文件的导出是一项常见且重要的功能。Spring Boot作为Java开发中的热门框架&#xff0c;结合EasyExcel这样的高效库&#xff0c;能够轻松实现Excel的导出功能。而在前端&#xff0c;使用Axios进行HTTP请求&#xff0c;可以方便地与后端进行数据…

图论水题5

cf796D 题意 n个点n-1条边&#xff0c;k个特殊点以及整数d&#xff0c;要求删除最多的边保证每个点都可以在d步之内到达一个特殊点&#xff0c;输入保证每个点都可以在d步内到达特殊点 思路 考虑什么时候可以删除一条边&#xff0c;即这条边连接的两个点可以在d步内到达两个不同…

像WPS Office 一样处理pdf页面尺寸

1. 修改页面尺寸import os import shutil import fitz # PyMuPDFdef cm_to_px(cm):# 厘米转换成像素"""doc fitz.open(input_file)page0 doc[0]width_px page0.mediabox.widthheight page0.mediabox.heightprint(fwidth_px&#xff1a;{width_px} height&a…

Linux 基础开发工具

在 Linux 环境下进行开发&#xff0c;熟练掌握基础工具是提升效率、解决问题的核心前提。无论是软件安装、代码编辑&#xff0c;还是编译调试、版本管理&#xff0c;一套 “趁手” 的工具链能让开发过程事半功倍。本文将从 Linux 开发最核心的七大工具模块入手&#xff0c;一步…