自研一个 Spring Cloud starter 灰度路由 组件,实现动态灰度 流量的路由

自研一个 Spring Cloud starter 灰度路由 组件,实现动态灰度 流量的路由

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:

如果要你设计一个 SaaS 多租户平台,你会如何设计 隔离架构?

如何 自研一个非侵入式 SaaS 多租户组件?

最近又有小伙伴在面试很多 SaaS类软件公司,都遇到了相关的面试题。虽然 回答了一些边边角角,但是回答不全面不体系,面试官不满意,面试挂了。

借着此文,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,展示一下雄厚的 “技术肌肉、技术实力”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提,offer自由”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V140版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取

自研一个非侵入式 SaaS 多租户组件

在软件行业,“软件即服务”(SaaS)已成为主流商业模式。

SaaS 承诺为不同客户(即“租户”)提供标准化的服务,同时实现快速交付与规模化运维。

然而, SaaS 架构师都必须面对的核心挑战:

如何 为成百上千的租户构建一个既能共享资源以降低成本,又能严格隔离数据以确保安全的系统?

想象一下,如果 A 公司的财务报表意外地出现在了 B 公司的后台,这将是怎样一场灾难性的数据泄露事故。

因此,数据隔离不仅是技术问题,更是 SaaS 产品的生命线。

本文通过自研***mon-tenant-starter模块为例, 深入探索一套企业级的多租户数据隔离解决方案。

本文 基础的 前置知识

京东面试:说说mybatis底层原理。 MyBatis 如何实现 “面向接口” 查询的 ?

全文大纲

为了系统性地解构多租户架构的实现,我们将遵循从宏观设计到微观实现的路径,逐一攻克各个技术要点。

第一章:核心挑战:在“共享”与“隔离”之间寻求平衡

构建 SaaS 平台,首先要做的便是在多租户的数据隔离方案上做出抉择。

业界主流的方案大致可分为三层:

独立数据库(物理隔离):为每个租户提供独立的数据库实例。隔离级别最高,但成本和运维复杂度也最高。

共享数据库,独立 Schema:所有租户共享同一个数据库实例,但每个租户拥有独立的 Schema。隔离性和成本之间取得了较好的平衡。

共享数据库,共享 Schema,字段隔离:所有租户共享同一个数据库、同一套表结构。通过在每张业务表中增加一个tenant_id字段来区分数据归属。资源利用率最高,但对应用层的代码设计提出了最高的挑战。

本项目选择的正是第三种方案,这也是绝大多数 SaaS 产品的主流选择。

对比维度 1. 独立数据库 2. 共享库,独立 Schema 3. 共享库,共享 Schema (本项目)
隔离级别 非常高 (物理隔离) 高 (逻辑隔离) 一般 (应用层隔离)
数据安全 非常好 依赖代码实现
开发成本 较高 高,需保证所有 SQL 正确
维护成本 非常高 较高
资源成本 非常高 较高
扩展性 一般 非常好

这种方案的魅力在于其极高的资源利用率和灵活性,但其最大的风险在于“忘记”添加tenant_id过滤条件。

因此,我们的核心设计目标必须是:通过一套自动化的、非侵入的机制,让开发者无需时刻关心tenant_id,系统也能百分之百正确地执行数据过滤。

第二章:一键使用我们的 ***mon-tenant-starter

在开始自研 组件之前,先了解如何为在一个新的微服务中,一键使用我们 自研的 多租户组件 ***mon-tenant-starter 开启多租户功能。

整个过程被设计得极其简单,只需三步。

第一步:引入依赖

在你的微服务模块的 pom.xml 文件中,添加 ***mon-tenant-starter 的依赖。


<dependency>
    <groupId>org.dromara</groupId>
    <artifactId>***mon-tenant-starter</artifactId>
</dependency>

第二步:开启租户功能

修改配置文件,在服务的 application.yml 配置文件中,添加以下配置来开启多租户功能。


tenant:
    enable: true
    # 配置不需要进行租户ID过滤的表, 例如全局配置表、字典表等
    excludes:
        - not_tenant_table
        - not_tenant_column

