ShardingSphere实现多租户数据隔离

  1. 本方案可以实现多schema、多数据库隔离
  2. 在Zookeeper配置中心修改数据库等配置,可以实时同步到系统中
  3. 真实项目需要给Zookeeper节点配置权限

版本

  1. springboot 2.2.1.RELEASE
  2. Zookeeper 3.7.1
  3. ShardingSphere 4.1.1
    官方文档

pom.xml

注意:不要使用集成了springboot的数据源,比如:druid-spring-boot-starter

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.test</groupId>
    <artifactId>sharding-jdbc</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/>
    </parent>

    <dependencies>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!--数据库相关依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.39</version>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>3.4.5</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--mybatis分页插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
         <!--tk-mapper-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.5</version>
        </dependency>

        <dependency>
            <artifactId>guava</artifactId>
            <groupId>com.google.guava</groupId>
            <version>20.0</version>
        </dependency>

        <!-- sharding-jdbc编排治理 -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-orchestration-spring-boot-starter</artifactId>
            <version>4.1.1</version>
            <exclusions>
                <exclusion>
                    <artifactId>guava</artifactId>
                    <groupId>com.google.guava</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- sharding-jdbc编排治理,使用zookeeper作为配置中心 -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-orchestration-center-zookeeper-curator</artifactId>
            <version>4.1.1</version>
            <exclusions>
                <exclusion>
                    <artifactId>guava</artifactId>
                    <groupId>com.google.guava</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
            <exclusions>
                <exclusion>
                    <artifactId>guava</artifactId>
                    <groupId>com.google.guava</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
            <exclusions>
                <exclusion>
                    <artifactId>guava</artifactId>
                    <groupId>com.google.guava</groupId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.16</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

application.yml

server:
  port: 18000

spring:
  shardingsphere:
    orchestration:
      orchestration_ds:
        orchestrationType: registry_center,config_center
        instanceType: zookeeper
        serverLists: localhost:2181
        namespace: safety_v1
        props:
          overwrite: true
          # digest: root:root 

mybatis:
  type-aliases-package: com.test.domain
  mapper-locations: classpath:mapper/**/*.xml
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# 分页配置
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql
  pageSizeZero: true #pageSize=0 or RowBounds.Limit = 0的时候就不适用分页,但是返回对象还是PageInfo

Zookeeper配置

节点目录
在这里插入图片描述

datasource节点数据

# 租户1的数据库
ds_1: !!org.apache.shardingsphere.orchestration.core.configuration.YamlDataSourceConfiguration
  dataSourceClassName: com.zaxxer.hikari.HikariDataSource
  properties:
    jdbcUrl: jdbc:mysql://localhost:3306/test_v1?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    maxPoolSize: 10
    maintenanceIntervalMilliseconds: 30000
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    minPoolSize: 1
    maxLifetimeMilliseconds: 1800000


# 租户2的数据库
ds_2: !!org.apache.shardingsphere.orchestration.core.configuration.YamlDataSourceConfiguration
  dataSourceClassName: com.zaxxer.hikari.HikariDataSource
  properties:
    jdbcUrl: jdbc:mysql://localhost:3306/test_v2?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root
    maxPoolSize: 10
    maintenanceIntervalMilliseconds: 30000
    connectionTimeoutMilliseconds: 30000
    idleTimeoutMilliseconds: 60000
    minPoolSize: 1
    maxLifetimeMilliseconds: 1800000

rule节点数据

CustomHintShardingAlgorithm类,需要自己实现HintShardingAlgorithm接口

下面配置中只配置了sys_user表,系统有多少张表就配置多少个logic table
已优化,不需要配置多个 logic table,随便瞎写一个logic table就行。详情见最下面【优化说明】

tables:
  sys_user:
    actualDataNodes: ds_$->{1..2}.sys_user
defaultDatabaseStrategy:
  hint:
    algorithmClassName: com.test.config.sharding.CustomHintShardingAlgorithm
defaultTableStrategy:
  none:
  

自定义分库策略

import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.hint.HintShardingValue;
import java.util.ArrayList;
import java.util.Collection;

public class CustomHintShardingAlgorithm implements HintShardingAlgorithm<Long> {

    /**
     * Sharding.
     *
     * <p>sharding value injected by hint, not in SQL.</p>
     *
     * @param availableTargetNames available data sources or tables's names
     * @param shardingValue sharding value
     * @return sharding result for data sources or tables's names
     */
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<Long> shardingValue) {
        Collection<String> result = new ArrayList<>();
        for (String targetName : availableTargetNames) {
            for (Long value : shardingValue.getValues()) {
                if (targetName.endsWith("_" + value)) {
                    result.add(targetName);
                }
            }
        }
        return result;
    }
}

