博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一种简单易懂的 MyBatis 分库分表方案
阅读量:2442 次
发布时间:2019-05-10

本文共 9772 字,大约阅读时间需要 32 分钟。

数据库分库分表除了使用中间件来代理请求分发之外,另外一种常见的方法就是在客户端层面来分库分表 —— 通过适当地包装客户端代码使得分库分表的数据库访问操作代码编写起来也很方便。本文的分库分表方案基于 MyBatis 框架,但是又不同于市面上常用的方案,它们一般都是通过编写复杂的 MyBatis 插件来重写 SQL 语句,这样的插件代码会巨复杂无比,可能最终只有插件的原作者自己可以完全吃透相关代码,给项目的维护性带来一定问题。本文的方案非常简单易懂,而且也不失使用上的便捷性。它的设计哲学来源于 Python —— Explicit is better than Implicit,也就是显式优于隐式,它不会将分库分表的过程隐藏起来。

很多分库分表的设计在实现上会尽量将分库分表的逻辑隐藏起来,其实这是毫无必要的。使用者必须知道背后确实进行了分库分表,否则他怎么会无法进行全局的索引查找?他怎么会无法随意进行多表的 join 操作。如果你真的将它当成单表来用,到上线时必然会出大问题。

640?wx_fmt=png

项目名称叫:shardino,项目地址:https://github.com/pyloque/shardino

接下来我们来看看在本文的方案之下,数据库操作代码的形式是怎样的

帖子表一共分出来 64 个表,不同的记录会各自分发到其中一个表,可以是按 hash 分发,也可以按照日期分发,分发逻辑由用户代码自己来决定。在不同的环境中可以将分表数量设置为不同的值,比如在单元测试下分表设为 4 个,而线上可能需要设置为 64 个。

@Configurationpublic class PartitionConfig {    private int post = 64;    public int post() {        return post;    }    public void post(int post) {        this.post = post;    }}

从配置文件中构建 MySQLGroupStore 数据库组对象,这个对象是我们执行 MySQL 操作的入口,通过它可以找到具体的物理的 MySQL 主从数据源。

@Configurationpublic class RepoConfig {    @Autowired    private Environment env;    private MySQLGroupBuilder mysqlGroupBuilder = new MySQLGroupBuilder();    @Bean    @Qualifier("post")    public MySQLGroupStore replyMySQLGroupStore() {        MySQLGroupStore store = mysqlGroupBuilder.buildStore(env, "post");        store.prepare(factory -> {            factory.getConfiguration().addMapper(PostMapper.class);        });        return store;    }}

mysql.post0.master.addrWeights=localhost:3306mysql.post0.master.db=samplemysql.post0.master.user=samplemysql.post0.master.password=123456mysql.post0.master.poolSize=10mysql.post0.slave.addrWeights=localhost:3307=100&localhost:3308=100mysql.post0.slave.db=samplemysql.post0.slave.user=samplemysql.post0.slave.password=123456mysql.post0.slave.poolSize=10mysql.post1.master.addrWeights=localhost:3309mysql.post1.master.db=samplemysql.post1.master.user=samplemysql.post1.master.password=123456mysql.post1.master.poolSize=10mysql.post1.slave.addrWeights=localhost:3310=100&localhost:3311=100mysql.post1.slave.db=samplemysql.post1.slave.user=samplemysql.post1.slave.password=123456mysql.post1.slave.poolSize=10mysqlgroup.post.nodes=post0,post1mysqlgroup.post.slaveEnabled=true

mysqlgroup 还有一个特殊的配置选项 slaveEnabled 来控制是否需要从库,从而关闭读写分离,默认是关闭的,这样就不会去构建从库实例相关对象。

post_k 这张表后缀 k 我们称之为 partition number,也就是后续代码中到处在用的 partition 变量,表明当前的记录被分配到对应物理数据表的序号。我们需要根据记录的内容计算出 partition number,再根据 partition number 决定出这条记录所在的物理表属于那个物理数据库,然后对这个物理数据库进行相应的读写操作。

在本例中,帖子表按照 userId 字段 hash 出 64 张表,平均分配到 2 对物理库中,每个物理库包含一个主库和2个从库。

有了 MySQLGroupStore 实例,我们就可以尽情操纵所有数据库了。

