Springboot多租户SaaS如何搭建
技术框架
springboot版本为2.3.4.RELEASE
持久层采用JPA
租户Model设计
因为saas应用所有租户都使用同个服务和数据库,为隔离好租户数据,这里创建一个BaseSaasEntity
public abstract class BaseSaasEntity { @JsonIgnore @Column(nullable = false, updatable = false) protected Long tenantId; }
里面只有一个字段tenantId,对应的就是租户Id,所有租户业务entity都继承这个父类。最后通过tenantId来区分数据是哪个租户。
sql租户数据过滤
按往常,表建好就该接着对应的模块的CURD。但saas应用最基本的要求就是租户数据隔离,就是公司B的人不能看到公司A的数据,怎么过滤呢,这里上面我们建立的BaseSaasEntity就起作用了,通过区分当前请求是来自那个公司后,在所有tenant业务sql中加上where tenant=?就实现了租户数据过滤。
Hibernate filter
如果让我们在业务中都去加上租户sql过滤代码,那工作量不仅大,而且出错的概率也很大。理想是过滤sql拼接统一放在一起处理,在租户业务接口开启sql过滤。因为JPA是有hibernate实现的,这里我们可以利用hibernate的一些功能
@MappedSuperclass @Data @FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantId", type = "long")}) @Filter(condition = "tenant_id=:tenantId", name = "tenantFilter") public abstract class BaseSaasEntity { @JsonIgnore @Column(nullable = false, updatable = false) protected Long tenantId; @PrePersist public void onPrePersist() { if (getTenantId() != null) { return; } Long tenantId = TenantContext.getTenantId(); Check.notNull(tenantId, "租户不存在"); setTenantId(tenantId); } }
Hibernate3 提供了一种创新的方式来处理具有“显性(visibility)”规则的数据,那就是使用Hibernate 过滤器。Hibernate 过滤器是全局有效的、具有名字、可以带参数的过滤器,对于某个特定的 Hibernate session 您可以选择是否启用(或禁用)某个过滤器。
这里我们通过@FilterDef和@Filter预先定义了一个sql过滤条件。然后通过一个@TenantFilter注解来标识接口需要进行数据过滤
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Transactional public @interface TenantFilter { boolean readOnly() default true; }
可以看出这个接口是放在方法上,对应的就是Controller层。@Transactional增加事务注解的意义是因为激活hibernate filter必须要开启事务,这里默认是只读事务。 最后定义一个切面来激活filter
@Aspect @Slf4j @RequiredArgsConstructor public class TenantSQLAspect { private static final String FILTER_NAME = "tenantFilter"; private final EntityManager entityManager; @SneakyThrows @Around("@annotation(com.lvjusoft.njcommon.annotation.TenantFilter)") public Object aspect(ProceedingJoinPoint joinPoint) { Session session = entityManager.unwrap(Session.class); try { Long tenantId = TenantContext.getTenantId(); Check.notNull(tenantId, "租户不存在"); session.enableFilter(FILTER_NAME).setParameter("tenantId", tenantId); return joinPoint.proceed(); } finally { session.disableFilter(FILTER_NAME); } } }
这里切面的对象就是刚才自定义的@TenantFilter注解,在方法执行前拿到当前租户id,开启filter,这样租户数据隔离就大功告成了,只需要在租户业务接口上增加@TenantFilter注解即可, 开发只用关心业务代码。上图中的TenantContext是当前线程租户context,通过和前端约定好,接口请求头中增加租户id,服务端利用拦截器把获取到的租户id缓存在ThreadLocal中
public class IdentityInterceptor extends HandlerInterceptorAdapter { public IdentityInterceptor() { log.info("IdentityInterceptor init"); } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader(AuthConstant.USER_TOKEN_HEADER_NAME); UserContext.setToken(token); String tenantId = request.getHeader(AuthConstant.TENANT_TOKEN_HEADER_NAME); TenantContext.setTenantUUId(tenantId); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { UserContext.clear(); TenantContext.clear(); } }
分库
随着租户数量的增加,mysql单库单表的数据肯定会达到瓶颈,这里只采用分库的手段。利用多数据源,将租户和数据源进行多对一的映射。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource { private Map<Object, Object> targetDataSources; public DynamicRoutingDataSource() { targetDataSources =new HashMap<>(); DruidDataSource druidDataSource1 = new DruidDataSource(); druidDataSource1.setUsername("username"); druidDataSource1.setPassword("password"); druidDataSource1.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&useUnicode=true&characterEncoding=utf-8"); targetDataSources.put("db1",druidDataSource1); DruidDataSource druidDataSource2 = new DruidDataSource(); druidDataSource2.setUsername("username"); druidDataSource2.setPassword("password"); druidDataSource2.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&useUnicode=true&characterEncoding=utf-8"); targetDataSources.put("db2",druidDataSource1); this.targetDataSources = targetDataSources; super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } public void addDataSource(String key, DataSource dataSource) { if (targetDataSources.containsKey(key)) { throw new IllegalArgumentException("dataSource key exist"); } targetDataSources.put(key, dataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { return DataSourceContext.getSource(); } }
通过实现AbstractRoutingDataSource来声明一个动态路由数据源,在框架使用datesource前,spring会调用determineCurrentLookupKey()方法来确定使用哪个数据源。这里的DataSourceContext和上面的TenantContext类似,在拦截器中获取到tenantInfo后,找到当前租户对应的数据源key并设置在ThreadLocal中。
以上是Springboot多租户SaaS如何搭建的详细内容。更多信息请关注PHP中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

热门话题

Jasypt介绍Jasypt是一个java库,它允许开发员以最少的努力为他/她的项目添加基本的加密功能,并且不需要对加密工作原理有深入的了解用于单向和双向加密的高安全性、基于标准的加密技术。加密密码,文本,数字,二进制文件...适合集成到基于Spring的应用程序中,开放API,用于任何JCE提供程序...添加如下依赖:com.github.ulisesbocchiojasypt-spring-boot-starter2.1.1Jasypt好处保护我们的系统安全,即使代码泄露,也可以保证数据源的

使用场景1、下单成功,30分钟未支付。支付超时,自动取消订单2、订单签收,签收后7天未进行评价。订单超时未评价,系统默认好评3、下单成功,商家5分钟未接单,订单取消4、配送超时,推送短信提醒……对于延时比较长的场景、实时性不高的场景,我们可以采用任务调度的方式定时轮询处理。如:xxl-job今天我们采

一、Redis实现分布式锁原理为什么需要分布式锁在聊分布式锁之前,有必要先解释一下,为什么需要分布式锁。与分布式锁相对就的是单机锁,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通常会使用一把锁来互斥以保证共享变量的正确性,其使用范围是在同一个进程中。如果换做是多个进程,需要同时操作一个共享资源,如何互斥呢?现在的业务应用通常是微服务架构,这也意味着一个应用会部署多个进程,多个进程如果需要修改MySQL中的同一行记录,为了避免操作乱序导致脏数据,此时就需要引入分布式锁了。想要实现分

springboot读取文件,打成jar包后访问不到最新开发出现一种情况,springboot打成jar包后读取不到文件,原因是打包之后,文件的虚拟路径是无效的,只能通过流去读取。文件在resources下publicvoidtest(){Listnames=newArrayList();InputStreamReaderread=null;try{ClassPathResourceresource=newClassPathResource("name.txt");Input

在Springboot+Mybatis-plus不使用SQL语句进行多表添加操作我所遇到的问题准备工作在测试环境下模拟思维分解一下:创建出一个带有参数的BrandDTO对象模拟对后台传递参数我所遇到的问题我们都知道,在我们使用Mybatis-plus中进行多表操作是极其困难的,如果你不使用Mybatis-plus-join这一类的工具,你只能去配置对应的Mapper.xml文件,配置又臭又长的ResultMap,然后再去写对应的sql语句,这种方法虽然看上去很麻烦,但具有很高的灵活性,可以让我们

SpringBoot和SpringMVC都是Java开发中常用的框架,但它们之间有一些明显的差异。本文将探究这两个框架的特点和用途,并对它们的差异进行比较。首先,我们来了解一下SpringBoot。SpringBoot是由Pivotal团队开发的,它旨在简化基于Spring框架的应用程序的创建和部署。它提供了一种快速、轻量级的方式来构建独立的、可执行

1、自定义RedisTemplate1.1、RedisAPI默认序列化机制基于API的Redis缓存实现是使用RedisTemplate模板进行数据缓存操作的,这里打开RedisTemplate类,查看该类的源码信息publicclassRedisTemplateextendsRedisAccessorimplementsRedisOperations,BeanClassLoaderAware{//声明了key、value的各种序列化方式,初始值为空@NullableprivateRedisSe

在项目中,很多时候需要用到一些配置信息,这些信息在测试环境和生产环境下可能会有不同的配置,后面根据实际业务情况有可能还需要再做修改。我们不能将这些配置在代码中写死,最好是写到配置文件中,比如可以把这些信息写到application.yml文件中。那么,怎么在代码里获取或者使用这个地址呢?有2个方法。方法一:我们可以通过@Value注解的${key}即可获取配置文件(application.yml)中和key对应的value值,这个方法适用于微服务比较少的情形方法二:在实际项目中,遇到业务繁琐,逻
