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

2019-02-27  本文已影响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. 返回真实库名
    到此就完成了一个简单的分库查询
上一篇下一篇

猜你喜欢

热点阅读