@Repositorypublic class PostMySQL {    @Autowired    private PartitionConfig partitions;    @Autowired    @Qualifier("post")    private MySQLGroupStore mysql;    public void createTables() {        for (int i = 0; i < partitions.post(); i++) {            int k = i;            mysql.master(k).execute(session -> {                PostMapper mapper = session.getMapper(PostMapper.class);                mapper.createTable(k);            });        }    }    public void dropTables() {        for (int i = 0; i < partitions.post(); i++) {            int k = i;            mysql.master(k).execute(session -> {                PostMapper mapper = session.getMapper(PostMapper.class);                mapper.dropTable(k);            });        }    }    public Post getPostFromMaster(String userId, String id) {        Holder
 holder = new Holder<>();        int partition = this.partitionFor(userId);        mysql.master(partition).execute(session -> {            PostMapper mapper = session.getMapper(PostMapper.class);            holder.value(mapper.getPost(partition, id));        });        return holder.value();    }    public Post getPostFromSlave(String userId, String id) {        Holder
 holder = new Holder<>();        int partition = this.partitionFor(userId);        mysql.slave(partition).execute(session -> {            PostMapper mapper = session.getMapper(PostMapper.class);            holder.value(mapper.getPost(partition, id));        });        return holder.value();    }    public void savePost(Post post) {        int partition = this.partitionFor(post);        mysql.master(partition).execute(session -> {            PostMapper mapper = session.getMapper(PostMapper.class);            Post curPost = mapper.getPost(partition, post.getId());            if (curPost != null) {                mapper.updatePost(partition, post);            } else {                mapper.insertPost(partition, post);            }        });    }    public void deletePost(String userId, String id) {        int partition = this.partitionFor(userId);        mysql.master(partition).execute(session -> {            PostMapper mapper = session.getMapper(PostMapper.class);            mapper.deletePost(partition, id);        });    }    private int partitionFor(Post post) {        return Post.partitionFor(post.getUserId(), partitions.post());    }    private int partitionFor(String userId) {        return Post.partitionFor(userId, partitions.post());    }}

mysql.master(partition)mysql.slave(partition)// 如果没有分库mysql.master()mysql.slave()// 如果既没有分库也没有读写分离mysql.db()// 操作具体的表时要带 partitionmapper.getPost(partition, postId)mapper.savePost(partition, post)

public interface PostMapper {    @Update("create table if not exists post_#{partition}(id varchar(128) primary key not null, user_id varchar(1024) not null, title varchar(1024) not null, content text, create_time timestamp not null) engine=innodb")    public void createTable(int partition);    @Update("drop table if exists post_#{partition}")    public void dropTable(int partition);    @Results({@Result(property = "createTime", column = "create_time"),            @Result(property = "userId", column = "user_id")})    @Select("select id, user_id, title, content, create_time from post_#{partition} where id=#{id}")    public Post getPost(@Param("partition") int partition, @Param("id") String id);    @Insert("insert into post_#{partition}(id, user_id, title, content, create_time) values(#{p.id}, ${p.userId}, #{p.title}, #{p.content}, #{p.createTime})")    public void insertPost(@Param("partition") int partition, @Param("p") Post post);    @Update("update post_#{partition} set title=#{p.title}, content=#{p.content}, create_time=#{p.createTime} where id=#{p.id}")    public void updatePost(@Param("partition") int partition, @Param("p") Post post);    @Delete("delete from post_#{partition} where id=#{id}")    public void deletePost(@Param("partition") int partition, @Param("id") String id);}

在 MyBatis 的注解 Mapper 类中,如果方法含有多个参数,需要使用 @Param 注解进行名称标注,这样才可以在 SQL 语句中直接使用相应的注解名称。否则你得使用默认的变量占位符名称 param0、param1 来表示,这就很不直观。

我们将分表的 hash 算法写在实体类 Post 中,这里使用 CRC32 算法进行 hash。

