Springboot?多租戶SaaS搭建方案
正文
相信大家對(duì)SaaS架構(gòu)都有所了解,這里也不過多介紹,讓我們直奔主題。
技術(shù)框架
springboot版本為2.3.4.RELEASE
持久層采用JPA
租戶Model設(shè)計(jì)
因?yàn)閟aas應(yīng)用所有租戶都使用同個(gè)服務(wù)和數(shù)據(jù)庫,為隔離好租戶數(shù)據(jù),這里創(chuàng)建一個(gè)BaseSaasEntity
public abstract class BaseSaasEntity { @JsonIgnore @Column(nullable = false, updatable = false) protected Long tenantId; }
里面只有一個(gè)字段tenantId,對(duì)應(yīng)的就是租戶Id,所有租戶業(yè)務(wù)entity都繼承這個(gè)父類。最后通過tenantId來區(qū)分?jǐn)?shù)據(jù)是哪個(gè)租戶。
sql租戶數(shù)據(jù)過濾
按往常,表建好就該接著對(duì)應(yīng)的模塊的CURD。但saas應(yīng)用最基本的要求就是租戶數(shù)據(jù)隔離,就是公司B的人不能看到公司A的數(shù)據(jù),怎么過濾呢,這里上面我們建立的BaseSaasEntity就起作用了,通過區(qū)分當(dāng)前請(qǐng)求是來自那個(gè)公司后,在所有tenant業(yè)務(wù)sql中加上where tenant=?就實(shí)現(xiàn)了租戶數(shù)據(jù)過濾。
Hibernate filter
如果讓我們?cè)跇I(yè)務(wù)中都去加上租戶sql過濾代碼,那工作量不僅大,而且出錯(cuò)的概率也很大。理想是過濾sql拼接統(tǒng)一放在一起處理,在租戶業(yè)務(wù)接口開啟sql過濾。因?yàn)镴PA是有hibernate實(shí)現(xiàn)的,這里我們可以利用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 提供了一種創(chuàng)新的方式來處理具有“顯性(visibility)”規(guī)則的數(shù)據(jù),那就是使用Hibernate 過濾器。Hibernate 過濾器是全局有效的、具有名字、可以帶參數(shù)的過濾器,對(duì)于某個(gè)特定的 Hibernate session 您可以選擇是否啟用(或禁用)某個(gè)過濾器。
這里我們通過@FilterDef和@Filter預(yù)先定義了一個(gè)sql過濾條件。然后通過一個(gè)@TenantFilter注解來標(biāo)識(shí)接口需要進(jìn)行數(shù)據(jù)過濾
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Transactional public @interface TenantFilter { boolean readOnly() default true; }
可以看出這個(gè)接口是放在方法上,對(duì)應(yīng)的就是Controller層。@Transactional增加事務(wù)注解的意義是因?yàn)榧せ頷ibernate filter必須要開啟事務(wù),這里默認(rèn)是只讀事務(wù)。 最后定義一個(gè)切面來激活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); } } }
這里切面的對(duì)象就是剛才自定義的@TenantFilter注解,在方法執(zhí)行前拿到當(dāng)前租戶id,開啟filter,這樣租戶數(shù)據(jù)隔離就大功告成了,只需要在租戶業(yè)務(wù)接口上增加@TenantFilter注解即可, 開發(fā)只用關(guān)心業(yè)務(wù)代碼。上圖中的TenantContext是當(dāng)前線程租戶context,通過和前端約定好,接口請(qǐng)求頭中增加租戶id,服務(wù)端利用攔截器把獲取到的租戶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(); } }
分庫
隨著租戶數(shù)量的增加,mysql單庫單表的數(shù)據(jù)肯定會(huì)達(dá)到瓶頸,這里只采用分庫的手段。利用多數(shù)據(jù)源,將租戶和數(shù)據(jù)源進(jìn)行多對(duì)一的映射。
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(); } }
通過實(shí)現(xiàn)AbstractRoutingDataSource來聲明一個(gè)動(dòng)態(tài)路由數(shù)據(jù)源,在框架使用datesource前,spring會(huì)調(diào)用determineCurrentLookupKey()方法來確定使用哪個(gè)數(shù)據(jù)源。這里的DataSourceContext和上面的TenantContext類似,在攔截器中獲取到tenantInfo后,找到當(dāng)前租戶對(duì)應(yīng)的數(shù)據(jù)源key并設(shè)置在ThreadLocal中。
結(jié)尾
到這里一個(gè)多租戶的基礎(chǔ)應(yīng)用就搭建好了。
到此這篇關(guān)于Springboot 多租戶SaaS方案的文章就介紹到這了,更多相關(guān)Springboot 多租戶SaaS內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一文詳細(xì)解析Java?8?Stream?API中的flatMap方法
這篇文章主要介紹了Java?8?Stream?API中的flatMap方法的相關(guān)資料,flatMap方法是Java?StreamAPI中的重要中間操作,用于將流中的每個(gè)元素轉(zhuǎn)換為一個(gè)新的流,并將多個(gè)流合并為一個(gè)單一的流,常用于處理嵌套集合和一對(duì)多映射,需要的朋友可以參考下2024-12-1216個(gè)SpringBoot擴(kuò)展接口的總結(jié)和實(shí)例
Spring Boot是一個(gè)開源的Java框架,它簡化了基于Spring的應(yīng)用程序的開發(fā)和部署,它提供了許多強(qiáng)大的特性和擴(kuò)展接口,本文給大家介紹了16個(gè)常用的Spring Boot擴(kuò)展接口,需要的朋友可以參考下2023-09-09Java多線程之循環(huán)柵欄技術(shù)CyclicBarrier使用探索
這篇文章主要介紹了Java多線程之循環(huán)柵欄技術(shù)CyclicBarrier,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2024-01-01springboot2.5.6集成RabbitMq實(shí)現(xiàn)Topic主題模式(推薦)
這篇文章主要介紹了springboot2.5.6集成RabbitMq實(shí)現(xiàn)Topic主題模式(推薦),pom.xml引入依賴和常量類創(chuàng)建,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-11-11