HintManager指定访问的数据库

自定义拦截器,在拦截器内部指定该用户访问哪个数据库

HintManager.clear();
HintManager.getInstance().setDatabaseShardingValue(2L);

Zookeeper客户端工具

zooinspector

  1. 下载zookeeper3.7.1源码,按照根目录下README_packaging.md文件说明,打包编译
  2. 进入源码根目录下 zookeeper-contrib/zookeeper-contrib-zooinspector目录,运行 mvn install 命令
  3. 将zookeeper-contrib-zooinspector整个文件夹拷贝出来,运行启动脚本zooInspector.cmd
    在这里插入图片描述

优化

不需要配置logic table(可以随便瞎写一个logic table)
修改框架的源码:ShardingUnicastRoutingEngine.java
在这里插入图片描述

package org.apache.shardingsphere.sharding.route.engine.type.unicast;

import cn.test.exception.BaseException;
import com.google.common.collect.Sets;
import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.api.hint.HintManager;
import org.apache.shardingsphere.underlying.common.rule.DataNode;
import org.apache.shardingsphere.core.rule.ShardingRule;
import org.apache.shardingsphere.core.rule.TableRule;
import org.apache.shardingsphere.sharding.route.engine.type.ShardingRouteEngine;
import org.apache.shardingsphere.underlying.common.config.exception.ShardingSphereConfigurationException;
import org.apache.shardingsphere.underlying.route.context.RouteResult;
import org.apache.shardingsphere.underlying.route.context.RouteUnit;
import org.apache.shardingsphere.underlying.route.context.RouteMapper;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Sharding unicast routing engine.
 */
@RequiredArgsConstructor
public final class ShardingUnicastRoutingEngine implements ShardingRouteEngine {

    private final Collection<String> logicTables;

    @Override
    public RouteResult route(final ShardingRule shardingRule) {
        RouteResult result = new RouteResult();
        Collection<Comparable<?>> databaseShardingValues = HintManager.getDatabaseShardingValues();
        if (databaseShardingValues.size()!=1){
            throw new BaseException("未指定租户");
        }
        
        // 修改部分:指定数据源 start
        logicTables.clear();
        String dataSourceName = "ds_"+databaseShardingValues.iterator().next();
        // 修改部分:指定数据源 end
        
        //String dataSourceName = shardingRule.getShardingDataSourceNames().getRandomDataSourceName();
        RouteMapper dataSourceMapper = new RouteMapper(dataSourceName, dataSourceName);
        if (shardingRule.isAllBroadcastTables(logicTables)) {
            List<RouteMapper> tableMappers = new ArrayList<>(logicTables.size());
            for (String each : logicTables) {
                tableMappers.add(new RouteMapper(each, each));
            }
            result.getRouteUnits().add(new RouteUnit(dataSourceMapper, tableMappers));
        } else if (logicTables.isEmpty()) {
            result.getRouteUnits().add(new RouteUnit(dataSourceMapper, Collections.emptyList()));
        } else if (1 == logicTables.size()) {
            String logicTableName = logicTables.iterator().next();
            if (!shardingRule.findTableRule(logicTableName).isPresent()) {
                result.getRouteUnits().add(new RouteUnit(dataSourceMapper, Collections.emptyList()));
                return result;
            }
            DataNode dataNode = shardingRule.getDataNode(logicTableName);
            result.getRouteUnits().add(new RouteUnit(dataSourceMapper, Collections.singletonList(new RouteMapper(logicTableName, dataNode.getTableName()))));
        } else {
            List<RouteMapper> tableMappers = new ArrayList<>(logicTables.size());
            Set<String> availableDatasourceNames = null;
            boolean first = true;
            for (String each : logicTables) {
                TableRule tableRule = shardingRule.getTableRule(each);
                DataNode dataNode = tableRule.getActualDataNodes().get(0);
                tableMappers.add(new RouteMapper(each, dataNode.getTableName()));
                Set<String> currentDataSourceNames = new HashSet<>(tableRule.getActualDatasourceNames().size());
                for (DataNode eachDataNode : tableRule.getActualDataNodes()) {
                    currentDataSourceNames.add(eachDataNode.getDataSourceName());
                }
                if (first) {
                    availableDatasourceNames = currentDataSourceNames;
                    first = false;
                } else {
                    availableDatasourceNames = Sets.intersection(availableDatasourceNames, currentDataSourceNames);
                }
            }
            if (availableDatasourceNames.isEmpty()) {
                throw new ShardingSphereConfigurationException("Cannot find actual datasource intersection for logic tables: %s", logicTables);
            }
            dataSourceName = shardingRule.getShardingDataSourceNames().getRandomDataSourceName(availableDatasourceNames);
            result.getRouteUnits().add(new RouteUnit(new RouteMapper(dataSourceName, dataSourceName), tableMappers));
        }
        return result;
    }
}