第三步:改造数据库表

改造数据库表, 为所有需要按租户隔离的业务表添加 tenant_id 字段。


ALTER TABLE your_business_table ADD COLUMN tenant_id VARCHAR(20) NOT NULL ***MENT '租户编号';

完成以上三步,你的微服务就已经具备了全方位的数据隔离能力!就是这么简单。

第三章:自研组件第一步: TenantHelper 上下文基础类

要实现自动化的多租户 数据过滤, 首先必须能在任何时刻、任何代码位置,准确地知道“当前正在为哪个租户服务”。TenantHelper正是为此而生的核心工具类。

核心原理:ThreadLocal

TenantHelper的底层精髓是ThreadLocal

ThreadLocal为每个线程提供独立的变量副本,确保了在同一个请求的处理流程中,无论代码调用链有多深,都能随时获取到正确的租户上下文,且不会与其它并发请求发生混淆。

获取租户id的两个场景:

当需要获取当前租户 ID 时,它遵循一个清晰的优先级顺序:

  • 优先动态 获取租户 ID

    TenantHelper提供了一个dynamic(tenantId, handle)方法,允许在代码中临时切换到指定的租户上下文来执行一段逻辑。

  • 其次 获取 当前登录用户的租户 ID

    如果不存在动态租户 ID,TenantHelper会通过LoginHelper.getTenantId()获取当前登录用户的租户 ID。


 
public class TenantHelper {

    public static String getTenantId() {
        if (!isEnable()) {
            return null;
        }
        // 1. 尝试获取动态设置的租户ID
        String tenantId = TenantHelper.getDynamic();
        if (StringUtils.isBlank(tenantId)) {
            // 2. 获取当前登录用户的租户ID
            tenantId = LoginHelper.getTenantId();
        }
        return tenantId;
    }
}

getDynamic 、getTenantId 这两个方法的源码通常如下:

1. TenantHelper.getDynamic()方法源码


public class TenantHelper {
    
    /**
     * 动态租户ID的ThreadLocal容器
     */
    private static final ThreadLocal<String> DYNAMIC_TENANT_ID = new ThreadLocal<>();
    
    /**
     * 获取动态设置的租户ID
     * 这个方法通常用于在特定业务场景下临时覆盖当前线程的租户ID
     */
    public static String getDynamic() {
        return DYNAMIC_TENANT_ID.get();
    }
    
    /**
     * 设置动态租户ID
     */
    public static void setDynamic(String tenantId) {
        DYNAMIC_TENANT_ID.set(tenantId);
    }
    
    /**
     * 清除动态租户ID
     */
    public static void clearDynamic() {
        DYNAMIC_TENANT_ID.remove();
    }
    
    /**
     * 判断是否启用多租户模式
     */
    public static boolean isEnable() {
        // 通常从配置文件中读取多租户是否启用的配置
        return RuoYiConfig.getTenantEnable();
    }
}

2. LoginHelper.getTenantId()方法源码


public class LoginHelper {
    
    /**
     * 获取当前登录用户的租户ID
     */
    public static String getTenantId() {
        try {
            LoginUser loginUser = getLoginUser();
            if (loginUser != null) {
                return loginUser.getTenantId();
            }
        } catch (Exception e) {
            // 忽略异常,可能是用户未登录等情况
        }
        return null;
    }
    
    /**
     * 获取当前登录用户信息
     */
    public static LoginUser getLoginUser() {
        try {
            // 从SecurityContextHolder中获取认证信息
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null) {
                Object principal = authentication.getPrincipal();
                if (principal instanceof LoginUser) {
                    return (LoginUser) principal;
                }
            }
        } catch (Exception e) {
            // 异常处理
        }
        return null;
    }
}

第四章: Mybatis-Plus 拦截器如何成为“SQL 非入侵修改”

…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

转载请说明出处内容投诉
CSS教程网 » 自研一个 Spring Cloud starter 灰度路由 组件,实现动态灰度 流量的路由

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买