Springboot?多租戶SaaS搭建方案
正文
相信大家對(duì)SaaS架構(gòu)都有所了解,這里也不過(guò)多介紹,讓我們直奔主題。
技術(shù)框架
springboot版本為2.3.4.RELEASE
持久層采用JPA
租戶Model設(shè)計(jì)
因?yàn)閟aas應(yīng)用所有租戶都使用同個(gè)服務(wù)和數(shù)據(jù)庫(kù),為隔離好租戶數(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è)父類(lèi)。最后通過(guò)tenantId來(lái)區(qū)分?jǐn)?shù)據(jù)是哪個(gè)租戶。
sql租戶數(shù)據(jù)過(guò)濾
按往常,表建好就該接著對(duì)應(yīng)的模塊的CURD。但saas應(yīng)用最基本的要求就是租戶數(shù)據(jù)隔離,就是公司B的人不能看到公司A的數(shù)據(jù),怎么過(guò)濾呢,這里上面我們建立的BaseSaasEntity就起作用了,通過(guò)區(qū)分當(dāng)前請(qǐng)求是來(lái)自那個(gè)公司后,在所有tenant業(yè)務(wù)sql中加上where tenant=?就實(shí)現(xiàn)了租戶數(shù)據(jù)過(guò)濾。
Hibernate filter
如果讓我們?cè)跇I(yè)務(wù)中都去加上租戶sql過(guò)濾代碼,那工作量不僅大,而且出錯(cuò)的概率也很大。理想是過(guò)濾sql拼接統(tǒng)一放在一起處理,在租戶業(yè)務(wù)接口開(kāi)啟sql過(guò)濾。因?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)新的方式來(lái)處理具有“顯性(visibility)”規(guī)則的數(shù)據(jù),那就是使用Hibernate 過(guò)濾器。Hibernate 過(guò)濾器是全局有效的、具有名字、可以帶參數(shù)的過(guò)濾器,對(duì)于某個(gè)特定的 Hibernate session 您可以選擇是否啟用(或禁用)某個(gè)過(guò)濾器。
這里我們通過(guò)@FilterDef和@Filter預(yù)先定義了一個(gè)sql過(guò)濾條件。然后通過(guò)一個(gè)@TenantFilter注解來(lái)標(biāo)識(shí)接口需要進(jìn)行數(shù)據(jù)過(guò)濾
@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必須要開(kāi)啟事務(wù),這里默認(rèn)是只讀事務(wù)。 最后定義一個(gè)切面來(lái)激活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,開(kāi)啟filter,這樣租戶數(shù)據(jù)隔離就大功告成了,只需要在租戶業(yè)務(wù)接口上增加@TenantFilter注解即可, 開(kāi)發(fā)只用關(guān)心業(yè)務(wù)代碼。上圖中的TenantContext是當(dāng)前線程租戶context,通過(guò)和前端約定好,接口請(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(); } }
分庫(kù)
隨著租戶數(shù)量的增加,mysql單庫(kù)單表的數(shù)據(jù)肯定會(huì)達(dá)到瓶頸,這里只采用分庫(kù)的手段。利用多數(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(); } }
通過(guò)實(shí)現(xiàn)AbstractRoutingDataSource來(lái)聲明一個(gè)動(dòng)態(tài)路由數(shù)據(jù)源,在框架使用datesource前,spring會(huì)調(diào)用determineCurrentLookupKey()方法來(lái)確定使用哪個(gè)數(shù)據(jù)源。這里的DataSourceContext和上面的TenantContext類(lèi)似,在攔截器中獲取到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è)開(kāi)源的Java框架,它簡(jiǎn)化了基于Spring的應(yīng)用程序的開(kāi)發(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引入依賴(lài)和常量類(lèi)創(chuàng)建,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-11-11Java中Klass模型與類(lèi)加載的詳細(xì)機(jī)制
這篇文章主要介紹了Java中Klass模型與類(lèi)加載的詳細(xì)機(jī)制,java語(yǔ)言是在jvm中運(yùn)行而jvm是不認(rèn)識(shí)java代碼的我們使用javac編譯的class文件jvm是不認(rèn)識(shí)的 所以有一個(gè)類(lèi)加載的動(dòng)作 這個(gè)動(dòng)作就是把class字節(jié)碼拼裝成一個(gè)klass類(lèi)型,需要的朋友可以參考下2023-08-08SpringBoot基本web開(kāi)發(fā)demo過(guò)程解析
這篇文章主要介紹了SpringBoot基本web開(kāi)發(fā)demo過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11