Sharding-JDBC的實(shí)踐

基本概念

這幾天在研究分表分庫(kù)的方案。綜合了幾種數(shù)據(jù)庫(kù)方案。
在這里插入圖片描述
最后選型Sharding-jdbc。它主要有如下幾個(gè)優(yōu)點(diǎn)。

  1. 支持分布式事務(wù)
  2. 適用于任何基于Java的ORM框架。
  3. 對(duì)業(yè)務(wù)零侵入。

數(shù)據(jù)分片

數(shù)據(jù)分片是指按照某個(gè)維度將存放在單一數(shù)據(jù)庫(kù)中的數(shù)據(jù)分散地存放至多個(gè)數(shù)據(jù)庫(kù)或者表中以達(dá)到提升性能瓶頸以及可用性的效果。數(shù)據(jù)分片有效手段是對(duì)關(guān)系型數(shù)據(jù)庫(kù)進(jìn)行分庫(kù)和分表。分表可以降低每個(gè)單表的數(shù)據(jù)閾值,同時(shí)還能夠?qū)⒎植际绞聞?wù)轉(zhuǎn)化為本地事務(wù)的。分庫(kù)可以有效的分散數(shù)據(jù)庫(kù)單點(diǎn)的訪問(wèn)量。

分片鍵

用于分片的數(shù)據(jù)庫(kù)字段,是將數(shù)據(jù)庫(kù)(表)進(jìn)行水平拆分的關(guān)鍵字段。例如:將訂單表中的訂單主鍵的尾數(shù)取模分批拿,則訂單主鍵就是分片字段,SQL中如果沒(méi)有分片字段,則執(zhí)行全庫(kù)路由,性能較差。Sharding-JDBC也支持多個(gè)字段進(jìn)行分片。

分片策略和分片算法

Sharding-JDBC 中共有五種分片策略。1、標(biāo)準(zhǔn)分片策略;2、復(fù)合分片策略;3、行表達(dá)式分片策略;4、Hint分片策略;5、不分片策略;對(duì)應(yīng)的有4種分片算法,1、精確分片算法;2、范圍分片算法;3、復(fù)合分片算法 ;4、Hint分片算法;
分片算法:
Sharding-JDBC并沒(méi)有提供內(nèi)置分片算法,而是通過(guò)分片策略將各種場(chǎng)景提煉出來(lái),提供更高層次的抽象,并提供接口讓?xiě)?yīng)用開(kāi)發(fā)者自行實(shí)現(xiàn)分片算法。
分片策略:
包含分片鍵和分片算法,由于分片算法的獨(dú)立性,將其獨(dú)立抽離。真正可用于分片操作的是分片鍵+分片算法,也就是分片策略。





在這里插入圖片描述

























Sharding-JDBC與SpringBoot整合策略

總體說(shuō)明

本實(shí)例是結(jié)合相關(guān)項(xiàng)目來(lái)的,在該項(xiàng)目中訂單id(orders_id)是一個(gè)核心的熱點(diǎn)字段。然后,訂單號(hào)是帶有日期的,所以,本次分片的方案是按照時(shí)間分庫(kù)分表,時(shí)間粒度可以年,月,季度。本demo中的時(shí)間粒度是年。

