基于Mycat的多租户分库方案

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

方案二:
每个租户独占MyCat中的一个schema,schema的表不会跨datanode,类似的拓扑如下:
基于Mycat的多租户分库方案_第1张图片
MyCat核心配置:
  1. server.xml
password
GLOBALDB,JG1DB,JG2DB,JG3DB,JG4DB,JG5DB

2. schema.xml

writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
select user()
password="password">
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
select user()
password="password">
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
select user()
password="password">


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

  1. 在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 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 {

@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 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", "[email protected]");
userDao.update("JG3DB", newUser);
User newUser2 = userDao.findUserById("JG3DB",newUser.getId());
System.out.println(newUser2);
}

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

}

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


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

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


踩坑记录:

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

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


完整的代码,请访问:
https://github.com/tangaiyun/multitenancybymycat

你可能感兴趣的:(后端架构)