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


<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.0modelVersion>

    <groupId>com.testgroupId>
    <artifactId>sharding-jdbcartifactId>
    <version>1.0-SNAPSHOTversion>

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

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-aopartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.39version>
        dependency>
        <dependency>
            <groupId>com.zaxxergroupId>
            <artifactId>HikariCPartifactId>
            <version>3.4.5version>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>1.3.2version>
        dependency>
        
        <dependency>
            <groupId>com.github.pagehelpergroupId>
            <artifactId>pagehelper-spring-boot-starterartifactId>
            <version>1.2.5version>
        dependency>
         
        <dependency>
            <groupId>tk.mybatisgroupId>
            <artifactId>mapper-spring-boot-starterartifactId>
            <version>2.1.5version>
        dependency>

        <dependency>
            <artifactId>guavaartifactId>
            <groupId>com.google.guavagroupId>
            <version>20.0version>
        dependency>

        
        <dependency>
            <groupId>org.apache.shardingspheregroupId>
            <artifactId>sharding-jdbc-orchestration-spring-boot-starterartifactId>
            <version>4.1.1version>
            <exclusions>
                <exclusion>
                    <artifactId>guavaartifactId>
                    <groupId>com.google.guavagroupId>
                exclusion>
            exclusions>
        dependency>
        
        <dependency>
            <groupId>org.apache.shardingspheregroupId>
            <artifactId>sharding-orchestration-center-zookeeper-curatorartifactId>
            <version>4.1.1version>
            <exclusions>
                <exclusion>
                    <artifactId>guavaartifactId>
                    <groupId>com.google.guavagroupId>
                exclusion>
            exclusions>
        dependency>

        
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger-uiartifactId>
            <version>2.9.2version>
            <exclusions>
                <exclusion>
                    <artifactId>guavaartifactId>
                    <groupId>com.google.guavagroupId>
                exclusion>
            exclusions>
        dependency>
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger2artifactId>
            <version>2.9.2version>
            <exclusions>
                <exclusion>
                    <artifactId>guavaartifactId>
                    <groupId>com.google.guavagroupId>
                exclusion>
            exclusions>
        dependency>

        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.16.16version>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            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配置

节点目录
ShardingSphere实现多租户数据隔离_第1张图片

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.
     *
     * 

sharding value injected by hint, not in SQL.

* * @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
    ShardingSphere实现多租户数据隔离_第2张图片

优化

不需要配置logic table(可以随便瞎写一个logic table)
修改框架的源码:ShardingUnicastRoutingEngine.java
ShardingSphere实现多租户数据隔离_第3张图片

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

你可能感兴趣的:(Java,spring,boot,mybatis,java)