>  기사  >  Java  >  Springboot 다중 테넌트 SaaS를 구축하는 방법

Springboot 다중 테넌트 SaaS를 구축하는 방법

WBOY
WBOY앞으로
2023-05-12 16:49:061595검색

기술 프레임워크

springboot 버전은 2.3.4입니다.RELEASE

지속성 계층은 JPA를 채택합니다

테넌트 모델 디자인

Saas 애플리케이션의 모든 테넌트는 테넌트 데이터를 격리하기 위해 동일한 서비스와 데이터베이스를 사용하기 때문에 BaseSaasEntity

public abstract class BaseSaasEntity {
    @JsonIgnore
    @Column(nullable = false, updatable = false)
    protected Long tenantId;
    }

에는 테넌트 ID에 해당하는 테넌트 ID 필드가 하나만 있습니다. 모든 테넌트 비즈니스 엔터티는 이 상위 클래스를 상속합니다. 마지막으로, 테넌트 ID는 데이터가 속한 테넌트를 구별하는 데 사용됩니다.

SQL 테넌트 데이터 필터링

평소와 마찬가지로 테이블이 생성된 후에는 해당 모듈의 CURD를 따라야 합니다. 그러나 SaaS 애플리케이션의 가장 기본적인 요구 사항은 테넌트 데이터 격리입니다. 즉, 회사 B의 사람들은 회사 A의 데이터를 볼 수 없습니다. 이를 필터링하는 방법 위에서 설정한 BaseSaasEntity는 현재 요청이 오는 회사를 구별하여 여기에서 작동합니다. from, 모든 테넌트 비즈니스 SQL에 테넌트=?를 추가하여 테넌트 데이터 필터링을 구현합니다.

Hibernate 필터

우리 사업에 테넌트 SQL 필터링 코드를 추가하면 작업량이 엄청날 뿐만 아니라 오류가 발생할 확률도 높아집니다. 이상적인 방법은 필터링된 SQL 접합을 함께 처리하고 테넌트 비즈니스 인터페이스에서 SQL 필터링을 활성화하는 것입니다. JPA는 최대 절전 모드로 구현되므로 여기서는 최대 절전 모드

@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은 "가시성" 규칙, 즉 Hibernate 필터를 사용하여 데이터를 처리하는 혁신적인 방법을 제공합니다. Hibernate 필터는 매개변수를 취할 수 있는 전역적으로 유효한 명명된 필터입니다. 특정 Hibernate 세션에 대해 필터를 활성화(또는 비활성화)할지 여부를 선택할 수 있습니다.

여기에서는 @FilterDef 및 @Filter를 통해 SQL 필터 조건을 미리 정의합니다. 그런 다음 @TenantFilter 주석을 사용하여 인터페이스에 데이터 필터링이 필요하다는 것을 식별합니다

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional
public @interface TenantFilter {

    boolean readOnly() default true;

}

이 인터페이스가 Controller 레이어에 해당하는 메서드에 배치되어 있음을 볼 수 있습니다. 트랜잭션 주석 @Transactional을 추가하는 것의 중요성은 최대 절전 모드 필터를 활성화하려면 트랜잭션을 활성화해야 한다는 것입니다. 여기서 기본값은 읽기 전용 트랜잭션입니다. 마지막으로 필터를 활성화하기 위한 측면을 정의합니다

@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를 가져오고 필터를 켜면 테넌트 데이터가 격리됩니다. 테넌트 비즈니스 인터페이스에 @TenantFilter 주석만 추가하면 됩니다. @TenantFilter 주석만 추가하면 개발은 비즈니스 코드에만 신경 쓰면 됩니다. 위 그림의 TenantContext는 현재 스레드 테넌트 컨텍스트입니다. 프런트 엔드에 동의하면 서버는 인터셉터를 사용하여 획득한 테넌트 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();
    }
}

Sub-library에 캐시합니다.

테넌트가 많아질수록 단일 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은 사용할 데이터 소스를 결정하기 위해determinCurrentLookupKey() 메서드를 호출합니다. 여기의 DataSourceContext는 위의 TenantContext와 유사합니다. 인터셉터에서 테넌트정보를 얻은 후 현재 테넌트에 해당하는 데이터 소스 키를 찾아 ThreadLocal에 설정합니다.

위 내용은 Springboot 다중 테넌트 SaaS를 구축하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제