引入依賴

    <properties>
     <sharding-sphere.version>4.0.0-RC1</sharding-sphere.version>
    </properties>
     <!--shardingsphere -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-core-api</artifactId>
            <version>${sharding-sphere.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-core</artifactId>
            <version>${sharding-sphere.version}</version>
        </dependency>

配置好依賴之后,接著就是配置數(shù)據(jù)源,以及數(shù)據(jù)的分片鍵,分片策略。在application.yml中配置如下

配置

#數(shù)據(jù)庫(kù)連接
spring:
  shardingsphere:
    datasource:
      names: shard_order_0,shard_order_1
      shard_order_0:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/shard_order_0
          username: root
          password: admin
      shard_order_1:
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/shard_order_1
          username: root
          password: admin
# 如下對(duì)orders表和orders_detail都做了分片配置,分片鍵分別是id,orders_id,
    sharding:
      tables:
        orders:
          actualDataNodes: shard_order_$->{0..1}.orders_$->{0..1}
          databaseStrategy:
            inline:
              shardingColumn:  adddate
          tableStrategy:
              inline:
                shardingColumn: id
        orders_detail:
          actualDataNodes: shard_order_$->{0..1}.orders_detail_$->{0..1}
          tableStrategy:
            inline:
              shardingColumn: orders_id     

如上可以看出,我這里配置了兩個(gè)分庫(kù)shard_order_0和shard_order_1;然后,每個(gè)分庫(kù)下面又配置了兩個(gè)邏輯表orders和orders_detail,每個(gè)邏輯表下有兩個(gè)物理表。數(shù)據(jù)庫(kù)的分片鍵是adddate,邏輯表orders的分片鍵是id,邏輯表orders_detail的分片鍵是orders_id。
完成配置之后接著就是要定義數(shù)據(jù)庫(kù)的分片策略和分片的策略以及初始化DataSource。因?yàn)楸敬卧谥鲙?kù)中加了一個(gè)路由表,在路由時(shí)動(dòng)態(tài)查取該分片值所需要查找的分庫(kù)分表,所以,需要再多配置一個(gè)數(shù)據(jù)源。用于執(zhí)行分庫(kù)的算法時(shí)可以查詢路由表,項(xiàng)目結(jié)構(gòu)如圖所示:
在這里插入圖片描述

分庫(kù)的數(shù)據(jù)源配置

//*** DataSourceConfig
    /**
     * 設(shè)置數(shù)據(jù)源
     * @return
     * @throws SQLException
     */
    @Bean(name = "shardingDataSource")
    DataSource getShardingDataSource() throws SQLException {
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getBindingTableGroups().add(ordersLogicTable);
        shardingRuleConfig.getBindingTableGroups().add(ordersDetailLogicTable);
//       配置Orders表規(guī)則
       shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration());
        //配置ordersItem表規(guī)則
        shardingRuleConfig.getTableRuleConfigs().add(getOrderDetailTableRuleConfiguration());
        shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(databaseShardingColumn, preciseModuloDatabaseShardingAlgorithm));
        shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(ordersShardingColumn,preciseModuloTableShardingAlgorithm));

        //設(shè)置默認(rèn)數(shù)據(jù)庫(kù)
        shardingRuleConfig.setDefaultDataSourceName(defaultDataSource);

        return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new Properties());
    }


    /**
     * 獲取sqlSessionFactory實(shí)例
     * @param shardingDataSource
     * @return
     * @throws Exception
     */
    @Bean
    @Primary
    public SqlSessionFactory sqlSessionFactory(DataSource shardingDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(shardingDataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        return bean.getObject();
    }
	
	    TableRuleConfiguration getOrderTableRuleConfiguration() {
        TableRuleConfiguration orderTableRuleConfig=new TableRuleConfiguration(ordersLogicTable, ordersActualDataNodes);
        orderTableRuleConfig.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(databaseShardingColumn, preciseModuloDatabaseShardingAlgorithm));
        orderTableRuleConfig.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(ordersShardingColumn,preciseModuloTableShardingAlgorithm));
        return orderTableRuleConfig;
    }
    TableRuleConfiguration getOrderDetailTableRuleConfiguration() {
        TableRuleConfiguration orderDetailTableRuleConfig=new TableRuleConfiguration(ordersDetailLogicTable, ordersDetailActualDataNodes);
        orderDetailTableRuleConfig.setDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration(databaseShardingColumn, preciseModuloDatabaseShardingAlgorithm));
        orderDetailTableRuleConfig.setTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(ordersDetailShardingColumn, orderDetailTableShardingAlgorithm));
        return orderDetailTableRuleConfig;
    }

如上,可以看出orders和orders_detail 兩個(gè)邏輯表都采用的標(biāo)準(zhǔn)分片策略,使用的是精確分片算法。分庫(kù)的策略也是標(biāo)準(zhǔn)分片策略,使用的是精確分片算法。

數(shù)據(jù)庫(kù)的分庫(kù)策略

@Data
@Slf4j
@Service("preciseModuloDatabaseShardingAlgorithm")
public class DatabaseShardingAlgorithm implements PreciseShardingAlgorithm<Timestamp>{

    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//定義格式,不顯示毫秒
    @Autowired
    private ShardConfigMapper shardConfigMapper;

