MybatisPlus 多租户架构(Multi-tenancy)实现详解

“MybatisPlus 多租户架构(Multi-tenancy)实现详解”旨在为需要在一个应用中支持多个租户的开发人员提供一种解决方案。在这个架构中,多个租户可以共享相同的代码库和实例,并在逻辑上隔离数据。

实现多租户架构需要考虑以下三个方面:

  1. 租户隔离

使用 Mybatis-Plus 提供的 SqlParserInterceptor 对 SQL 进行拦截,通过替换表名、增加 WHERE 条件等方式实现租户隔离。具体实现代码如下:

public class MybatisPlusConfig {

    @Bean
    public SqlParserInterceptor sqlParserInterceptor() {
        return new SqlParserInterceptor() {
            @Override
            public void prepare(Invocation invocation) {
                if (TenantContextHolder.getTenantId() != null) {
                    MetaObject metaObject = SystemMetaObject.forObject(invocation.getTarget());
                    MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
                    BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
                    String sql = boundSql.getSql();
                    sql = sql.replaceAll(mappedStatement.getId() + ".", mappedStatement.getId() + "_" + TenantContextHolder.getTenantId() + ".");
                    sql = "select t.* from (" + sql + ") t where t.tenant_id = " + TenantContextHolder.getTenantId();
                    metaObject.setValue("delegate.boundSql.sql", sql);
                }
            }
        };
    }

}
  1. 租户切换

使用 ThreadLocal 实现当前租户ID在同一线程中的共享,在实际访问数据库时增加租户ID参数。具体实现代码如下:

public class TenantContextHolder {

    private static final ThreadLocal<String> TENANT_ID = new ThreadLocal<>();

    public static void setTenantId(String tenantId) {
        TENANT_ID.set(tenantId);
    }

    public static String getTenantId() {
        return TENANT_ID.get();
    }

    public static void clearTenantId() {
        TENANT_ID.remove();
    }

}
  1. 租户配置

为每一个租户配置独立的数据源,使用 Mybatis-Plus 提供的 DynamicDataSource 实现动态数据源切换。具体实现代码如下:

public class DataSourceConfig {

    private final static Map<String, DruidDataSource> DATA_SOURCE_MAP = new ConcurrentHashMap<>();

    private final static String DEFAULT_TENANT_ID = "1";

    @Bean("tenantDataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource tenantDataSource() {
        return new DruidDataSource();
    }

    @Bean("dynamicDataSource")
    public DynamicDataSource dataSource(@Qualifier("tenantDataSource") DataSource tenantDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DEFAULT_TENANT_ID, tenantDataSource);
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(tenantDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        DynamicDataSourceContextHolder.addDataSourceKeys(DEFAULT_TENANT_ID);
        DATA_SOURCE_MAP.put(DEFAULT_TENANT_ID, (DruidDataSource) tenantDataSource);
        return dynamicDataSource;
    }

    private static DruidDataSource createDataSource(String driverClassName, String url, String username, String password) {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

    public static void addDataSource(String tenantId, String driverClassName, String url, String username, String password) {
        DruidDataSource dataSource = createDataSource(driverClassName, url, username, password);
        DATA_SOURCE_MAP.put(tenantId, dataSource);
        DynamicDataSourceContextHolder.addDataSourceKeys(tenantId);
    }

    public static void removeDataSource(String tenantId) {
        DATA_SOURCE_MAP.remove(tenantId);
        DynamicDataSourceContextHolder.removeDataSourceKeys(tenantId);
    }

    public static void switchDataSource(String tenantId) throws SQLException {
        if (!DATA_SOURCE_MAP.containsKey(tenantId)) {
            throw new RuntimeException("tenantId not exists: " + tenantId);
        }
        DynamicDataSource dynamicDataSource = (DynamicDataSource) ApplicationContextHolder.getBean("dynamicDataSource");
        dynamicDataSource.setTargetDataSource(DATA_SOURCE_MAP.get(tenantId));
        TenantContextHolder.setTenantId(tenantId);
    }

}

示例一:基于 Header 实现租户切换

在这个示例中,我们将在 HTTP Header 中增加 Tenant ID,通过定义 Filter 将该 Header 设置为当前租户ID。具体实现代码如下:

public class HeaderTenantFilter implements Filter {

    private final static String TENANT_HEADER = "X-TENANT-ID";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String tenantId = httpServletRequest.getHeader(TENANT_HEADER);
        TenantContextHolder.setTenantId(tenantId != null ? tenantId : DataSourceConfig.DEFAULT_TENANT_ID);
        chain.doFilter(request, response);
        TenantContextHolder.clearTenantId();
    }

}

示例二:基于 sub-domain 实现租户切换

在这个示例中,我们将为每个租户分配独立的子域名,并通过定义 Servlet 拦截器解析当前访问的子域名并设置为当前租户ID。具体实现代码如下:

public class SubdomainTenantInterceptor extends HandlerInterceptorAdapter {

