分库分表开源中间件之Sharding-JDBC使用体验

分库分表开源中间件之Sharding-JDBC使用体验

数据库分片思想

垂直切分

垂直切分就是把表按模块划分到不同数据库表中,单表大数据量依然存在性能瓶颈。

水平切分

水平切分就是把一个表按照某种规则(比如按用户Id取模)把数据划分到不同表和数据库里。

垂直切分更能清晰化模块划分,区分治理,水平切分能解决大数据量性能瓶颈问题,因此常常会把两者结合使用。

Sharding-JDBC简介

Sharding-JDBC是一个开源的适用于微服务的分布式数据访问的数据库水平切分框架。

Sharding-JDBC是当当应用框架ddframe中,从关系型数据库dd-rdb中分离出来的数据库水平切分框架,实现透明化数据库分库分表访问。Sharding-JDBC是继dubbox和elastic-job之后,ddframe系列开源的第三个项目。项目源码地址:https://github.com/shardingjdbc/sharding-jdbc

架构图如下:

《分库分表开源中间件之Sharding-JDBC使用体验》 架构图.png

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

  • 可适用于任何基于Java的ORM框架,如JPA、Hibernate、MyBatis、Spring JDBC Template或直接使用JDBC。
  • 可基于任何第三方的数据库连接池,如DBCP、C3P0、BoneCP、Druid等。
  • 理论上可支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。

Sharding-JDBC功能列表:

  • 分库分表
  • 读写分离
  • 柔性事务
  • 分布式主键
  • 分布式治理能力(2.0新功能)
    • 配置集中化与动态化,可支持数据源、表与分片策略的动态切换(2.0.0.M1)
    • 客户端的数据库治理,数据源失效自动切换(2.0.0.M2)
    • 基于Open Tracing协议的APM信息输出(2.0.0.M3)

Sharding-JDBC定位为轻量Java框架,使用客户端直连数据库,以jar包形式提供服务,无proxy代理层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。

Sharding-JDBC分片策略灵活,可支持等号、between、in等多维度分片,也可支持多分片键。

SQL解析功能完善,支持聚合、分组、排序、limit、or等查询,并支持Binding Table以及笛卡尔积表查询。

使用体验

下面的例子使用Spring Boot + Mybatis + Druid + Sharding-JDBC

项目结构

《分库分表开源中间件之Sharding-JDBC使用体验》 项目结构.png

  • Application是项目启动的入口。
  • DataSourceConfig是数据源配置,包括如何结合Sharding-JDBC设置分库分表。
  • algorithm下面是设置的分库分表策略,实现相关接口即可。
  • UserMapper是Mybatis的接口,采用了全注解配置,所以没有Mapper文件。
  • druid下面是druid的监控页面配置。

POM依赖[外加Spring Boot相关依赖]

<dependency>
    <groupId>com.dangdang</groupId>
    <artifactId>sharding-jdbc-core</artifactId>
    <version>1.5.4.1</version>
</dependency>

数据源配置

@Configuration
@ConfigurationProperties(prefix = DataSourceConstants.DATASOURCE_PREFIX)
@MapperScan(basePackages = { DataSourceConstants.MAPPER_PACKAGE }, sqlSessionFactoryRef = "mybatisSqlSessionFactory")
public class DataSourceConfig {

    private String url;

    private String username;

    private String password;

