基于Mycat的多租户分库方案

方案一:
租户共享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 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值