精华内容
下载资源
问答
  • mongodb多租户实现分库数据隔离

    千次阅读 2019-04-18 23:14:37
    mongodb的多租户实现其实跟hibernate的多租户实现原理差不多,都是获得数据库连接之后,再切换数据库。 具体代码如下: mongodb自定义配置类: import com.mongodb.MongoClient; import ...

    mongodb的多租户实现其实跟hibernate的多租户实现原理差不多,都是获得数据库连接之后,再切换数据库。

    具体代码如下:

    mongodb自定义配置类:

    import com.mongodb.MongoClient;
    import com.mongodb.MongoClientOptions;
    import org.springframework.beans.factory.ObjectProvider;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    import org.springframework.boot.autoconfigure.mongo.MongoProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.env.Environment;
    import org.springframework.data.mongodb.core.MongoTemplate;
    
    import javax.annotation.PreDestroy;
    import java.net.UnknownHostException;
    
    @Configuration
    @EnableAutoConfiguration
    public class MongodbConfig {
    
        @Value("${spring.data.mongodb.default_db}")
        String defaultName;
        private final MongoClientOptions options;
        private MongoClient mongo;
    
        public MongodbConfig(MongoProperties properties, ObjectProvider<MongoClientOptions> options, Environment environment) throws UnknownHostException {
            this.options = (MongoClientOptions)options.getIfAvailable();
            this.mongo = properties.createMongoClient(this.options,environment);
        }
    
        @PreDestroy
        public void close() {
            if (this.mongo != null) {
                this.mongo.close();
            }
        }
    
    
        @Bean
        public MySimpleMongoDbFactory getMongodb(){
            MySimpleMongoDbFactory mySimpleMongoDbFactory = new MySimpleMongoDbFactory(mongo,defaultName);
            return mySimpleMongoDbFactory;
        }
    
        @Bean
        public MongoTemplate mongoTemplate() throws Exception {
            return new MongoTemplate(getMongodb());
        }
    
    }

    继承实现mongodb的SimpleMongoDbFactory类 

    import com.mongodb.DB;
    import com.mongodb.MongoClient;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.data.mongodb.core.MongoTemplate;
    import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
    
    public class MySimpleMongoDbFactory extends SimpleMongoDbFactory {
        private static final Logger logger = LoggerFactory.getLogger(MySimpleMongoDbFactory.class);
    
        /**
         * 默认数据库名称
         **/
        private  String defaultName = "";
        /**
         * MongoDB模板类
         **/
        private MongoTemplate mongoTemplate;
        /**
         * 用户所在线程使用数据库集合
         **/
        private static final ThreadLocal<String> dbName = new ThreadLocal<String>();
    
        public MySimpleMongoDbFactory(MongoClient mongoClient, String databaseName) {
            super(mongoClient, databaseName);
            logger.debug("Instantiating " + MySimpleMongoDbFactory.class.getName() + " with default database name: " + databaseName);
            this.defaultName = databaseName;
        }
    
        public static void switchDatabase(final String databaseName) {
            logger.debug("Switching to database: " + databaseName);
            dbName.set(databaseName);
        }
    
        @Override
        public DB getDb() {
            final String databaseName = dbName.get();
            final String dbToUse = (databaseName != null ? databaseName : this.defaultName);
            logger.debug("Acquiring database: " + dbToUse);
            return super.getDb(dbToUse);
        }
    }
    

     使用示例:

    import com.alibaba.fastjson.JSON;
    import com.medicalrobot.config.MySimpleMongoDbFactory;
    import com.medicalrobot.dao.UserRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.annotation.DirtiesContext;
    import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
    import org.testng.annotations.Test;
    
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
    @DirtiesContext
    public class InterfaceTest extends AbstractTestNGSpringContextTests {
    
    
        @Autowired
        UserRepository userRepository;
    
        @Test
        public void test(){
            MySimpleMongoDbFactory.switchDatabase("test");
           /* User user = new User();
            user.setId("333");
            user.setName("zhangsan");
            user.setSex("男");
            userRepository.save(user);*/
            System.out.println(JSON.toJSONString(userRepository.findAll()));
            System.out.println("==============================");
            MySimpleMongoDbFactory.switchDatabase("test2");
            System.out.println(JSON.toJSONString(userRepository.findAll()));
        }
    }

    user类:

    import lombok.Data;
    import org.springframework.data.annotation.Id;
    import org.springframework.data.mongodb.core.mapping.Document;
    
    @Document
    @Data
    public class User {
        @Id
        private String id;
        private String name;
        private String sex;
    }

     

    展开全文
  • 前几天有人想做一个多租户的平台,每个租户一个库,可以...实际上这个需求就是分库分表的实现,通过设置数据库/表映射关系,根据传入的定位数据进行匹配,找到正确的库表配置,生成数据访问对象 以core控制台程序为例 clas
  • 租户共享MyCat中的schema,schema中的表会跨越个datanode,因此每个表应该指定primary key, sharding rule可以解析primary key中包含的租户code,从而进一步确定每个租户对应的datanode.这就要求每个表的主键生成...

    方案一:
    租户共享MyCat中的schema,schema中的表会跨越多个datanode,因此每个表应该指定primary key, sharding rule可以解析primary key中包含的租户code,从而进一步确定每个租户对应的datanode.这就要求每个表的主键生成必须要主键生成器来生成(key generator),主键生成器要满足以下要求:
    主键生成效率高
    生成的主键全局无冲突
    生成的主键要包含租户code信息,并可被反向解析出来

    方案二:
    每个租户独占MyCat中的一个schema,schema的表不会跨datanode,类似的拓扑如下:

    MyCat核心配置:
    server.xml
    <user name="root">
    <property name="password">password</property>
    <property name="schemas">GLOBALDB,JG1DB,JG2DB,JG3DB,JG4DB,JG5DB</property>
    </user>

    2. schema.xml

    <?xml version="1.0"?>
    <!DOCTYPE mycat:schema SYSTEM "schema.dtd">
    <mycat:schema xmlns:mycat="http://io.mycat/">
    <schema name="GLOBALDB" checkSQLschema="false" sqlMaxLimit="100">
    <!-- global table is auto cloned to all defined data nodes ,so can join
    with any table whose sharding node is in the same data node -->
    <table name="orgmapping" primaryKey="id" type="global" dataNode="gdn" />
    </schema>
    <schema name="JG1DB" checkSQLschema="false" sqlMaxLimit="100">
    <table name="user" primaryKey="id" autoIncrement="true" dataNode="jg1dn" />
    <table name="user_order" primaryKey="id" autoIncrement="true" dataNode="jg1dn" />
    </schema>
    <schema name="JG2DB" checkSQLschema="false" sqlMaxLimit="100">
    <table name="user" primaryKey="id" autoIncrement="true" dataNode="jg2dn" />
    <table name="user_order" primaryKey="id" autoIncrement="true" dataNode="jg2dn" />
    </schema>
    <schema name="JG3DB" checkSQLschema="false" sqlMaxLimit="100">
    <table name="user" primaryKey="id" autoIncrement="true" dataNode="jg3dn" />
    <table name="user_order" primaryKey="id" autoIncrement="true" dataNode="jg3dn" />
    </schema>
    <schema name="JG4DB" checkSQLschema="false" sqlMaxLimit="100">
    <table name="user" primaryKey="id" autoIncrement="true" dataNode="jg4dn" />
    <table name="user_order" primaryKey="id" autoIncrement="true" dataNode="jg4dn" />
    </schema>
    <schema name="JG5DB" checkSQLschema="false" sqlMaxLimit="100">
    <table name="user" primaryKey="id" autoIncrement="true" dataNode="jg5dn" />
    <table name="user_order" primaryKey="id" autoIncrement="true" dataNode="jg5dn" />
    </schema>
    <dataNode name="gdn" dataHost="globalhost" database="wymglobal" />
    <dataNode name="jg1dn" dataHost="g1host" database="jg1" />
    <dataNode name="jg2dn" dataHost="g1host" database="jg2" />
    <dataNode name="jg3dn" dataHost="g2host" database="jg3" />
    <dataNode name="jg4dn" dataHost="g2host" database="jg4" />
    <dataNode name="jg5dn" dataHost="g2host" database="jg5" />
    <dataHost name="globalhost" maxCon="1000" minCon="10" balance="0"
    writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
    <heartbeat>select user()</heartbeat>
    <!-- can have multi write hosts -->
    <writeHost host="hostM1" url="192.168.0.199:3306" user="root"
    password="password">
    </writeHost>
    </dataHost>
    <dataHost name="g1host" maxCon="1000" minCon="10" balance="0"
    writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
    <heartbeat>select user()</heartbeat>
    <!-- can have multi write hosts -->
    <writeHost host="hostM1" url="192.168.1.13:3306" user="root"
    password="password">
    </writeHost>
    </dataHost>
    <dataHost name="g2host" maxCon="1000" minCon="10" balance="0"
    writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
    <heartbeat>select user()</heartbeat>
    <!-- can have multi write hosts -->
    <writeHost host="hostM1" url="192.168.1.142:3306" user="root"
    password="password">
    </writeHost>
    </dataHost>
    </mycat:schema>


    验证方案:
    利用Spring boot jdbc 写测试程序:

    在src/main/resources/application.yml中定义数据源
    logging:
    level:
    org.springframework: INFO
    com.wym: DEBUG
    ################### DataSource Configuration ##########################
    spring:
    application:
    name: gs-relational-data-access
    datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:8066
    username: root
    password: password
    initialize: false
    init-db: false
    2. 在DatasourceConfig.java中定义Datasource Bean和JdbcTemplate Bean

    package com.wym.mycatdemo;

    import javax.sql.DataSource;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.env.Environment;
    import org.springframework.jdbc.core.JdbcTemplate;

    import com.alibaba.druid.pool.DruidDataSource;

    @Configuration
    public class DatasourceConfig {

    @Autowired
    private Environment env;

    @Bean(name = "dataSource")
    public DataSource dataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUrl(env.getProperty("spring.datasource.url"));
    dataSource.setUsername(env.getProperty("spring.datasource.username"));// 用户名
    dataSource.setPassword(env.getProperty("spring.datasource.password"));// 密码
    dataSource.setInitialSize(2);
    dataSource.setMaxActive(20);
    dataSource.setMinIdle(0);
    dataSource.setMaxWait(60000);
    dataSource.setValidationQuery("SELECT 1");
    dataSource.setTestOnBorrow(false);
    dataSource.setTestWhileIdle(true);
    dataSource.setPoolPreparedStatements(false);
    return dataSource;
    }

    @Bean(name = "businessJdbcTemplate")
    public JdbcTemplate primaryJdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
    return new JdbcTemplate(dataSource);
    }

    }
    3. 4个机构数据库中user表的建表语句如下:
    CREATE TABLE `user` (
    `id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
    `name` varchar(20) DEFAULT NULL COMMENT '用户名',
    `password` varchar(20) DEFAULT NULL COMMENT '密码',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

    4. 定义User.java
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
    private Integer id;
    private String name;
    private String password;
    public User(String name, String password) {
    this.name = name;
    this.password = password;
    }
    }
    5. 定义User的数据库访问对象UserDao.java
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.jdbc.core.PreparedStatementCreator;
    import org.springframework.jdbc.core.RowMapper;
    import org.springframework.jdbc.support.GeneratedKeyHolder;
    import org.springframework.jdbc.support.KeyHolder;
    import org.springframework.stereotype.Repository;
    import org.springframework.transaction.annotation.Transactional;

    import com.wym.mycatdemo.model.User;

    @Repository
    public class UserDao {
    public static final String TENANT_SQL_TEMPLATE = "/*!mycat:schema= {0}*/{1}";
    @Autowired
    @Qualifier("businessJdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    @Transactional(readOnly = true)
    public List<User> findAll(String tenantSchema) {
    return jdbcTemplate.query(MessageFormat.format(TENANT_SQL_TEMPLATE, tenantSchema, "select * from user"), new UserRowMapper());
    }

    @Transactional(readOnly = true)
    public User findUserById(String tenantSchema, int id) {
    return jdbcTemplate.queryForObject(MessageFormat.format(TENANT_SQL_TEMPLATE, tenantSchema, "select * from user where id=?"), new Object[] { id }, new UserRowMapper());
    }

    @Transactional
    public User create(String tenantSchema, final User user) {
    final String sql = MessageFormat.format(TENANT_SQL_TEMPLATE, tenantSchema, "insert into user(name,password) values(?,?)");

    KeyHolder holder = new GeneratedKeyHolder();

    jdbcTemplate.update(new PreparedStatementCreator() {

    @Override
    public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
    PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
    ps.setString(1, user.getName());
    ps.setString(2, user.getPassword());
    return ps;
    }
    }, holder);

    int newUserId = holder.getKey().intValue();
    user.setId(newUserId);
    return user;
    }

    @Transactional
    public void delete(String tenantSchema, final Integer id) {
    final String sql = MessageFormat.format(TENANT_SQL_TEMPLATE, tenantSchema, "delete from user where id=?");
    jdbcTemplate.update(sql, new Object[] { id }, new int[] { java.sql.Types.INTEGER });
    }

    @Transactional
    public void update(String tenantSchema, final User user) {
    jdbcTemplate.update(MessageFormat.format(TENANT_SQL_TEMPLATE, tenantSchema, "update user set name=?,password=? where id=?"),
    new Object[] { user.getName(), user.getPassword(), user.getId() });
    }

    class UserRowMapper implements RowMapper<User> {

    @Override
    public User mapRow(ResultSet rs, int rowNum) throws SQLException {
    User user = new User();
    user.setId(rs.getInt("id"));
    user.setName(rs.getString("name"));
    user.setPassword(rs.getString("password"));
    return user;
    }

    }
    }
    6. 测试内编写
    import java.util.List;

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

    import com.wym.mycatdemo.dao.UserDao;
    import com.wym.mycatdemo.model.User;

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringBootTest(classes = SpringBootJdbcDemoApplication.class) // 指定spring-boot的启动类

    public class SpringBootJdbcDemoApplicationTests {

    @Autowired
    private UserDao userDao;

    @Test
    public void findAllUsers() {
    List<User> users = userDao.findAll("JG1DB");
    System.out.println(users);

    }

    @Test
    public void findUserById() {
    User user = userDao.findUserById("JG2DB",1);
    System.out.println(user);
    }

    @Test
    public void updateById() {
    User user = userDao.findUserById("JG3DB",2);
    System.out.println(user);
    User newUser = new User(2, "JackChen", "JackChen@qq.com");
    userDao.update("JG3DB", newUser);
    User newUser2 = userDao.findUserById("JG3DB",newUser.getId());
    System.out.println(newUser2);
    }

    @Test
    public void createUser() {
    User user = new User("rose", "rose@gmail.com");
    User savedUser = userDao.create("JG4DB",user);
    user = userDao.findUserById("JG4DB",savedUser.getId());
    System.out.println(user);
    }
    @Test
    public void findAllUsers1() {
    List<User> users = userDao.findAll("JG5DB");
    System.out.println("----------------------------------"+users);

    }

    }
    7. 由运行结果即可得知,数据访问被MyCat正确的路由到各个机构的数据库中。


    当然这个只是演示例子,正式项目里一个比较好的思路是:

    前端登录验证成功后,租户编码存入cookie中或本地缓存里
    前端通过HTTP方式访问后端api时,都需要在header中附带租户编码信息
    后端的 Restful API 的 定义2个filter, 比较一个叫prefilter, 一个叫postfilter, prefilter中读取http request中的header里的租户编码信息,并把它写入一个public static 的ThreadLocal对象, postfilter负责ThreadLocal对象的清除
    DAO层从ThreadLocal对象中抓取租户编码,并把租户编码附加到sql语句头部

    踩坑记录:

    1. 为每个机构都定义个一个表名叫order的表,但是mycat不认,查了半天,结果发现是由于order是SQL查询关键字造    成的

    2.MyCat中的表ID定义成自增长型,而且id自增长配置为
    <property name="sequnceHandlerType">2</property>, 文档上说明为本地时间戳算法:
    ID= 64位二进制 (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加)
    换算成十进制为18位数的long类型,每毫秒可以并发12位二进制的累加
    但是MyCAT老是报id冲突,但从日志看生成的id与数据库中已有的并不冲突,查了半天是因为ID的类型是int 32位,生成的id为64的,插入时被截断(可能是保留高位),因此产生冲突。


    完整的代码,请访问:
    https://github.com/tangaiyun/multitenancybymycat
    --------------------- 
    作者:suncold 
    来源:CSDN 
    原文:https://blog.csdn.net/suncold/article/details/79814926 
    版权声明:本文为博主原创文章,转载请附上博文链接!

    展开全文
  • saas系统服务数据按不同商户分库是比较简单安全的方案,不同商户数据分库隔离后不存在访问数据跨表跨库的问题,根据不同商户的单量灵活配置,单量少的可以公用一个库,单量大的可以独立集群。 第一步 实现spring...

    SAAS 按租户分库方案

     

    saas系统服务数据按不同商户分库是比较简单安全的方案,不同商户数据分库隔离后不存在访问数据跨表跨库的问题,根据不同商户的单量灵活配置,单量少的可以公用一个库,单量大的可以独立集群。 

     

    第一步

    实现spring 的AbstractRoutingDataSource 抽象类:

    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    /**
     * Created by chenwenshun on 2018/12/12.
     */
    
    public class RoutingDataSource extends AbstractRoutingDataSource{
    
        @Override
        protected Object determineCurrentLookupKey() {
            return RoutingDataSourceContextHolder.get();
        }
    }
    
    

    写一个线程容器保存每个请求的数据源信息:

    /**
     * Created by chenwenshun on 2018/12/12.
     */
    public class RoutingDataSourceContextHolder {
    
    
        private static final ThreadLocal<DataSourceEnum> threadlocalDataSourceKey = new ThreadLocal<>();
    
        public static  void set(DataSourceEnum key){
            threadlocalDataSourceKey.set(key);
        }
    
        public static DataSourceEnum get(){
             return threadlocalDataSourceKey.get();
        }
    
    
    
        public static void clear()  {
            threadlocalDataSourceKey.remove();
        }
    
    
    
    
    }

    第二步

    修改application.yml数据源配置:

    datasource:
      druid:
        url: jdbc:mysql://{ip}:6630/dict?useUnicode=true&characterEncoding=utf8
        driver-class: com.mysql.jdbc.Driver
        username: xxxxxx
        password: xxxxxx
        initial-size: 1
        min-idle: 1
        max-active: 20
        test-on-borrow: true
        filters: stat
    
    datasource2:
      druid:
        url: jdbc:mysql://{ip}:6630/dict2?useUnicode=true&characterEncoding=utf8
        driver-class: com.mysql.jdbc.Driver
        username: xxxxxx
        password: xxxxxx
        initial-size: 1
        min-idle: 1
        max-active: 20
        test-on-borrow: true
        filters: stat

    对应的java 配置:

        
    
    /**
     * Created by chenwenshun on 2018/8/31.
     */
    @Configuration
    public class DataSourceConfig {
    
        @Bean
        @ConfigurationProperties(prefix = "datasource.druid")
        public DataSource dataSource_0(){
            return DataSourceBuilder.create()
                    .type(DruidDataSource.class)
                    .build();
        }
    
        @Bean
        @ConfigurationProperties(prefix = "datasource2.druid")
        public DataSource dataSource_1(){
            return DataSourceBuilder.create()
                    .type(DruidDataSource.class)
                    .build();
        }
    
    
        @Bean
        @Primary
        public DataSource RoutingDataSource(
                @Autowired @Qualifier("dataSource_0") DataSource dataSource_0,
                @Autowired @Qualifier("dataSource_1") DataSource dataSource_1
        ){
            Map<Object, Object> map = new HashMap<>();
            map.put(DataSourceEnum.DS_0, dataSource_0);
            map.put(DataSourceEnum.DS_1, dataSource_1);
    
            RoutingDataSource routingDataSource = new RoutingDataSource();
            routingDataSource.setTargetDataSources(map);
            routingDataSource.setDefaultTargetDataSource(dataSource_0);
            return routingDataSource;
        }

    第三步

    实现一个商户与数据的映射逻辑,接口类似定义如下:

    
    /**
     * Created by chenwenshun on 2018/12/14.
     * 商户与数据源的映射关系
     * 具体项目不同实现
     * 如:用数据库配置或者apollo,等方式
     */
    
    public interface DataSourceMapping {
    
    
        /**
         * 商户ID 返回对应的数据源,可以采取两种方式
         *
         * 1、通过具体的配置
         *
         * 2、通过自己实现路由算法返回对应的数据源
         * @param availableDataSources 可用数据源
         * @param shareValue 业务ID如:商户ID
         * @return 数据源标示
         */
        String getDataSource(List<String> availableDataSources, String shareValue);
    }

    第四步

    通过切面拦截所有controller 请求,获取商户ID,然后更具商户ID 调用DataSourceMapping.getDataSource 获取不同商户对应的数据源, 设置到RoutingDataSourceContextHolder 线程变量:

    /**
     * Created by chenwenshun on 2018/12/12.
     */
    @Aspect
    @Component
    public class RoutingDataSourceAspect {
    
    
        @Autowired
        private DataSourceMapping dataSourceMapping;
    
        @Pointcut("execution(public * com.freemud.springbootdemo.controller.*.*(..))")
        public void point() {
        }
    
    
    
    
        @Before("point()")
        public void doBefore(JoinPoint joinPoint) throws ClassNotFoundException, NotFoundException {
            String classType = joinPoint.getTarget().getClass().getName();
    //        Class<?> clazz = Class.forName(classType);
    //        String clazzName = clazz.getName();
            String methodName = joinPoint.getSignature().getName(); //获取方法名称
            Object[] args = joinPoint.getArgs();//参数
            Map<String,Object> nameAndArgs = this.getFieldsName(this.getClass(),classType,methodName,args);
    
            String companyId = null;
    
            if (nameAndArgs.containsKey("companyId")){
                companyId = (String)nameAndArgs.get("companyId");
            }else if (nameAndArgs.containsKey("requestBody")){
    
                 BaseRequest request = (BaseRequest)nameAndArgs.get("requestBody");
                 companyId = request.getCompanyId();
    
            }else {
                BaseRequest request = (BaseRequest)Lists.newArrayList(nameAndArgs.values()).get(0);
                companyId = request.getCompanyId();
            }
    
            if (StringUtils.isBlank(companyId)){
                throw new  UnsupportedOperationException("companyId can not be null!");
            }
    
            String ds = DataSourceEnum.DS_0.name();
            if (dataSourceMapping != null){
                List<String> dataSourceList = Lists.newArrayList();
                for ( DataSourceEnum dataSourceEnum :DataSourceEnum.values() ) {
                    dataSourceList.add( dataSourceEnum.name() );
                }
    
                ds = dataSourceMapping.getDataSource(dataSourceList , companyId) ;
            }
    
            RoutingDataSourceContextHolder.set(DataSourceEnum.valueOf(ds));
        }
    
    
        @After("point()")
        public void doAfter(){
            RoutingDataSourceContextHolder.clear();
        }
    
    
        private Map<String,Object> getFieldsName(Class cls, String clazzName, String methodName, Object[] args) throws NotFoundException {
            Map<String,Object > map=new LinkedHashMap<>();
            ClassPool pool = ClassPool.getDefault();
            ClassClassPath classPath = new ClassClassPath(cls);
            pool.insertClassPath(classPath);
    
            CtClass cc = pool.get(clazzName);
            CtMethod cm = cc.getDeclaredMethod(methodName);
            MethodInfo methodInfo = cm.getMethodInfo();
            CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
            LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
            int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
            for (int i = 0; i < cm.getParameterTypes().length; i++){
                map.put( attr.variableName(i + pos),args[i]);//paramNames即参数名
            }
            return map;
        }
    
    
    }
    

    over ^0^

    转载于:https://www.cnblogs.com/wenshun/p/10160960.html

    展开全文
  • 目录 0.前言 1.需求分析 2.系统架构设计 3.环境准备 4.编码实现 4.1添加父项目依赖坐标 4.2实现eureka注册中心 4.3实现zuul网关 ...上一篇文章中,我们自己实现了saas系统架构中租户数据隔离的其中一...

    目录

    0. 前言

    1. 需求分析

    2. 系统架构设计

    3. 环境准备

    4. 编码实现

    4.1 添加父项目依赖坐标

    4.2 实现eureka注册中心

    4.3 实现zuul网关

    4.4 实现用户微服务mt2-user

    4.5 实现资料微服务mt2-profile

    5. 项目测试

    总结

    参考资料


    0. 前言

    上一篇文章中,我们自己实现了saas系统架构中租户数据隔离的其中一种解决方案,即使用租户id字段来实现同一张数据表中不同租户数据的增删改查。本文中,我们再来尝试实现另外一种解决方案,即每个租户使用独立的表空间(schema)的方式。

    我们还是将编写一个小小的demo来实现这个方案,只不过这次我们将使用springcloud + mybatis-plus的框架组合,将上一篇文章中的demo升级一下。

    事先声明,本文仅说明如何实现多租户的数据隔离,并不展开讨论其他问题,例如登录后会话有效期,超时后会话清理,数据库集群环境下的数据同步,微服务节点的负载均衡和路由等。

    嫌看文章麻烦啰嗦的大神,可以直接去看本文所涉及的代码。下面是码云仓库地址

    https://gitee.com/zectorlion/MultiTenancy

    仓库中的Solution2项目既是本文相关的代码(带sql脚本哦)

    在开始正文之前,有几个概念有必要先和大家交代一下,要不然大家后面看的可能会感觉晕。首先,表空间、schema,你可以把它俩理解为一个东西。不过数据库和schema,可能很多人会搞混。本文中,数据库和schema也可以理解为同一个概念。但是数据库和数据库系统是两个不同的概念,数据库系统就是我们安装在机器上的一个软件,而数据库就是数据库系统中保存数据的一个仓库,这个仓库中有表、视图、索引等数据模型,所以有人也叫它schema。一个数据库系统中可以创建有多个数据库或者schema。

     

    1. 需求分析

    本次我们实现的demo仍然是提供两个对外的api接口,调用方式也和上一篇文章中的demo一样。也既是用户登录接口和资料数据增删改查接口。

    1. 用户登录接口:接口访问地址是/user/login/{id},用户将自己的id号传递给接口进行登录。用户登录成功后,系统为用户生成一个 token 并返回给用户。

    2. 资料数据增删改查接口:接口访问地址是/profile/findAll/{token}、/profile/add/{token}等。用户调用接口时,必须携带其登录时系统返回给他的 token。

     

    2. 系统架构设计

    上一篇文章中的demo项目是将用户服务和资料管理服务写在一起的,本文中我们将这两个服务拆分成两个微服务,即user微服务和profile微服务,然后通过zuul网关实现统一的访问路径路由映射。当然,有了zuul,那就得配套一个eureka注册中心了。整个架构其实没那么复杂,如下图所示

    从图中我们可以看到端口的分配情况,汇总如下表

    微服务名称 使用的端口号
    eureka注册中心(mt2-eureka) 8000
    zuul网关(mt2-zuul) 8080
    用户微服务(mt2-user) 8081
    资料微服务(mt2-profile) 8082

     

    3. 环境准备

    首先仍然给大家交代一下我所使用的系统环境和对应的版本,避免大家在版本号的问题上踩坑。

    - springboot:2.1.4.RELEASE

    - springcloud:Greenwich.SR1

    - mybatis-plus:3.0.5

    - mysql数据库:5.7.26-log MySQL Community Server (GPL)

    - 谷歌浏览器:76.0.3809.132(正式版本) (64 位)
     

    首先我们还是要先准备一些测试数据,只不过这次我们要创建三个库,也既是三个schema,分别是mt-user,profile1和profile2。其中mt-user库中就一张user表,内容和之前一样,有6个用户,分属于两个租户。profile1和profile2两个库各自有一张profile表,分别存放不同租户的数据。整个数据库中schema的结构如下图所示

    使用下面的sql脚本,可以直接完成建库,建表和导数据的整个过程。

    -- Dump created by MySQL pump utility, version: 5.7.26, Win64 (x86_64)
    -- Dump start time: Sun Sep 01 20:55:29 2019
    -- Server version: 5.7.24
    
    SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
    SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
    SET @OLD_SQL_MODE=@@SQL_MODE;
    SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
    SET @@SESSION.SQL_LOG_BIN= 0;
    SET @OLD_TIME_ZONE=@@TIME_ZONE;
    SET TIME_ZONE='+00:00';
    SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT;
    SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS;
    SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION;
    SET NAMES utf8mb4;
    CREATE DATABASE /*!32312 IF NOT EXISTS*/ `mt-user` /*!40100 DEFAULT CHARACTER SET utf8 */;
    CREATE TABLE `mt-user`.`user` (
    `id` bigint(20) NOT NULL COMMENT '主键',
    `tenant_id` bigint(20) NOT NULL COMMENT '服务商ID',
    `name` varchar(30) DEFAULT NULL COMMENT '姓名',
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    ;
    INSERT INTO `mt-user`.`user` VALUES (1,1,"Tony老师"),(2,1,"William老师"),(3,2,"路人甲"),(4,2,"路人乙"),(5,2,"路人丙"),(6,2,"路人丁");
    CREATE DATABASE /*!32312 IF NOT EXISTS*/ `profile1` /*!40100 DEFAULT CHARACTER SET utf8 */;
    CREATE TABLE `profile1`.`profile` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `title` varchar(20) DEFAULT NULL,
    `content` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
    ;
    INSERT INTO `profile1`.`profile` VALUES (1,"1号档案","1号档案");
    CREATE DATABASE /*!32312 IF NOT EXISTS*/ `profile2` /*!40100 DEFAULT CHARACTER SET utf8 */;
    CREATE TABLE `profile2`.`profile` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `title` varchar(20) DEFAULT NULL,
    `content` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
    ;
    INSERT INTO `profile2`.`profile` VALUES (2,"2号档案","2号档案");
    SET TIME_ZONE=@OLD_TIME_ZONE;
    SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT;
    SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS;
    SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION;
    SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
    SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
    SET SQL_MODE=@OLD_SQL_MODE;
    -- Dump end time: Sun Sep 01 20:55:31 2019
    

     

    搞定测试数据以后,下面我们再把项目骨架创建出来。本文中的项目结构还是使用典型的一父多子的结构,父项目是Solution2,子项目分别是mt2-eureka,mt2-zuul,mt2-user,mt2-profile。整个项目的结构如下图

    至此,测试数据和项目骨架都已经准备好了,我们的准备工作也完成了。下面可以开始动手写代码了。

     

    4. 编码实现

    下面我们开始为项目添加代码,填充“血肉”,给项目赋予“灵魂”。

    4.1 添加父项目依赖坐标

    首先我们先编辑父项目Solution2的pom文件,把几个子项目都会用到的依赖坐标添加进去

    <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-dependencies</artifactId>
                    <version>2.1.4.RELEASE</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>Greenwich.SR1</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
        </dependencies>

    4.2 实现eureka注册中心

    这个没啥好说的,经典的三部曲,改pom文件,添加application.yml配置文件,编写启动引导类。我就不啰嗦了,直接上代码上配置。注意,改的是mt2-eureka项目中的代码和文件,别搞错了。

    pom文件:

        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            </dependency>
        </dependencies>

    application.yml文件:

    server:
      port: 8000
    spring:
      application:
        name: mt2-zureka
    eureka:
      client:
        register-with-eureka: false #是否将自己注册到eureka中
        fetch-registry: false #是否从eureka中获取信息
        service-url:
          defaultZone: http://0.0.0.0:${server.port}/eureka/

    启动引导类:

    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EurekaApplication.class);
        }
    
    }

     

    4.3 实现zuul网关

    这个也没啥好说的,也是改pom文件,添加application.yml配置文件,编写启动引导类这经典的三部曲。不啰嗦,直接上代码上配置。注意,改的是mt2-zuul项目中的代码和文件,别搞错了。

    pom文件:

        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
            </dependency>
        </dependencies>

    application.yml文件:

    server:
      port: ${SERVER_PORT:8080}
    spring:
      application:
        name: mt2-zuul
    ribbon:
      ReadTimeout: 300000
      ConnectTimeout: 300000
      MaxAutoRetries: 3
      MaxAutoRetriesNextServer: 3
      eureka:
        enabled: true
    
    #hystrix超时熔断配置
    hystrix:
      command:
        cmut-app-api:
          execution:
            timeout:
              enabled: true
            isolation:
              thread:
                timeoutInMilliseconds: 300000
    
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:8000/eureka/
      instance:
        prefer-ip-address: true
    
    zuul:
      routes:
        mt2-user: #用户
          path: /user/** #配置请求URL的请求规则
          serviceId: mt2-user #指定Eureka注册中心中的服务id
          strip-prefix: true
          sentiviteHeaders:
          customSensitiveHeaders: true
        mt2-profile: #用户
          path: /profile/** #配置请求URL的请求规则
          serviceId: mt2-profile #指定Eureka注册中心中的服务id
          strip-prefix: true
          sentiviteHeaders:
          customSensitiveHeaders: true
    
    

    启动引导类:

    @SpringBootApplication
    @EnableEurekaClient
    @EnableZuulProxy
    public class ZuulApplication {
    
    
        public static void main(String[] args) {
            SpringApplication.run(ZuulApplication.class);
        }
    }

     

    4.4 实现用户微服务mt2-user

    首先还是先进行经典三部曲,改pom文件,加配置,创建启动引导类。

    pom文件:

        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.0.5</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus</artifactId>
                <version>3.0.5</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>3.0.5</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.9</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
        </dependencies>

    application.yml文件:

    server: 
      port: 8081
    spring: 
      application:  
        name: mt2-user
      datasource:
        url: jdbc:mysql://192.168.228.100:3306/mt-user?characterEncoding=UTF8
        username: root
        password: 123456
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
    
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:8000/eureka/
      instance:
        prefer-ip-address: true
    

    启动引导类:

    @SpringBootApplication
    @EnableEurekaClient
    public class Mt2UserApp {
    
        public static void main(String[] args) {
            SpringApplication.run(Mt2UserApp.class, args);
        }
    
        @Bean
        public TenantContext tenantContext() {
            return new TenantContext();
        }
    }

    然后把上一篇文章中的Solution3项目中所有关于user服务的controller,实体类,mapper等代码复制过来。当然,配置类MybatisPlusConfig需要重写。下面是MybatisPlusConfig配置类的代码

    @Configuration
    @MapperScan("mt2.user.mapper")
    public class MybatisPlusConfig {
    
        @Bean
        public PaginationInterceptor paginationInterceptor() {
            PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
    
            return paginationInterceptor;
        }
    
        @Bean(name = "performanceInterceptor")
        public PerformanceInterceptor performanceInterceptor() {
            return new PerformanceInterceptor();
        }
    }

    其实就是把paginationInterceptor方法中设置多租户sql处理的那段代码给去掉,变成标准的返回分页对象的代码。

     

    UserController中也要添加一个tenantIdByToken接口,让其他微服务可以根据token获取到租户id。把下面这段代码添加到UserController中即可。

        @GetMapping("/tenantIdByToken/{token}")
        public Long tenantIdByToken(@PathVariable("token") String token) {
            return tenantContext.getTenantIdWithToken(token);
        }

     

    至此,mt2-user用户微服务就搭建完成了。整个mt2-user项目的目录结构如下图

     

    4.5 实现资料微服务mt2-profile

    mt2-profile项目是今天的重点,每个租户使用独立表空间的方案,就是由这个项目来实现的。我的想法是使用租户id去识别应该要连接的schema,schema的名字是 profile 加租户id的格式。例如,租户id如果是1,那么租户1连接的就应该是 profile1 这个schema。具体是如何来实现的,后面我会详细去说。现在的话,我们还是先把经典三部曲搞完,保证mt2-profile项目有一个良好的基础环境。

    pom文件:

        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.0.5</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus</artifactId>
                <version>3.0.5</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>3.0.5</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.9</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
        </dependencies>

    因为用户调用profile接口时会将token传递过来,而mt2-profile项目需要用token向mt2-user微服务换取租户id号,以便在进行数据库sql操作之前切换到租户对应的datasource上去,所以需要使用feign调用mt2-user微服务的接口。

     

    application.yml文件:

    server: 
      port: 8082
    spring: 
      application:  
        name: mt2-profile
    
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:8000/eureka/
      instance:
        prefer-ip-address: true
    
    tenant:
      datasource:
        host: 192.168.228.100
        port: 3306
        username: root
        password: 123456
        schema: profile
    

    在mt2-profile的application.yml配置文件中,并没有数据源datasource的配置,这是因为租户的数据源是在mt2-profile微服务运行过程中动态创建的。我们会在mt2-profile微服务中创建一个配置类TenantDatasouceConfig,读取该配置文件中前缀为tenant.datasource的配置,以便在动态创建datasource的时候使用。

     

    启动引导类:

    @SpringBootApplication
    @EnableEurekaClient
    @EnableFeignClients
    public class Mt2ProfileApp {
    
        public static void main(String[] args) {
            SpringApplication.run(Mt2ProfileApp.class, args);
        }
    
        @Bean
        public DataSourceBuilder dataSourceBuilder() {
            DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create().driverClassName("com.mysql.jdbc.Driver");
            return dataSourceBuilder;
        }
    
        @Bean(name = "dataSourceMap")
        public Map<Object, Object> dataSourceMap() {
            Map<Object, Object> dataSourceMap = Maps.newConcurrentMap();
            return dataSourceMap;
        }
    }

    在mt2-profile微服务的启动引导类中,我们向springIOC容器中注入了两个bean对象,一个是用于在mt2-profile微服务运行时动态创建datasource的DataSourceBuilder,一个是用于保存租户id和datasource键值对的CocurrentHashMap,名字是dataSourceMap。我们随后将在很多地方使用到这两个bean。

     

    然后我们创建两个工具类,AppContextHelper和DynamicRoutingDataSource,并把它们放到utils包下面。

    AppContextHelper可以让我们根据class的名称,类型等信息,从springIOC容器中拿到对应的class实例。

    DynamicRoutingDataSource是实现动态切换数据源的类,它是AbstractRoutingDataSource抽象类的扩展,也是我们实现根据租户id切换数据源的关键。以前也有人写过很多根据名称切换数据源的示例项目,只不过数据源的配置是提前在配置文件中定义好的,然后通过自定义注解,在程序运行的过程当中进行切换。而切换的关键,就是通过扩展AbstractRoutingDataSource抽象类,然后实现determineCurrentLookupKey方法,返回要切换的数据源的名称,实现切换数据源的目的。详细的情况,大家看一下AbstractRoutingDataSource抽象类的源码,就都清楚了。

    AppContextHelper的代码

    @Component
    public class AppContextHelper implements ApplicationContextAware {
        private static ApplicationContext applicationContext;
    
        public AppContextHelper() {
        }
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            AppContextHelper.applicationContext = applicationContext;
        }
    
        public static Object getBean(String beanName) {
            return applicationContext != null?applicationContext.getBean(beanName):null;
        }
    
        //通过class获取Bean.
        public static <T> T getBean(Class<T> clazz) {
            return applicationContext.getBean(clazz);
        }
    }

    DynamicRoutingDataSource的代码

    /**
     * Multiple DataSource Configurer
     */
    @Data
    public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    
        private Long tenantId;
    
        private final Logger logger = LoggerFactory.getLogger(getClass());
    
        /**
         * Set dynamic DataSource to Application Context
         *
         * @return
         */
        @Override
        protected Object determineCurrentLookupKey() {
            logger.debug("Current DataSource is [{}]", tenantId);
            return tenantId;
        }
    }

     

    然后我们创建一个TenantDatasouceConfig配置类,通过@ConfigurationProperties注解读取配置文件中前缀是tenant.datasource配置项的内容,并使用@Component注解将该配置类注入到springIOC容器中

    @Data
    @ConfigurationProperties(prefix = "tenant.datasource")
    @Component
    public class TenantDatasouceConfig {
        private String host;
        private int port;
        private String username;
        private String password;
        private String schema;
    }

     

    我们再在config包下面编写MybatisPlusConfig配置类,在该配置类中创建一个DynamicRoutingDataSource类的实例并注入到springIOC容器中

    @Configuration
    @MapperScan("mt2.profile.mapper")
    public class MybatisPlusConfig {
    
        @Autowired
        private Map<Object, Object> dataSourceMap;
    
        @Autowired
        private DataSourceBuilder dataSourceBuilder;
    
        @Autowired
        private TenantDatasouceConfig tdc;
    
        @Bean
        public PaginationInterceptor paginationInterceptor() {
    
            return new PaginationInterceptor();
        }
    
        @Bean
        public PerformanceInterceptor performanceInterceptor() {
            return new PerformanceInterceptor();
        }
    
        /**
         * Dynamic data source.
         *
         * @return the data source
         */
        @Bean("dynamicDataSource")
        public DataSource dynamicDataSource() {
            DynamicRoutingDataSource dynamicRoutingDataSource = new DynamicRoutingDataSource();
    
            dataSourceBuilder.url(String.format("jdbc:mysql://%s:%d?useSSL=false", tdc.getHost(), tdc.getPort()));
            dataSourceBuilder.username(tdc.getUsername());
            dataSourceBuilder.password(tdc.getPassword());
            DataSource dataSource = dataSourceBuilder.build();
    
            dataSourceMap.put((long) 0, dataSource);
            dynamicRoutingDataSource.setDefaultTargetDataSource(dataSource);
            // 可动态路由的数据源里装载了所有可以被路由的数据源
            dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
    
            return dynamicRoutingDataSource;
        }
    }
    

     

    接下来我们编写一个拦截器TenantInterceptor,它的功能是,拦截访问mt2-profile微服务的请求,从访问路径中获取到token,然后携带token调用mt2-user微服务的接口,拿到租户id。然后使用租户id从dataSourceMap中查询datasource对象,如果为空,则创建该租户id的专属数据源,并放入dataSourceMap和DynamicRoutingDataSource中。如果不为空,则将租户id设置进DynamicRoutingDataSource对象的tenantId属性。这样在进行数据库操作之前,mybatis-plus就知道该去哪个数据源执行sql操作了。TenantInterceptor代码如下

    @Component
    public class TenantInterceptor implements HandlerInterceptor {
    
        @Autowired
        private DataSourceBuilder dataSourceBuilder;
    
        @Autowired
        private Map<Object, Object> dataSourceMap;
    
        @Autowired
        private UserClient  userClient;
    
        @Autowired
        private TenantDatasouceConfig tdc;
    
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
            String path=httpServletRequest.getRequestURI();
            String token = path.substring(path.lastIndexOf("/") + 1);
            if (null != token) {
                //UserClient userClient = (UserClient) AppContextHelper.getBean(UserClient.class);
                Long tenantId = userClient.tenantIdByToken(token);
                if(null != tenantId) {
                    prepareDatasource(tenantId);
    
                    return true;
                }
            }
    
            return false;
        }
    
        private void prepareDatasource(Long tenantId) {
            DynamicRoutingDataSource dynamicDataSource = (DynamicRoutingDataSource) AppContextHelper.getBean("dynamicDataSource");
            DataSource dataSource = (DataSource) dataSourceMap.get(tenantId);
    
            if (null == dataSource) {
                dataSourceBuilder.url(String.format("jdbc:mysql://%s:%d/%s%d?useSSL=false&characterEncoding=UTF8", tdc.getHost(), tdc.getPort(), tdc.getSchema(), tenantId));
                dataSourceBuilder.username(tdc.getUsername());
                dataSourceBuilder.password(tdc.getPassword());
                dataSource = dataSourceBuilder.build();
    
                dataSourceMap.put(tenantId, dataSource);
                dynamicDataSource.setTargetDataSources(dataSourceMap);
                dynamicDataSource.afterPropertiesSet();
            }
    
            dynamicDataSource.setTenantId(tenantId);
        }
    }
    

    然后我们再为TenantInterceptor拦截器编写一个配置类,将拦截器注入进springMVC容器中,使其生效。

    @Configuration
    @Order()
    public class InterceptorConfig extends WebMvcConfigurerAdapter {
    
        @Autowired
        private TenantInterceptor tenantInterceptor;
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(tenantInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login/**");
            super.addInterceptors(registry);
        }
    }

     

    由于TenantInterceptor拦截器中使用了feign远程调用,所以我们还需要编写feign client远程调用代码。在client包下面创建UserClient接口,代码如下

    @FeignClient(value="mt2-user")
    public interface UserClient {
    
        @GetMapping("/user/tenantIdByToken/{token}")
        public Long tenantIdByToken(@PathVariable("token") String token);
    }

     

    最后是controller的代码

    @RestController
    @RequestMapping("/profile")
    public class ProfileController {
    
        @Autowired
        private ProfileMapper profileMapper;
    
        @GetMapping("/findAll/{token}")
        public String findAll(@PathVariable String token) {
    
            //prepareTenantContext(token);
    
            List<Profile> profiles = profileMapper.selectList(null);
            profiles.forEach(System.out::println);
            return "operation complete, the following is the result \n" + profiles.toString();
        }
    
        @GetMapping("/add/{token}")
        public String add(@PathVariable String token) {
    
            Profile p = new Profile();
            p.setId((long) 3);
            p.setTitle("3号档案");
            p.setContent("3号档案");
    
            int result = profileMapper.insert(p);
            return "operation complete, the following is the result \n" + String.valueOf(result);
        }
    
        @GetMapping("/update/{token}")
        public String update(@PathVariable String token) {
    
            Profile p = new Profile();
            p.setId((long) 3);
            p.setTitle("4号档案");
            p.setContent("4号档案");
    
            int result = profileMapper.updateById(p);
            return "operation complete, the following is the result \n" + String.valueOf(result);
        }
    
        @GetMapping("/delete/{token}")
        public String delete(@PathVariable String token) {
    
            int result = profileMapper.deleteById((long)3);
            return "operation complete, the following is the result \n" + String.valueOf(result);
        }
    }

    实体类和mapper文件的代码我就不贴了,没啥技术含量。

     

    至此,mt2-profile微服务就编写完成了。整个mt2-profile微服务项目的文件结构如下

     

    5. 项目测试

    测试方法和Solution3项目的测试方法一样。我们首先把mt2-eureka,mt2-zuul,mt2-user,mt2-profile这几个微服务依次运行起来,然后打开浏览器,依次访问 http://localhost:8080/user/user/login/1 和 http://localhost:8080/user/user/login/6,得到两个token,然后再分别携带两个不同的token去调用 http://localhost:8080/profile/profile/findAll/{token} 接口,验证一下是否返回了对应租户的数据。篇幅问题,我就不亲自演示了。

     

    总结

    本文所展示的多租户使用独立的表空间的解决方案,核心是如何使用AbstractRoutingDataSource抽象类实现动态的数据源的切换,也就是本文中mt2-profile微服务所做的主要的工作。通过在进行数据库操作之前指定租户的schema,就可以对指定租户的数据进行增删改查操作,而不影响其他租户的schema中的数据,实现了租户数据的隔离。

     

    参考资料

    https://www.cnblogs.com/hsbt2333/p/9347249.html

     

    https://blog.csdn.net/jinsedeme0881/article/details/79171151

     

    展开全文
  • 多租户下的数据治理

    2019-08-12 10:05:15
    近期政府项目正在探讨如何给委办局开放大数据平台的能力,包括存储计算、数据治理、数据挖掘、数据分析等能力,要求平台以租户的形式支撑各项能力开放。其中,数据挖掘、数据分析等...公共数据区与租户数据区。将统...
  • SpringBoot 多租户多数据

    千次阅读 2020-04-04 01:07:51
    多租户多数据源开发背景方案实现实现思路实现代码如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个...
  • 聊完以后,实在手痒难耐,于是花了两天时间自己实现了两个saas系统多租户数据隔离实现方案。俗话说“独乐乐不如众乐乐”,所以我把我的“研究成果”写出来,让大家乐呵乐呵。 在分享我的研究成果之前,我们先了解...
  • 多租户技术现在是一种很常见的软件技术。网上会有很“优雅”的设计方案,但是在实际项目开发中,尤其是老项目,需要综合考虑“历史设计问题”、“对用户的影响程度”、“异常能否回滚”等各种因素。 目前项目现状...
  • 对可行性进行分析后选择了共享表,按租户id字段区分租户的方式去实现。以此记录一下方便日后所需查阅 1.熟悉多租户之前先来了解一下什么是SaaS系统 以下内容来着百度百科 SaaS平台是运营saas软件的平台。SaaS...
  • 多租户逻辑分离预发数据与正式数据 1. 依赖 <dependency> <groupId>com.github.rxyor</groupId> <artifactId>carp-mybatis-plus-tenant-starter</artifactId> <version>1.0....
  • // 利用 HasQueryFilter 进行租户数据隔离 var tenantId = _tenantInfoProvider.GetTenantId(); if (!string.IsNullOrEmpty(tenantId)) { modelBuilder.Entity<Album>().HasQueryFilter(x => x....
  • 多租户架构的核心定义包括两点,第一点是一个服务,或者更通俗地讲是一套代码,被多方客户共用,第二点是不同客户之间的数据在逻辑上是隔离的,即每方客户拥有自己独立的数据空间,并在这个独立的数据空间内完成自身...
  • Mycat 多租户方案

    万次阅读 2016-09-25 15:40:55
    Mycat多租户方案1、需求 1、1 需求图 1、2 环境说明 环境说明: 2 每租户一逻辑方案 2.1实现思想 用户在用用户名登陆时,首先,需要根据用户名,查询到该用户所在的逻辑,然后登陆成功后,将和会话信息...
  • 在数据库的世界里,我们经常面对的一个现实就是:久必合,合久必。大多数企业都在这样一个轮回中不断革新。比如国内的银行,早期多数是按省市分布式布局,随着技术发展进步,慢慢建立了区域中心、全国中心,将...
  • 多租户模型

    万次阅读 2019-03-05 20:38:27
    真正意义上的SaaS一定是多租户的,但是多租户根据隔离程度的不同又分为不同模式。 多租户根据隔离程度和共享程度分为三种模型,其实就是在共享程度与隔离程度的权衡选择。 共享程度越高,租户成本自然越低,技术...
  • # 自定义路由配置, 格式: 数据库 :片id db-sharding-config: shardingColumn: user_id shardingDbConfs: - {"ds0": ["0","2"]} - {"ds1": ["1","3"]} mybatis-plus: mapper-locations: classpath*:...
  • 数据层面的架构来说,基本上分成了多租户共享单一数据库、单一租户独享单一数据库以及介于两者之间的单一数下的单一租户独享单一schema三种方案。这篇文章 ...
  • 从参与方案会议到搭建开发上线过程中有很讨论点,故产生本文,希望藉此总结和分享一些经验。 1. 业务模型 接触已有的业务时,数据模型是最早需要知道的信息。我和同事负责接入 Elasticsearch 的产品是一个业务...
  • camunda 多租户

    2020-08-10 22:25:35
    先标注以下,没有能成功使用官网说明的schema 模式实现不同租户租户的关键配置如下: 配置processes.xml 启动时候指定租户 spring boot 的annotation配置 spring boot 的annotation 配置如下: @SpringBoot...
  • 架构中,软件应用程序旨在为每个租户提供一个包含其数据*,配置,用户管理,租户个人功能和非功能属性的实例共享*多租户实例架构形成鲜明对比,其中独立的软件实例运行 代表不同的租户“(维基百科),简而言之...
  • 多租户

    2017-09-12 16:56:00
    我觉得要解决这个问题,一个方法就是用分库分表的方式。简单一点就是不用租户对应不同的数据库。这样一个用户的数据量增加是不会很快的。 多租户的模式,要求在每一个业务逻辑代码中都要添加关于租户id的筛选条件,...
  • docker多租户架构应用 多租户应用程序是一个正在运行的实例为许多客户提供服务的应用程序。 多租户的替代方法是托管服务,其中为每个客户设置一个正在运行的实例。... 资源是根据租户的实际负载...
  • 在上一篇文章中,我简单的描述了一下PaaS架构中面临的一些问题,接下来会描述更问题,同时也会在文章的最后浅谈一下我们的解决方案. 动态拓展,是数据访问层在运行时,...那在数据层面的,为了保持的简洁,易维护,同...
  • 在这篇文章中不会就云,PaaS,或SaaS做任何讨论,只想为多租户情况下的数据拓展设计一套解决方案.[/size] 人们总是渴望自由,追求个性,SaaS尽管为大多数公司的通用应用提供了解决方式,但是这不足以吸引客户.因为客户...
  • Mycat+druid+zk实现多租户

    千次阅读 2019-07-07 08:09:09
    Mycat的应用场景之一就是实现多租户多租户应用,每个应用一个,但应用程序只连接 Mycat,从而不改造程序本身,实现多租户化;接下来我们使用mycat,结合druid拦截sql添加注释头,利用zk修改mycat配置文件中的...
  • MyCat相当于一个逻辑上的大数据库,又N个物理数据库组成,可以通过各种分库分表规则(rule)将数据存到规则对应的数据库或schema或表中。 MyCat对自身不支持的Sql语句提供了一种解决方案——在要执行的SQL语句前...
  • MyCat多租户方案

    2021-01-07 14:57:05
    使用navicat连接,mysql怎么连接,mycat就怎么连接 推荐先采用命令行测试 mysql -uroot -pFRN22@zz6 -P8066 -h127.0.0.1 Mycat配置 说明: 根据zhxh字段(租户序号)进行分库,按照mod-long分库规则分成五个库。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,732
精华内容 2,692
关键字:

多租户数据分库