美文网首页
shadring-jdbc解决查询数据库分库分表的问题

shadring-jdbc解决查询数据库分库分表的问题

作者: 十五倍压枪 | 来源:发表于2019-02-27 17:52 被阅读0次

    问题背景

    业务组在最开始的数据库规划中,通过对未来数据规模的预估后,一开始就进行了分库分表的设计。简单的说,一个业务分为16个库,其中每个库有64张业务表…过去经常使用的和数据库交互的经验就完全派不上用场了,难道真的丑陋的在代码中组装16个连接然后每个语句都去拼接吗?对不起做不到。
    将自己的需求梳理清楚后可以看清楚,我需要的是一个不涉及原数据库的改动(不通过增加中间件),可以简单封装我的查询请求发送到所有分库分表中,让代码简洁好看(虽然实际上可能真的要做n * 16个数据库连接)直接一次能查询 16个库 * 64张表 的一个依赖,通过谷歌后,发现了一个叫做sharding-jdbc的开源完全贴合我的需求。

    工具介绍

    首先放上github的链接sharding-jdbc
    这边抄袭一段介绍来凑字数:

    Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:
    可适用于任何基于java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
    可基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid等。
    理论上可支持任意实现JDBC规范的数据库。虽然目前仅支持MySQL,但已有支持Oracle,SQLServer,DB2等数据库的计划

    事实上给我的使用体验还不错,当然也是因为我的查询语句是最简单的普通查询语句,并没有涉及到太复杂的操作。翻阅issues的时候还是能看到这种第三方的依赖对于一些复杂查询的支持不足。但现在已经完全满足我的需求了。

    操作步骤

    正常的一个ORM框架查询数据库的应用是比较固定和规范的,由DAO层来负责数据库的沟通,本次我的应用简单的使用了Mybatis来做和Mysql的沟通。DAO层代码简单如下

    public interface MtxxTagMapper {
    
        @Select("select tag_id,tag_name,uid,`desc`,status from tb_tag where tag_id = #{tag_id}")
        public Map<String, Object> findTagByTagId(@Param("tag_id") String tag_id);
    }
    
    

    是的就是这么简单操作。但是最大的麻烦就在于,并没有tb_tab表,或者人人都是tb_tag表。根据tag_id进行hash过后,tb_tag表被横向分布在16个库和64张表中,这条语句其实是无法执行的。所以重点不在DAO层而在于配置层,这也应了上面的介绍的话

    Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零

    确实在查询层几乎不需要怎么动,不过复杂应用另说吧。
    重点其实在于,数据源的配置,这里我使用了javaconfig的方式来配置,代码如下

    
    @Data
    @Configuration
    @ConfigurationProperties(prefix = "spring.datasource.feed")
    @MapperScan(basePackages = "com.meitu.dump.mapper.mtxx.tag", sqlSessionFactoryRef = "mtxxTagSqlSessionFactory")
    public class MtxxTagDataSourceConfig {
    
        private String url;
    
        private String username;
    
        private String password;
    
        @Bean(name = "mtxxTagDataSource")
        public DataSource getDataSource() throws SQLException {
            //设置分库映射
            Map<String, DataSource> dataSourceMap = new HashMap<>(16);
            dataSourceMap.put("xiuxiu_feed_0", mybatisDataSource("xiuxiu_feed_0"));
            dataSourceMap.put("xiuxiu_feed_1", mybatisDataSource("xiuxiu_feed_1"));
            dataSourceMap.put("xiuxiu_feed_2", mybatisDataSource("xiuxiu_feed_2"));
            dataSourceMap.put("xiuxiu_feed_3", mybatisDataSource("xiuxiu_feed_3"));
            dataSourceMap.put("xiuxiu_feed_4", mybatisDataSource("xiuxiu_feed_4"));
            dataSourceMap.put("xiuxiu_feed_5", mybatisDataSource("xiuxiu_feed_5"));
            dataSourceMap.put("xiuxiu_feed_6", mybatisDataSource("xiuxiu_feed_6"));
            dataSourceMap.put("xiuxiu_feed_7", mybatisDataSource("xiuxiu_feed_7"));
            dataSourceMap.put("xiuxiu_feed_8", mybatisDataSource("xiuxiu_feed_8"));
            dataSourceMap.put("xiuxiu_feed_9", mybatisDataSource("xiuxiu_feed_9"));
            dataSourceMap.put("xiuxiu_feed_10", mybatisDataSource("xiuxiu_feed_10"));
            dataSourceMap.put("xiuxiu_feed_11", mybatisDataSource("xiuxiu_feed_11"));
            dataSourceMap.put("xiuxiu_feed_12", mybatisDataSource("xiuxiu_feed_12"));
            dataSourceMap.put("xiuxiu_feed_13", mybatisDataSource("xiuxiu_feed_13"));
            dataSourceMap.put("xiuxiu_feed_14", mybatisDataSource("xiuxiu_feed_14"));
            dataSourceMap.put("xiuxiu_feed_15", mybatisDataSource("xiuxiu_feed_15"));
    
            List<String> actualTables = new ArrayList<>();
            for(int i = 0; i < XiuXiuTableItemHelper.tableNums; i++){
                actualTables.add(String.format("tb_tag_%s", Integer.toString(i)));
            }
    
            //设置默认库,两个库以上时必须设置默认库。默认库的数据源名称必须是dataSourceMap的key之一
            DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, "xiuxiu_feed_0");
    
            //设置分表映射
            TableRule userTableRule = TableRule.builder("tb_tag")
                    .generateKeyColumn("tag_id") //将user_id作为分布式主键
                    .actualTables(actualTables)
                    .dataSourceRule(dataSourceRule)
                    .build();
    
            //具体分库分表策略
            ShardingRule shardingRule = ShardingRule.builder()
                    .dataSourceRule(dataSourceRule)
                    .tableRules(Collections.singletonList(userTableRule))
                    .databaseShardingStrategy(new DatabaseShardingStrategy("tag_id", new ModuloDatabaseShardingAlgorithm()))
                    .tableShardingStrategy(new TableShardingStrategy("tag_id", new ModuloTableShardingAlgorithm()))
                    .build();
    
            DataSource dataSource = ShardingDataSourceFactory.createDataSource(shardingRule);
    
            //return new ShardingDataSource(shardingRule);
            return dataSource;
        }
    
        private DataSource mybatisDataSource(final String dataSourceName) throws SQLException {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName("com.mysql.jdbc.Driver");
            dataSource.setUrl(String.format(url, dataSourceName));
            dataSource.setUsername(username);
            dataSource.setPassword(password);
    
            /* 配置初始化大小、最小、最大 */
            dataSource.setInitialSize(1);
            dataSource.setMinIdle(1);
            dataSource.setMaxActive(20);
    
            /* 配置获取连接等待超时的时间 */
            dataSource.setMaxWait(60000);
    
            /* 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
            dataSource.setTimeBetweenEvictionRunsMillis(60000);
    
            /* 配置一个连接在池中最小生存的时间,单位是毫秒 */
            dataSource.setMinEvictableIdleTimeMillis(300000);
    
            dataSource.setValidationQuery("SELECT 'x'");
            dataSource.setTestWhileIdle(true);
            dataSource.setTestOnBorrow(false);
            dataSource.setTestOnReturn(false);
    
            /* 打开PSCache,并且指定每个连接上PSCache的大小。
               如果用Oracle,则把poolPreparedStatements配置为true,
               mysql可以配置为false。分库分表较多的数据库,建议配置为false */
            dataSource.setPoolPreparedStatements(false);
            dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
    
            /* 配置监控统计拦截的filters */
    //        dataSource.setFilters("stat,wall,log4j");
            return dataSource;
        }
    
        /**
         * Sharding-jdbc的事务支持
         *
         * @return
         */
        @Bean(name = "mtxxTagTransactionManager")
        public DataSourceTransactionManager mybatisTransactionManager(@Qualifier("mtxxTagDataSource") DataSource dataSource) throws SQLException {
            return new DataSourceTransactionManager(dataSource);
        }
    
        @Bean(name = "mtxxTagSqlSessionFactory")
        public SqlSessionFactory mybatisSqlSessionFactory(@Qualifier("mtxxTagDataSource") DataSource mybatisDataSource)
                throws Exception {
            final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
            sessionFactory.setDataSource(mybatisDataSource);
            return sessionFactory.getObject();
        }
    }
    
    

    在这里我确实配置了16个数据库的链接,不过是通过工厂方式生成的。对于shadring-jdbc来说,他引申出的概念叫做逻辑库逻辑表。在配置中描述逻辑库和逻辑表的生成规则,然后以后用查询逻辑库与逻辑表来代替。我们看看是在哪里描述这个逻辑

            //设置分表映射
            TableRule userTableRule = TableRule.builder("tb_tag")
                    .generateKeyColumn("tag_id") //将user_id作为分布式主键
                    .actualTables(actualTables)
                    .dataSourceRule(dataSourceRule)
                    .build();
    
           //具体分库分表策略
            ShardingRule shardingRule = ShardingRule.builder()
                    .dataSourceRule(dataSourceRule)
                    .tableRules(Collections.singletonList(userTableRule))
                    .databaseShardingStrategy(new DatabaseShardingStrategy("tag_id", new ModuloDatabaseShardingAlgorithm()))
                    .tableShardingStrategy(new TableShardingStrategy("tag_id", new ModuloTableShardingAlgorithm()))
                    .build();
    

    分表映射规则采用了链式生产对象的方式来描述,generateKeyColumn指定了虚拟表的主键,actualTables用来存储所有真实表的名字。
    分库映射规则除了包含分表规则以外,更重要的是指定了如何计算分库和分表尾缀的逻辑。方法databaseShardingStrategy(new DatabaseShardingStrategy("tag_id", new ModuloDatabaseShardingAlgorithm()))指定了分库的逻辑类,方法.tableShardingStrategy(new TableShardingStrategy("tag_id", new ModuloTableShardingAlgorithm()))指定了分表的逻辑类。而这两个类的逻辑由我们自己继承接口实现而得。

    再看分库实现类

    @Slf4j
    public class ModuloDatabaseShardingAlgorithm implements SingleKeyDatabaseShardingAlgorithm<String> {
    
        @Override
        public String doEqualSharding(Collection collection, ShardingValue shardingValue) {
            int databaseNum = XiuXiuTableItemHelper.getDatabase(shardingValue.getValue().toString());
            for (Object database : collection) {
                if (database.toString().endsWith("_" + Integer.toString(databaseNum))) {
                    return database.toString();
                }
            }
            return null;
        }
    
        @Override
        public Collection<String> doInSharding(Collection collection, ShardingValue shardingValue) {
            return null;
        }
    
        @Override
        public Collection<String> doBetweenSharding(Collection collection, ShardingValue shardingValue) {
            return null;
        }
    }
    
    

    类只要实现了SingleKeyDatabaseShardingAlgorithm即可作为逻辑类使用,3个实现方法分别是对应sql的一些语句使用,目前我只实现了doEqualSharding方法,我的语句where xx =就会执行这个方法来获得具体库名,在逻辑更复杂的情况下旧需要考虑另外2个方法的实现。
    public String doEqualSharding(Collection collection, ShardingValue shardingValue)方法中,collection是所有真实存在的库名,也就是刚刚配置代码中我们注册进去的16个库名的集合,而shardingValue指的是我们设置用来分片的字段,generateKeyColumn("tag_id")。根据业务,我需要通过对tag_id进行hash后得到在[0-15]中对应的数据做为库名尾缀,所以我的逻辑就变成这样:

    1. 循环所有真实库名
    2. 真实库名是否包含 "_n"(n为计算分库算法得到的尾缀)
    3. 返回真实库名
      到此就完成了一个简单的分库查询

    相关文章

      网友评论

          本文标题:shadring-jdbc解决查询数据库分库分表的问题

          本文链接:https://www.haomeiwen.com/subject/qiwfuqtx.html