    /**
     * 
     * @param collection
     * @param preciseShardingValue
     * @return
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Timestamp> preciseShardingValue) {
        String physicDatabase = null;
        Timestamp valueTime = preciseShardingValue.getValue();
        String orgValue = df.format(valueTime);
        String subValue = orgValue.substring(0, 4).replace("-", "");
        physicDatabase = getShardConfig(physicDatabase, subValue);
        if (StringUtils.isBlank(physicDatabase)) {
            log.info("----->該分片鍵值找不到對(duì)應(yīng)的分庫(kù),默認(rèn)取第一個(gè)庫(kù),分片鍵是={},邏輯表是={},分片值是={}",preciseShardingValue.getColumnName(),preciseShardingValue.getLogicTableName(),preciseShardingValue.getValue());
            for (String value : collection) {
                physicDatabase = value;
                break;
            }
        }
        return physicDatabase;
    }


    public String getShardConfig(String physicDatabase ,String subValue ) {
        ShardConfig shardConfig = shardConfigMapper.selectByPrimaryKey(subValue);
        if (shardConfig != null) {
            physicDatabase = shardConfig.getConfigValue().split(",")[0];
        }
        return physicDatabase;

    }
}

如上分片鍵是adddate,當(dāng)SQL中含有adddate字段時(shí)會(huì)執(zhí)行分片策略。如果SQL中adddate 字段是BETWEEN AND 需要執(zhí)行復(fù)合分片算法。否則會(huì)全庫(kù)查詢。因?yàn)槭前凑漳攴輥?lái)分庫(kù)的,所以先截取當(dāng)前的年份,然后去路由表中查。對(duì)應(yīng)的分庫(kù)id。分表策略也是類似的。

路由表

接著我們來(lái)看看路由表,路由表是該分庫(kù)分表方案中的核心表。表結(jié)構(gòu)如下:
在這里插入圖片描述




表的數(shù)據(jù)存儲(chǔ)如下:
在這里插入圖片描述




如上圖所示,路由表中按照年份存放了,每個(gè)年份所對(duì)應(yīng)的分庫(kù)id,和分表id。所以當(dāng)有分片鍵,進(jìn)入分表策略時(shí)就可以根據(jù)年份找到對(duì)應(yīng)的分庫(kù),分表。

多數(shù)據(jù)源配置

由于路由表是公共表,不參與分片,所以只在主庫(kù)中存儲(chǔ)了一份。當(dāng)進(jìn)入數(shù)據(jù)庫(kù)的分庫(kù)策略時(shí)就要查詢路由表。如果不配置多數(shù)據(jù)源的話,此時(shí)路由表對(duì)應(yīng)的shardConfigMapper為null。不能進(jìn)行查詢。過(guò)需要配置多數(shù)據(jù)源。

@MapperScan(basePackages = "com.jay.mapper.nosharding", sqlSessionTemplateRef = "noshardingSqlSessionTemplate")
public class NoShardingDataSourceConfig {

    @Value("${spring.shardingsphere.datasource.shard_order_0.username}")
    private String username0;
    @Value("${spring.shardingsphere.datasource.shard_order_0.url}")
    private String url0;
    @Value("${spring.shardingsphere.datasource.shard_order_0.password}")
    private String password0;

    @Bean(name = "noshardingDataSource")
    public DataSource testDataSource() {
        BasicDataSource result = new BasicDataSource();
        result.setDriverClassName(com.mysql.jdbc.Driver.class.getName());
        result.setUrl(url0);
        result.setUsername(username0);
        result.setPassword(password0);
        return result;
    }

    /**
     * 獲取sqlSessionFactory實(shí)例
     *
     * @param shardingDataSource
     * @return
     * @throws Exception
     */
    @Bean(name = "noshardingSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("noshardingDataSource") DataSource shardingDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(shardingDataSource);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/nosharding/*.xml"));
        return bean.getObject();
    }

總結(jié)

本文首先介紹了分庫(kù)分表的相關(guān)概念,然后,對(duì)比了幾種主流的分庫(kù)分表中間件。接著重點(diǎn)介紹了分片策略和相關(guān)的算法。最后通過(guò)一個(gè)demo,實(shí)現(xiàn)了對(duì)Sharding-JDBC 數(shù)據(jù)分片的落地。

參考資料

Sharding-JDBC官方文檔

源碼地址

https://github.com/XWxiaowei/springboot-shardingsphere-example.git





作者:碼農(nóng)飛哥
微信公眾號(hào):碼農(nóng)飛哥