    @Bean(name = "mybatisDataSource")
    public DataSource getDataSource() throws SQLException {
        //设置分库映射
        Map<String, DataSource> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put("springboot_0", mybatisDataSource("springboot"));
        dataSourceMap.put("springboot_1", mybatisDataSource("springboot2"));

        //设置默认库,两个库以上时必须设置默认库。默认库的数据源名称必须是dataSourceMap的key之一
        DataSourceRule dataSourceRule = new DataSourceRule(dataSourceMap, "springboot_0");

        //设置分表映射
        TableRule userTableRule = TableRule.builder("user")
                .generateKeyColumn("user_id") //将user_id作为分布式主键
                .actualTables(Arrays.asList("user_0", "user_1"))
                .dataSourceRule(dataSourceRule)
                .build();

        //具体分库分表策略
        ShardingRule shardingRule = ShardingRule.builder()
                .dataSourceRule(dataSourceRule)
                .tableRules(Collections.singletonList(userTableRule))
                .databaseShardingStrategy(new DatabaseShardingStrategy("city_id", new ModuloDatabaseShardingAlgorithm()))
                .tableShardingStrategy(new TableShardingStrategy("user_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(DataSourceConstants.DRIVER_CLASS);
        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 = "mybatisTransactionManager")
    public DataSourceTransactionManager mybatisTransactionManager(@Qualifier("mybatisDataSource") DataSource dataSource) throws SQLException {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "mybatisSqlSessionFactory")
    public SqlSessionFactory mybatisSqlSessionFactory(@Qualifier("mybatisDataSource") DataSource mybatisDataSource)
            throws Exception {
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(mybatisDataSource);
        return sessionFactory.getObject();
    }

    // 省略setter、getter

}

如上,指定了两个数据库springboot和springboot2,对应的key分别是springboot_0和springboot_1,在具体执行数据库写入的时候会先根据分库算法【实现SingleKeyDatabaseShardingAlgorithm接口】确定写入到哪个库,再根据分表算法【实现SingleKeyTableShardingAlgorithm接口】最终确定写入到哪个表(user_0或user_1)。所以这两个数据库都有两个表,表结构如下:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户id',
  `city_id` int(11) DEFAULT NULL COMMENT '城市id',
  `user_name` varchar(15) DEFAULT NULL,
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `birth` date DEFAULT NULL COMMENT '生日',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

以上,按照city_id分库,user_id分表:

如果cityId mod 2 为0,则落在springboot_0,也就是springboot库;如果cityId mod 2为1,则落在springboot_1,也就是springboot2库。

如果userId mod 2为0,则落在user_0表;如果userId mod 2为1,则落在user_1表。

在设置分表映射的时候,我们将user_id作为分布式主键,但是却将id作为了自增主键。因为在同一个逻辑表(user表)内的不同实际表(user_0和user_1)之间的自增键是无法互相感知的,这样会造成重复I
d的生成。而Sharding-JDBC的分布式主键保证了数据库进行分库分表后主键(userId)一定是唯一不重复的,这样就解决了生成重复Id的问题。

测试

如果插入下面这条数据,因为cityId模2余1,所以肯定落在springboot2库,但是无法实现确定落在哪个表,因为我们将user_id作为了分布式主键,主键由Sharding-JDBC内部生成,所以可能会落在user_0或user_1。

@Test
public void getOneSlave() throws Exception {
    UserEntity user = new UserEntity();
    user.setCityId(1);//1 mod 2 = 1,所以会落在springboot2库中
    user.setUserName("insertTest");
    user.setAge(10);
    user.setBirth(new Date());
    assertTrue(userMapper.insertSlave(user) > 0);
    Long userId = user.getUserId();
    System.out.println("Generated Key--userId:" + userId + "mod:" + 1 % 2);
    UserEntity one = userMapper.getOne(userId);
    System.out.println("Generated User:" + one);
    assertEquals("insertTest", one.getUserName());
}

表数据如下:

《分库分表开源中间件之Sharding-JDBC使用体验》 表数据.png

总结:分表规则的一些思考

  • 根据用户Id进行分配

    这种方式能够确保同一个用户的所有数据在同一个数据表中。如果经常按用户Id查询数据,推荐用这种方法。

  • 根据主键进行分配

    这种方式能够实现最平均的分配方法,每生成一条新数据,会依次保存到下一个数据表中。

  • 根据时间进行分配

    适用于一些经常按时间段进行查询的数据,将一个时间段内的数据保存在同一个数据表中。

欢迎微信扫码关注微信公众号:后端开发者中心,不定期推送服务端各类技术文章。

《分库分表开源中间件之Sharding-JDBC使用体验》

    原文作者:后端开发者中心
    原文地址: https://www.jianshu.com/p/2ca99c4e70c2
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