首頁  >  文章  >  Java  >  Springboot多租戶SaaS如何搭建

Springboot多租戶SaaS如何搭建

WBOY
WBOY轉載
2023-05-12 16:49:061599瀏覽

技術框架

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中文網其他相關文章!

陳述:
本文轉載於:yisu.com。如有侵權,請聯絡admin@php.cn刪除