    private final static String HTTP_SCHEMA = "http";
    private final static String HTTPS_SCHEMA = "https";
    private final static String DEFAULT_DOMAIN = "example.com";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String domain = (request.getServerName()).toLowerCase();
        String schema = request.isSecure() ? HTTPS_SCHEMA : HTTP_SCHEMA;
        String tenantId = getTenantIdBySubdomain(domain);
        String redirectUrl = getRedirectUrl(schema, domain);
        if (StringUtils.isBlank(tenantId)) {
            response.sendRedirect(redirectUrl);
            return false;
        } else {
            TenantContextHolder.setTenantId(tenantId);
            return true;
        }
    }

    private String getTenantIdBySubdomain(String domain) {
        String tenantId = null;
        if (StringUtils.isNotBlank(domain)) {
            String[] subdomains = domain.split("\\.");
            if (subdomains.length > 0) {
                tenantId = subdomains[0];
            }
        }
        return tenantId;
    }

    private String getRedirectUrl(String schema, String domain) {
        return schema + "://" + DOMAIN + "/no-tenant";
    }

}

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:MybatisPlus 多租户架构(Multi-tenancy)实现详解 - Python技术站

(0)
上一篇 2023年5月20日
下一篇 2023年5月20日

相关文章

  • java数据库唯一id生成工具类

    Java数据库唯一ID生成工具类是用于在关系型数据库中生成唯一ID的工具类。在开发中,经常需要使用唯一ID作为数据库表的主键,而使用数据库自增长的整数或GUID字符串作为主键,会存在一些问题,如分布式环境下高并发的ID生成、算法不唯一等问题。因此,使用Java数据库唯一ID生成工具类,可以解决这些问题。 下面给出一个完整的攻略,介绍如何使用Java数据库唯一…

    Java 2023年5月20日
    00
  • Spring Boot实现热部署的五种方式

    Spring Boot是一个快速开发框架,可以帮助开发人员快速构建Web应用程序。在开发过程中,经常需要修改代码并重新编译,这会浪费很多时间。为了提高开发效率,Spring Boot提供了热部署功能,可以在不重启应用程序的情况下实时更新代码。本文将介绍Spring Boot实现热部署的五种方式,并提供两个示例。 方式一:使用Spring Boot DevTo…

    Java 2023年5月15日
    00
  • spring整合redis以及使用RedisTemplate的方法

    Spring整合Redis以及使用RedisTemplate的方法 什么是Redis? Redis是一个开源的,高级的、基于内存的NoSQL数据库,常用于缓存、队列、分布式锁等应用。它支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等。 Spring整合Redis 1. 环境搭建 首先需要引入Spring Data Redis模块,以及Jedis或L…

    Java 2023年5月19日
    00
  • SpringBoot集成Kafka 配置工具类的详细代码

    下面是详细讲解SpringBoot集成Kafka配置工具类的完整攻略。 1、前置要求 在进行SpringBoot集成Kafka之前,需要准备以下环境: Java JDK 8及以上版本 Maven构建工具 Kafka集群及对应的Zookeeper集群 2、添加依赖 在进行SpringBoot集成Kafka之前,需要在pom.xml中添加以下依赖: <de…

    Java 2023年5月20日
    00
  • Spring Security保护用户密码常用方法详解

    Spring Security保护用户密码常用方法详解 前言 在现代的Web开发中,安全性已经成为一个重要的问题。尤其是涉及到用户密码的相关处理,更是需要严格保护。 Spring Security是一个开源的Web安全框架,它提供了一些集成化的解决方案,可以快速、轻松地保护我们的应用程序的安全。这篇文章将介绍Spring Security保护用户密码的一些常…

    Java 2023年5月20日
    00
  • SrpingDruid数据源加密数据库密码的示例代码

    首先我们需要明确什么是SpringDruid数据源,以及为什么需要加密数据库密码。 SpringDruid数据源是一种基于Spring框架和阿里巴巴德鲁伊连接池的数据源,它能够提高数据库的连接性能、可用性和稳定性。 在实际应用中,我们通常需要在配置文件中配置数据库连接信息,包括数据库用户名和密码。然而,这样做存在一定风险,因为配置文件可能会被非授权的人员获取…

    Java 2023年5月20日
    00
  • Java封装数组实现包含、搜索和删除元素操作详解

    Java封装数组实现包含、搜索和删除元素操作详解 简介 在Java中,数组是一种重要的数据类型,我们经常需要对数组进行操作。本攻略将讲解如何通过封装的方式实现数组的包含、搜索和删除元素操作,并提供相关的示例代码以供参考。 封装数组 在Java中,我们可以通过创建一个类来封装数组。对于数组的操作,则可以通过类的公共方法来实现。下面是一个示例类的结构: publ…

    Java 2023年5月26日
    00
  • NUXT SSR初级入门笔记(小结)

    NUXT SSR初级入门笔记(小结) 1. 什么是NUXT SSR NUXT SSR(Server-Side Rendering)是基于Vue.js的一个SSR框架。NUXT SSR可以将Vue组件实例渲染成HTML字符串,然后将这个HTML字符串响应给浏览器,从而让浏览器更快地呈现页面。通过NUXT SSR,可以提高页面的首屏渲染速度和SEO优化。 2. …

    Java 2023年6月15日
    00
合作推广
合作推广
分享本页
返回顶部