bug

sql中含有通配符*,会导致mybatis返回值字段未null(这个应该是ShardingSphere的bug)
重写 ShardingResultSetMetaData.java

package org.apache.shardingsphere.shardingjdbc.jdbc.core.resultset;

import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.core.rule.ShardingRule;
import org.apache.shardingsphere.shardingjdbc.jdbc.adapter.WrapperAdapter;
import org.apache.shardingsphere.shardingjdbc.jdbc.core.constant.SQLExceptionConstant;
import org.apache.shardingsphere.sql.parser.binder.segment.select.projection.Projection;
import org.apache.shardingsphere.sql.parser.binder.segment.select.projection.impl.ColumnProjection;
import org.apache.shardingsphere.sql.parser.binder.statement.SQLStatementContext;
import org.apache.shardingsphere.sql.parser.binder.statement.dml.SelectStatementContext;
import org.apache.shardingsphere.underlying.common.database.DefaultSchema;

import java.lang.reflect.Field;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.List;

/**
 * Sharding result set meta data.
 */
@RequiredArgsConstructor
public final class ShardingResultSetMetaData extends WrapperAdapter implements ResultSetMetaData {

    private final ResultSetMetaData resultSetMetaData;

    private final ShardingRule shardingRule;

    private final SQLStatementContext sqlStatementContext;

	// 修改这个方法
    @Override
    public int getColumnCount() {
        if (sqlStatementContext instanceof SelectStatementContext) {
            int size = ((SelectStatementContext) sqlStatementContext).getProjectionsContext().getExpandProjections().size();
           if (size == 0) {
               try {
                   return resultSetMetaData.getColumnCount();
               } catch (SQLException e) {
                   e.printStackTrace();
               }
           }else  {
               try {
                   Field field = resultSetMetaData.getClass().getDeclaredField("fields");
                   field.setAccessible(true);
                   com.mysql.jdbc.Field[] fields = (com.mysql.jdbc.Field[]) field.get(resultSetMetaData);
                   return fields.length;
               } catch (Exception e) {
                   e.printStackTrace();
               }
               return size;
           }
        }

        return 0;
    }

    @Override
    public boolean isAutoIncrement(final int column) throws SQLException {
        return resultSetMetaData.isAutoIncrement(column);
    }

    @Override
    public boolean isCaseSensitive(final int column) throws SQLException {
        return resultSetMetaData.isCaseSensitive(column);
    }

    @Override
    public boolean isSearchable(final int column) throws SQLException {
        return resultSetMetaData.isSearchable(column);
    }

    @Override
    public boolean isCurrency(final int column) throws SQLException {
        return resultSetMetaData.isCurrency(column);
    }

    @Override
    public int isNullable(final int column) throws SQLException {
        return resultSetMetaData.isNullable(column);
    }

    @Override
    public boolean isSigned(final int column) throws SQLException {
        return resultSetMetaData.isSigned(column);
    }

    @Override
    public int getColumnDisplaySize(final int column) throws SQLException {
        return resultSetMetaData.getColumnDisplaySize(column);
    }

    @Override
    public String getColumnLabel(final int column) throws SQLException {
        return resultSetMetaData.getColumnLabel(column);
    }

    @Override
    public String getColumnName(final int column) throws SQLException {
        if (sqlStatementContext instanceof SelectStatementContext) {
            List<Projection> actualProjections = ((SelectStatementContext) sqlStatementContext).getProjectionsContext().getExpandProjections();
            if (column > actualProjections.size()) {
                throw new SQLException(SQLExceptionConstant.COLUMN_INDEX_OUT_OF_RANGE, SQLExceptionConstant.OUT_OF_INDEX_SQL_STATE, 0);
            }
            Projection projection = ((SelectStatementContext) sqlStatementContext).getProjectionsContext().getExpandProjections().get(column - 1);
            if (projection instanceof ColumnProjection) {
                return ((ColumnProjection) projection).getName();
            }
        }
        return resultSetMetaData.getColumnName(column);
    }

    @Override
    public String getSchemaName(final int column) {
        return DefaultSchema.LOGIC_NAME;
    }

    @Override
    public int getPrecision(final int column) throws SQLException {
        return resultSetMetaData.getPrecision(column);
    }

    @Override
    public int getScale(final int column) throws SQLException {
        return resultSetMetaData.getScale(column);
    }

    @Override
    public String getTableName(final int column) throws SQLException {
        String actualTableName = resultSetMetaData.getTableName(column);
        return shardingRule.getLogicTableNames(actualTableName).isEmpty() ? actualTableName : shardingRule.getLogicTableNames(actualTableName).iterator().next();
    }

