赞
踩
多租户技术或称多重租赁技术,简称SaaS,是一种软件架构技术,是实现如何在多用户环境下(多用户一般是面向企业用户)共用相同的系统或程序组件,并且可确保各用户间数据的隔离性。
多租户在数据存储上主要存在三种方案,独立数据库、共享数据库,独立Schema、共享数据库,共享 Schema,共享数据表。
即一个租户一个数据库,这种方案的用户数据隔离级别最高,安全性最好,但成本较高。
共同使用一个数据库,使用表进行数据隔离,多个或所有租户共享Database,但是每个租户一个Schema(也可叫做一个user)。底层库比如是DB2、ORACLE等,一个数据库下可以有多个SCHEMA。
免费租户或者体验租户,考虑到维护和购置成本,使用共享数据库,共享 Schema,共享数据表方案,通过tenant_id区分不同租户数据,实现数据隔离。
收费租户为了获得更好的用户体验,结合MySql数据库,采用独立数据库方案,不同租户对应不同数据库,数据隔离级别最高,安全性最好。
由免费租户(体验租户)升级到付费租户,不仅要为该租户创建独立数据库,还要同步历史数据,在不影响用户体验的情况,升级操作可以考虑异步定时处理。
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
通过PaginationInterceptor拦截特定sql,加上tenant_id。具体代码如下:
- @Bean
- public PaginationInterceptor paginationInterceptor() {
- PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
- // SQL解析处理拦截:增加租户处理回调。
- List<ISqlParser> sqlParserList = new ArrayList<>();
- TenantSqlParser tenantSqlParser = new TenantSqlParser();
- tenantSqlParser.setTenantHandler(new TenantHandler() {
- @Override
- public Expression getTenantId(boolean where) {
- HttpServletRequest request = null;
- // 从当前系统上下文中取出当前请求的服务商ID,通过解析器注入到SQL中。
- if (null != RequestContextHolder.getRequestAttributes()) {
- request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
- }
- Long currentTenantId;
- try {
- String tenantId;
- if (null == request) {
- tenantId = TenantIdContext.peek();
- } else {
- tenantId = request.getHeader("tenantId");
- if (StringUtils.isBlank(tenantId)) {
- HttpSession session = request.getSession();
- tenantId = session.getAttribute(tenantId) == null ? null : session.getAttribute(tenantId).toString();
- }
- if (StringUtils.isBlank(tenantId)) {
- tenantId = TenantIdContext.peek();
- }
- }
- currentTenantId = Long.valueOf(tenantId);
- } catch (Exception e) {
- throw new RuntimeException("getTenantId error.");
- }
- return new LongValue(currentTenantId);
- }
- @Override
- public String getTenantIdColumn() {
- return SYSTEM_TENANT_ID;
- }
- @Override
- public boolean doTableFilter(String tableName) {
- // 这里可以判断是否过滤表
- return false;
- }
- });
- sqlParserList.add(tenantSqlParser);
- paginationInterceptor.setSqlParserList(sqlParserList);
- paginationInterceptor.setSqlParserFilter(new ISqlParserFilter() {
- @Override
- public boolean doFilter(MetaObject metaObject) {
- /**
- * 如果在程序中,有部分SQL不需要加上租户ID的表示,需要过滤特定的sql
- */
- MappedStatement ms = SqlParserHelper.getMappedStatement(metaObject);
- String msId = ms.getId();
- // 不是meeting下面的sql都不需要加租户ID
- if (msId.startsWith("com.hixiaoe.meeting.persistence.meeting.dao")) {
- return false;
- }
- return true;
- }
- });
- return paginationInterceptor;
- }
DynamicRoutingDataSource继承了AbstractRoutingDataSource抽象类,实现了InitializingBean, DisposableBean接口,内部维护了一个LinkedHashMap,用于存放当前所有数据源。通过DynamicDataSourceCreator创建未初始化的数据源,并添加数据源到当前数据源map中,以供动态切换使用,具体实现如下:
- public static void addDataSource(String dsName, String username, String password, String driverName, String url) {
- DataSourceProperty dataSourceProperty = new DataSourceProperty();
- dataSourceProperty.setPollName(dsName);
- dataSourceProperty.setDriverClassName(driverName);
- dataSourceProperty.setUrl(url);
- dataSourceProperty.setUsername(username);
- dataSourceProperty.setPassword(password);
- DruidConfig druidConfig = new DruidConfig();
- druidConfig.setInitialSize(1);
- dataSourceProperty.setDruid(druidConfig);
- Map<String, DataSource> currentDataSources = dsUtil.dynamicRoutingDataSource.getCurrentDataSources();
- if (!currentDataSources.containsKey(dsName)) {
- DataSource ds = dsUtil.dynamicDataSourceCreator.createDruidDataSource(dataSourceProperty);
- dsUtil.dynamicRoutingDataSource.addDataSource(dsName, ds);
- }
- }
使用 @DS ("dsName")切换数据源,@DS 可以注解在方法上和类上,同时存在,方法注解优先于类上注解。强烈建议只注解在service实现上,没有@DS,则采用默认数据源。dsName可以为组名也可以为具体某个库的名称(可以从session,header或参数中获取数据源)。
http请求header中,或者httpsession设置tenantId,通过sql解析器在特定sql中处理对应租户数据。
通过ThreadLocal设置当前线程tenantId,以供sql解析器在特定sql中处理对应租户数据。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。