您好,登錄后才能下訂單哦!
這篇文章主要介紹“Springboot多租戶SaaS如何搭建”,在日常操作中,相信很多人在Springboot多租戶SaaS如何搭建問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Springboot多租戶SaaS如何搭建”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
springboot版本為2.3.4.RELEASE
持久層采用JPA
因為saas應用所有租戶都使用同個服務和數據庫,為隔離好租戶數據,這里創建一個BaseSaasEntity
public abstract class BaseSaasEntity { @JsonIgnore @Column(nullable = false, updatable = false) protected Long tenantId; }
里面只有一個字段tenantId,對應的就是租戶Id,所有租戶業務entity都繼承這個父類。最后通過tenantId來區分數據是哪個租戶。
按往常,表建好就該接著對應的模塊的CURD。但saas應用最基本的要求就是租戶數據隔離,就是公司B的人不能看到公司A的數據,怎么過濾呢,這里上面我們建立的BaseSaasEntity就起作用了,通過區分當前請求是來自那個公司后,在所有tenant業務sql中加上where tenant=?就實現了租戶數據過濾。
如果讓我們在業務中都去加上租戶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如何搭建”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。