public class Post {    private String id;    private String userId;    private String title;    private String content;    private Date createTime;    public Post() {}    public Post(String id, String userId, String title, String content, Date createTime) {        this.id = id;        this.userId = userId;        this.title = title;        this.content = content;        this.createTime = createTime;    }    public String getId() {        return id;    }    public void setId(String id) {        this.id = id;    }    public String getUserId() {        return userId;    }    public void setUserId(String userId) {        this.userId = userId;    }    public String getTitle() {        return title;    }    public void setTitle(String title) {        this.title = title;    }    public String getContent() {        return content;    }    public void setContent(String content) {        this.content = content;    }    public Date getCreateTime() {        return createTime;    }    public void setCreateTime(Date createTime) {        this.createTime = createTime;    }    public int partitionFor(int num) {        return partitionFor(userId, num);    }    public static int partitionFor(String userId, int num) {        CRC32 crc = new CRC32();        crc.update(userId.getBytes(Charsets.UTF8));        return (int) (Math.abs(crc.getValue()) % num);    }}

还有最后一个问题是多个带权重的从库是如何做到概率分配的。这里就要使用到 spring-jdbc 自带的 AbstractRoutingDataSource —— 带路由功能的数据源。它可以包含多个子数据源,然后根据一定的策略算法动态挑选出一个数据源来,这里就是使用权重随机。

但是有个问题,我这里只需要这一个类,但是需要引入整个 spring-boot-jdbc-starter 包,有点拖泥带水的感觉。我研究了一下 AbstractRoutingDataSource 类的代码,发现它的实现非常简单,如果就仿照它自己实现了一个简单版的,这样就不需要引入整个包代码了。

public class RandomWeightedDataSource extends DataSourceAdapter {    private int totalWeight;    private Set
 sources;    private Map
 sourceMap;    public RandomWeightedDataSource(Map
 srcs) {        this.sources = new HashSet<>();        this.sourceMap = new HashMap<>();        for (Entry
 entry : srcs.entrySet()) {            // 权重值不宜过大            int weight = Math.min(10000, entry.getValue());            for (int i = 0; i < weight; i++) {                sourceMap.put(totalWeight, entry.getKey());                totalWeight++;            }            this.sources.add(entry.getKey());        }    }    private PooledDataSource getDataSource() {        return this.sourceMap.get(ThreadLocalRandom.current().nextInt(totalWeight));    }    public void close() {        for (PooledDataSource ds : sources) {            ds.forceCloseAll();        }    }    @Override    public Connection getConnection() throws SQLException {        return getDataSource().getConnection();    }    @Override    public Connection getConnection(String username, String password) throws SQLException {        return getDataSource().getConnection(username, password);    }}

git clone https://github.com/pyloque/shardino.git

docker-compose up -d

在本例中虽然用到了 springboot ,其实也只是用了它方便的依赖注入和单元测试功能,shardino 完全可以脱离 springboot 而独立存在。

shardino 并不是一个完美的开源库,它只是一份实现代码的样板,如果读者使用的是其它数据库或者 MySQL 的其它版本,那就需要自己微调一下代码来适配了。

640?wx_fmt=png

转载地址:http://cvbqb.baihongyu.com/

你可能感兴趣的文章
配置XDM--一种Linux的图形登录界面(转)
查看>>
计算机加锁 把U盘变成打开电脑的钥匙(转)
查看>>
C#开发的两个基本编程原则的深入讨论(转)
查看>>
Fedora Core 4 基础教程 (上传完毕)(转)
查看>>
删除MSSQL危险存储过程的代码(转)
查看>>
红旗软件:树立国际的Linux品牌(转)
查看>>
Linux学习要点(转)
查看>>
影响mysqld安全的几个选项(转)
查看>>
最新版本Linux Flash 9 Beta开放发布(转)
查看>>
mysql事务处理(转)
查看>>
Fedora 显示设备配置工具介绍(转)
查看>>
FREEBSD 升级及优化全攻略(转)
查看>>
系统移民须知:Linux操作系统安装要点(转)
查看>>
在redhat系统中使用LVM(转)
查看>>
Gentoo 2005.1 完整的USE参数清单中文详解(转)
查看>>
如何在嵌入式Linux产品中做立体、覆盖产品生命期的调试 (5)
查看>>
手机最新触控技术
查看>>
Kubuntu 项目遭遇困难(转)
查看>>
kubuntu使用日记之 eva的配置使用(转)
查看>>
unix下几个有用的小shell脚本(转)
查看>>