    @Override
    public String getCatalogName(final int column) {
        return DefaultSchema.LOGIC_NAME;
    }

    @Override
    public int getColumnType(final int column) throws SQLException {
        return resultSetMetaData.getColumnType(column);
    }

    @Override
    public String getColumnTypeName(final int column) throws SQLException {
        return resultSetMetaData.getColumnTypeName(column);
    }

    @Override
    public boolean isReadOnly(final int column) throws SQLException {
        return resultSetMetaData.isReadOnly(column);
    }

    @Override
    public boolean isWritable(final int column) throws SQLException {
        return resultSetMetaData.isWritable(column);
    }

    @Override
    public boolean isDefinitelyWritable(final int column) throws SQLException {
        return resultSetMetaData.isDefinitelyWritable(column);
    }

    @Override
    public String getColumnClassName(final int column) throws SQLException {
        return resultSetMetaData.getColumnClassName(column);
    }
}

zookeeper配置权限

public static void main(String[] args) throws NoSuchAlgorithmException {
    String digest = DigestAuthenticationProvider.generateDigest("admin:123456");
    System.out.println(digest);
}
    
超级管理员账号密码:
admin:123456

启动脚本中添加:
"-Dzookeeper.DigestAuthenticationProvider.superDigest=admin:0uek/hZ/V9fgiM35b0Z2226acMQ=" 

登录:
addauth digest admin:123456

刷新权限:
setAcl -R / digest:admin:0uek/hZ/V9fgiM35b0Z2226acMQ=:cdrwa
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现MySQL多租户数据隔离的一种方法是在数据库中为每个租户创建单独的schema(即数据库),并将其与单独的用户关联起来。下面是使用Java代码实现该方法的步骤: 1. 创建一个Java类来管理数据库连接和操作。这个类应该包含以下方法: ``` public class TenantDatabaseManager { private static final String DATABASE_HOST = "localhost"; private static final String DATABASE_USERNAME = "root"; private static final String DATABASE_PASSWORD = "password"; private static final String DATABASE_DRIVER = "com.mysql.jdbc.Driver"; private Connection connection; public TenantDatabaseManager(String tenantId) throws SQLException, ClassNotFoundException { String databaseName = "tenant_" + tenantId; Class.forName(DATABASE_DRIVER); this.connection = DriverManager.getConnection("jdbc:mysql://" + DATABASE_HOST + "/" + databaseName, DATABASE_USERNAME, DATABASE_PASSWORD); } public void executeQuery(String query) throws SQLException { Statement statement = connection.createStatement(); statement.executeQuery(query); statement.close(); } public void executeUpdate(String query) throws SQLException { Statement statement = connection.createStatement(); statement.executeUpdate(query); statement.close(); } public void closeConnection() throws SQLException { this.connection.close(); } } ``` 2. 创建一个Java类来管理租户列表。这个类应该包含以下方法: ``` public class TenantManager { private Map<String, TenantDatabaseManager> tenantDatabaseManagerMap = new HashMap<>(); public void addTenant(String tenantId) throws SQLException, ClassNotFoundException { if (!tenantDatabaseManagerMap.containsKey(tenantId)) { TenantDatabaseManager tenantDatabaseManager = new TenantDatabaseManager(tenantId); tenantDatabaseManagerMap.put(tenantId, tenantDatabaseManager); } } public TenantDatabaseManager getTenantDatabaseManager(String tenantId) { return tenantDatabaseManagerMap.get(tenantId); } public void closeAllConnections() throws SQLException { for (TenantDatabaseManager tenantDatabaseManager : tenantDatabaseManagerMap.values()) { tenantDatabaseManager.closeConnection(); } tenantDatabaseManagerMap.clear(); } } ``` 3. 在每次请求到达时,从请求中获取租户ID,并使用TenantManager获取对应的TenantDatabaseManager对象。然后使用该对象执行数据库操作。 ``` public class RequestHandler { private static final String TENANT_HEADER = "Tenant-Id"; private TenantManager tenantManager; public RequestHandler() { tenantManager = new TenantManager(); } public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException, SQLException, ClassNotFoundException { String tenantId = request.getHeader(TENANT_HEADER); if (tenantId == null) { response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.getWriter().println("Tenant Id is required."); return; } TenantDatabaseManager tenantDatabaseManager = tenantManager.getTenantDatabaseManager(tenantId); if (tenantDatabaseManager == null) { tenantManager.addTenant(tenantId); tenantDatabaseManager = tenantManager.getTenantDatabaseManager(tenantId); } // execute database operations using tenantDatabaseManager ... } public void destroy() throws SQLException { tenantManager.closeAllConnections(); } } ``` 以上就是使用Java代码实现MySQL多租户数据隔离的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值