96SEO 2026-02-19 17:42 12
。

最近雷袭又接到一项新的挑战#xff1a;了解SAAS模型#xff0c;考虑怎么将公司的产品转换成多租…
随着公司业务战略的发展相关的软件服务也逐步的向多元化转变之前是单纯的拿项目赚人工钱现在开始向产品化\服务化转变。
最近雷袭又接到一项新的挑战了解SAAS模型考虑怎么将公司的产品转换成多租户架构。
经过一番百度雷袭对多租户架构总算有了一番了解以下是整理的笔记。
多租户架构是一种软件架构用于实现多用户环境下使用相同的系统或程序组件时能保证用户之间数据的隔离性。
简单说就是使用共用的数据中心通过单一系统架构与服务提供多数客户端相同甚至可定制化的服务并且保障客户的数据隔离。
一个支持多租户架构的系统需要在设计上对它的数据和配置进行虚拟分区从而使系统的每个租户或组织都能够使用一个单独的系统实例每个租户都可以根据自己的需求对租用的系统实例进行个性化配置。
多租户技术的实现重点在于不同租户间应用程序环境的隔离以及数据的隔离使得不同租户间应用程序不会相互干扰。
应用程序可通过进程隔离或者多种运维工具实现数据存储上的隔离方案则是有三种
1、独立数据库优点是独立性最高缺点是数据库较多购置和维护成本高。
2、共享数据库隔离数据架构同一个数据库实例内多个用户/schema来对应多个租户优点是单实例可以支持更多租户缺点是数据恢复比较困难。
3、共享数据库共享数据结构物理分表表分区或者在表中通过字段区分优点是成本最低实现难度低缺点是数据隔离程度低。
第三种其实雷袭已经试过了之前的博客里就提到了表分区分表的实现方式这里不多缀述今天雷袭想试试前面两种因此不得不解决的一个问题如何实现同一个项目中数据源的动态切换
雷袭在网上查阅了很多资料最终找到了两种合适的方式实现一种是通过AOP来实现另一种是通过Filter实现以下是实现的方式说明。
数据库key即保存Map中的key(保证唯一并且和DataSourceType中的枚举项保持一致包括大小写);
SAAS_MASTER.sys_db_info.db_name
SAAS_MASTER.sys_db_info.driver_class_name
SAAS_MASTER.sys_db_info.password
SAAS_MASTER.sys_db_info.username
(id,url,username,password,driver_class_name,db_name,db_key,status,remark)
jdbc:dm://127.0.0.1:5236/SAAS_DEV,
(id,url,username,password,driver_class_name,db_name,db_key,status,remark)
jdbc:dm://127.0.0.1:5236/SAAS_UAT,
2、创建一个springboot项目项目环境为JDK17以下是相关配置和代码
xmlnshttp://maven.apache.org/POM/4.0.0
xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance
xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionpackagingjar/packagingparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion3.3.2/version
--/parentgroupIdcom.leixi.hub.saasdb/groupIdartifactIdleixi-saas-db/artifactIdversion1.0-SNAPSHOT/versionnameleixi-saas-db/namedescription用于动态切换数据源/descriptionpropertiesmaven.compiler.source17/maven.compiler.sourcemaven.compiler.target17/maven.compiler.targetproject.build.sourceEncodingUTF-8/project.build.sourceEncodinghutool.version5.8.15/hutool.versionmysql.version8.0.28/mysql.versiondruid.version1.2.16/druid.versionmybatis-plus.version3.5.3.1/mybatis-plus.versionlombok-mapstruct-binding.version0.2.0/lombok-mapstruct-binding.version/propertiesdependencies!--
--dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId!--编译测试环境不打包在lib--scopeprovided/scope/dependency!--
--dependencygroupIdorg.projectlombok/groupIdartifactIdlombok-mapstruct-binding/artifactIdversion${lombok-mapstruct-binding.version}/versionscopeprovided/scope/dependency!--
--dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion${hutool.version}/version/dependency!--
--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--
--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId/dependency!--
--dependencygroupIdcom.dameng/groupIdartifactIdDmJdbcDriver18/artifactIdversion8.1.1.193/version/dependency!--
--dependencygroupIdcom.alibaba/groupIdartifactIddruid-spring-boot-starter/artifactIdversion${druid.version}/version/dependencydependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion2.0.40/version/dependency!--
--dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-spring-boot3-starter/artifactIdversion3.5.5/version/dependency/dependenciesbuildpluginsplugingroupIdorg.springframework.boot/groupIdartifactIdspring-boot-maven-plugin/artifactId/pluginplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-archetype-plugin/artifactIdversion3.0.0/version/plugin/plugins/build/projectapplication.yml
com.alibaba.druid.pool.DruidDataSourcedriver-class-name:
jdbc:dm://127.0.0.1:5236/SAAS_MASTERusername:
60000time-between-eviction-runs-millis:
60000min-evictable-idle-time-millis:
falsetask:execution:thread-pool:core-size:
classpath:/mapper/**/*Mapper.xmlglobal-config:db-config:#主键类型
返回类型为Map,显示null对应的字段call-setters-on-nulls:
truemap-underscore-to-camel-case:
这个配置会将执行的sql打印出来在开发或测试的时候可以用log-impl:
org.apache.ibatis.logging.stdout.StdOutImpl
data_source_keyload_source_form_db:
true以下是设置数据源的核心代码其原理为在项目启动时先通过LoadDataSourceRunner从数据库中查询相关的数据连接存储在内存中对Controller中的方法添加DataSource注解执行方法时通过注解中的静态枚举切换对应的数据源对指定的数据库进行操作。
com.leixi.hub.saasdb.config;import
com.alibaba.druid.pool.DruidDataSource;
com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
com.leixi.hub.saasdb.entity.SysDbInfo;
org.springframework.beans.BeanUtils;
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import
实现动态数据源根据AbstractRoutingDataSource路由到不同数据源中**
数据源列表多数据源情况下具体使用哪一个数据源由此获取private
{super.setDefaultTargetDataSource(defaultDataSource);super.setTargetDataSources(targetDataSources);this.targetDataSourceMap
DynamicDataSourceContextHolder.getDataSource();}/***
(CollectionUtils.isNotEmpty(dataSources))
{//校验数据库是否可以连接Class.forName(ds.getDriverClassName());DriverManager.getConnection(ds.getUrl(),
ds.getPassword());//定义数据源DruidDataSource
DruidDataSource();BeanUtils.copyProperties(ds,
dataSource);//申请连接时执行validationQuery检测连接是否有效这里建议配置为TRUE防止取到的连接不可用dataSource.setTestOnBorrow(true);//建议配置为true不影响性能并且保证安全性。
//申请连接的时候检测如果空闲时间大于timeBetweenEvictionRunsMillis执行validationQuery检测连接是否有效。
dataSource.setTestWhileIdle(true);//用来检测连接是否有效的sql要求是一个查询语句。
dataSource.setValidationQuery(select
将数据源放入Map中key为数据源名称要和DataSourceType中的枚举项对应包括大小写并且保证唯一this.targetDataSourceMap.put(ds.getDbKey(),
更新数据源配置列表这里主要是从数据源super.setTargetDataSources(this.targetDataSourceMap);//
将TargetDataSources中的连接信息放入resolvedDataSources管理super.afterPropertiesSet();}}
Objects.nonNull(this.targetDataSourceMap)
Objects.nonNull(this.targetDataSourceMap.get(key));}
com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
org.springframework.boot.context.properties.ConfigurationProperties;
org.springframework.context.annotation.Bean;
org.springframework.context.annotation.Configuration;
org.springframework.context.annotation.Primary;import
该数据源是在application配置文件master中所配置的*/BeanConfigurationProperties(spring.datasource)public
DruidDataSourceBuilder.create().build();}/***
DynamicDataSource*/PrimaryBean(name
配置主数据源默认使用该数据源并且主数据源只能配置一个dataSourceMap.put(MASTER_SOURCE_KEY,
配置动态数据源默认使用主数据源如果有从数据源配则使用从数据库中读取源并加载到dataSourceMap中return
DynamicDataSource(defaultDataSource,
createDynamicDataSource()方法dataSourceMap的key保持一致/***
com.leixi.hub.saasdb.config;import
创建一个类用于实现ThreadLocal主要是通过getsetremove方法来获取、设置、删除当前线程对应的数据源。
**
{//此类提供线程局部变量。
这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。
private
dataSourceName);DATASOURCE_HOLDER.set(dataSourceName);}/***
getDataSource());DATASOURCE_HOLDER.remove();}
com.leixi.hub.saasdb.config;import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
com.leixi.hub.saasdb.dao.SysDbInfoMapper;
com.leixi.hub.saasdb.entity.SysDbInfo;
org.springframework.beans.factory.annotation.Value;
org.springframework.boot.CommandLineRunner;
org.springframework.stereotype.Component;
org.springframework.util.CollectionUtils;import
是否启用从库多数据源配置*/Value(${leixi.saas.load_source_form_db:false})private
dynamicDataSource;Resourceprivate
return;refreshDataSource();}/***
LambdaQueryWrapperSysDbInfo().eq(SysDbInfo::getStatus,
(CollectionUtils.isEmpty(dbInfos))
ArrayList();log.info(开始加载数据源);for
(StrUtil.isAllNotBlank(info.getUrl(),
数据库连接地址info.getDriverClassName(),
info.getRemark());}}dynamicDataSource.createDataSource(ds);log.info(数据源加载完成);}
com.leixi.hub.saasdb.config;import
优先级先方法后类如果方法覆盖了类上的数据源类型以方法的为准否则以类上的为准*
Retention(RetentionPolicy.RUNTIME)
com.leixi.hub.saasdb.config;import
io.micrometer.common.util.StringUtils;
org.aspectj.lang.ProceedingJoinPoint;
org.aspectj.lang.annotation.Around;
org.aspectj.lang.annotation.Aspect;
org.aspectj.lang.annotation.Pointcut;
org.springframework.core.annotation.AnnotationUtils;
org.springframework.core.annotation.Order;
org.springframework.stereotype.Component;
org.aspectj.lang.reflect.MethodSignature;
注解Pointcut(annotation(com.leixi.hub.saasdb.config.DataSource)
within(com.leixi.hub.saasdb.config.DataSource))public
StringUtils.isNotEmpty(dataSource.value().name()))
将用户自定义配置的数据源添加到线程局部变量中DynamicDataSourceContextHolder.setDataSource(dataSource.value().name());}try
在执行完方法之后销毁数据源DynamicDataSourceContextHolder.removeDataSource();}}/***
注意当类上配置后方法上没有该注解那么当前类中的所有方法都将使用类上配置的数据源*/public
getDataSource(ProceedingJoinPoint
AnnotationUtils.findAnnotation(signature.getMethod(),
AnnotationUtils.findAnnotation(signature.getDeclaringType(),
com.leixi.hub.saasdb.entity;import
com.baomidou.mybatisplus.annotation.TableField;
com.baomidou.mybatisplus.annotation.TableName;
lombok.experimental.Accessors;import
定义一个key用于作为DynamicDataSource中Map中的key。
*
这里的key需要和DataSourceType中的枚举项保持一致*/private
com.leixi.hub.saasdb.dao;import
com.baomidou.mybatisplus.core.mapper.BaseMapper;
com.leixi.hub.saasdb.entity.SysDbInfo;
org.apache.ibatis.annotations.Mapper;Mapper
com.leixi.hub.saasdb.dao;import
com.baomidou.mybatisplus.core.mapper.BaseMapper;
org.apache.ibatis.annotations.Mapper;
org.apache.ibatis.annotations.Param;import
http://mybatis.org/dtd/mybatis-3-mapper.dtd
namespacecom.leixi.hub.saasdb.dao.CommonMapperselect
resultTypejava.util.Map${sql}/selectupdate
com.leixi.hub.saasdb.controller;import
com.leixi.hub.saasdb.config.DataSource;
com.leixi.hub.saasdb.config.DataSourceType;
com.leixi.hub.saasdb.dao.CommonMapper;
org.springframework.beans.factory.annotation.Autowired;
org.springframework.web.bind.annotation.GetMapping;
org.springframework.web.bind.annotation.RequestParam;
org.springframework.web.bind.annotation.RestController;/****
commonMapper;GetMapping(/getDataBySqlFromMaster)DataSource(DataSourceType.MASTER)public
getDataBySqlFromMaster(RequestParam(value
commonMapper.getDataBySql(sql);}GetMapping(/getDataBySqlFromUat)DataSource(DataSourceType.UAT)public
getDataBySqlFromSlave(RequestParam(value
commonMapper.getDataBySql(sql);}GetMapping(/getDataBySql)public
getDataBySql(RequestParam(value
commonMapper.getDataBySql(sql);}}3、启动项目通过Postman测试结果和预期一致
上述的方法虽然有效但多少有些固化了为何一只有添加了注解的类或方法才能动态切换数据源需要对已有代码进行修改那就多少会有漏改少改的位置二来可选的数据源在枚举或代码中写死了假设在数据库里新增了一个数据源则程序中必须要做相应的调整可扩展性不高综合考虑后我决定再用过滤器的方式试试。
过滤器的原理其实和AOP相似只是在Header中添加一个数据库的Key在过滤器中根据这个Key来指定数据源实现代码如下
com.leixi.hub.saasdb.filter;import
com.leixi.hub.saasdb.config.DynamicDataSourceContextHolder;
io.micrometer.common.util.StringUtils;
jakarta.servlet.ServletException;
jakarta.servlet.ServletRequest;
jakarta.servlet.ServletResponse;
jakarta.servlet.http.HttpServletRequest;
org.springframework.core.annotation.Order;import
httpRequest.getHeader(dataSourceKey);if
(StringUtils.isNotEmpty(dataSource))
{DynamicDataSourceContextHolder.setDataSource(dataSource);chain.doFilter(request,
{DynamicDataSourceContextHolder.removeDataSource();}}package
org.springframework.beans.factory.annotation.Value;
org.springframework.boot.web.servlet.FilterRegistrationBean;
org.springframework.context.annotation.Bean;
org.springframework.context.annotation.Configuration;/****
{Value(${leixi.saas.data_source_key:data_source_key})private
FilterRegistrationBeanDataSourceChangeFilter
licenseValidationFilterRegistration()
{FilterRegistrationBeanDataSourceChangeFilter
FilterRegistrationBean();registration.setFilter(new
DataSourceChangeFilter(dataSourceKey));registration.addUrlPatterns(/*);
当前这个项目是已经实现了多数据源的动态切换那么如果想让其他项目也支持应该怎么办呢咱可以把这个项目打成一个jar包然后让其他项目引入依赖即可改动如下
!--可以打成供其他包依赖的包--buildpluginsplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-archetype-plugin/artifactIdversion3.0.0/version/pluginplugingroupIdorg.apache.maven.plugins/groupIdartifactIdmaven-compiler-plugin/artifactIdversion3.11.0/versionconfigurationsource17/sourcetarget17/targetencodingUTF-8/encoding/configuration/plugin/pluginsresourcesresourcedirectorysrc/main/resources/config/directoryfilteringtrue/filteringexcludesexclude*/exclude/excludes/resource/resources/build
3、打包完成后可以在target中看到对应的jar文件也可以在其他项目中引用该文件如下
Demo比较简单手法也相对稚嫩希望不会贻笑大方也希望新手看到这个Demo能有所启发。
这次实践也并非一蹴而就的离不开大佬们的支持和点拨雷袭在网上找了很多资料以下这篇博客是最有价值的可以说雷袭完全是照抄了他的成果这里附上原文链接拜谢大佬
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback