Springboot?多租戶SaaS搭建方案
正文
相信大家對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,對應(yīng)的就是租戶Id,所有租戶業(yè)務(wù)entity都繼承這個(gè)父類。最后通過tenantId來區(qū)分?jǐn)?shù)據(jù)是哪個(gè)租戶。
sql租戶數(shù)據(jù)過濾
按往常,表建好就該接著對應(yīng)的模塊的CURD。但saas應(yīng)用最基本的要求就是租戶數(shù)據(jù)隔離,就是公司B的人不能看到公司A的數(shù)據(jù),怎么過濾呢,這里上面我們建立的BaseSaasEntity就起作用了,通過區(qū)分當(dāng)前請求是來自那個(gè)公司后,在所有tenant業(yè)務(wù)sql中加上where tenant=?就實(shí)現(xiàn)了租戶數(shù)據(jù)過濾。
Hibernate filter
如果讓我們在業(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ù)的過濾器,對于某個(gè)特定的 Hibernate session 您可以選擇是否啟用(或禁用)某個(gè)過濾器。
這里我們通過@FilterDef和@Filter預(yù)先定義了一個(gè)sql過濾條件。然后通過一個(gè)@TenantFilter注解來標(biāo)識接口需要進(jìn)行數(shù)據(jù)過濾
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional
public @interface TenantFilter {
boolean readOnly() default true;
}可以看出這個(gè)接口是放在方法上,對應(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);
}
}
}這里切面的對象就是剛才自定義的@TenantFilter注解,在方法執(zhí)行前拿到當(dāng)前租戶id,開啟filter,這樣租戶數(shù)據(jù)隔離就大功告成了,只需要在租戶業(yè)務(wù)接口上增加@TenantFilter注解即可, 開發(fā)只用關(guān)心業(yè)務(wù)代碼。上圖中的TenantContext是當(dāng)前線程租戶context,通過和前端約定好,接口請求頭中增加租戶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ù)肯定會達(dá)到瓶頸,這里只采用分庫的手段。利用多數(shù)據(jù)源,將租戶和數(shù)據(jù)源進(jìn)行多對一的映射。
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會調(diào)用determineCurrentLookupKey()方法來確定使用哪個(gè)數(shù)據(jù)源。這里的DataSourceContext和上面的TenantContext類似,在攔截器中獲取到tenantInfo后,找到當(dāng)前租戶對應(yīng)的數(shù)據(jù)源key并設(shè)置在ThreadLocal中。
結(jié)尾
到這里一個(gè)多租戶的基礎(chǔ)應(yīng)用就搭建好了。
到此這篇關(guān)于Springboot 多租戶SaaS方案的文章就介紹到這了,更多相關(guān)Springboot 多租戶SaaS內(nèi)容請搜索腳本之家以前的文章或繼續(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è)單一的流,常用于處理嵌套集合和一對多映射,需要的朋友可以參考下2024-12-12
16個(gè)SpringBoot擴(kuò)展接口的總結(jié)和實(shí)例
Spring Boot是一個(gè)開源的Java框架,它簡化了基于Spring的應(yīng)用程序的開發(fā)和部署,它提供了許多強(qiáng)大的特性和擴(kuò)展接口,本文給大家介紹了16個(gè)常用的Spring Boot擴(kuò)展接口,需要的朋友可以參考下2023-09-09
Java多線程之循環(huán)柵欄技術(shù)CyclicBarrier使用探索
這篇文章主要介紹了Java多線程之循環(huán)柵欄技術(shù)CyclicBarrier,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2024-01-01
springboot2.5.6集成RabbitMq實(shí)現(xiàn)Topic主題模式(推薦)
這篇文章主要介紹了springboot2.5.6集成RabbitMq實(shí)現(xiàn)Topic主題模式(推薦),pom.xml引入依賴和常量類創(chuàng)建,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-11-11

