SpringBoot的4种数据水平分片策略

一、前言

数据水平分片作为一种水平扩展策略,通过将数据分散到多个物理节点上,有效解决了存储容量和性能瓶颈问题。

而分片键(Sharding Key)作为数据分片的核心,决定了数据如何在各个分片中分布,直接影响到分片系统的性能、数据分布均衡性以及查询效率。

本文将分享4种数据分片策略。

二、哈希分片

2.1 原理

哈希分片通过对分片键值应用哈希函数,然后对分片数量取模,将数据均匀分布到各个分片。

scss 复制代码
分片索引 = hash(分片键值) % 分片数量

2.2 SpringBoot实现

在SpringBoot中,我们可以使用ShardingSphere-JDBC实现哈希分片。首先添加依赖:

xml 复制代码
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>5.1.2</version>
</dependency>

然后在application.yml中配置哈希分片策略:

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1,ds2,ds3
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/order_db_0
        username: root
        password: password
      # 其他数据源配置...
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..3}.t_order
            database-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: order-id-hash
        sharding-algorithms:
          order-id-hash:
            type: HASH_MOD
            props:
              sharding-count: 4

在实体类和Repository中使用:

kotlin 复制代码
@Entity
@Table(name = "t_order")
public class Order {
    @Id
    private Long orderId;  // 分片键
    private Long userId;
    private BigDecimal amount;
    private String status;
    // getters and setters
}

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    // 查询方法
}

2.3 优缺点分析

优点:

  1. 数据分布均匀:哈希函数确保数据在各分片上分布均衡
  2. 读写负载均衡:避免热点分片问题
  3. 简单易实现:配置简单,无需复杂的分片算法

缺点:

  1. 不支持范围查询:相邻的键值会被分散到不同分片,导致范围查询需要访问所有分片
  2. 分片扩缩容困难:增加或减少分片数量时,需要重新哈希和迁移大量数据
  3. 不易直观理解:数据分布不直观,难以预测特定记录在哪个分片

2.4 适用场景

  • 读写操作频繁且均衡的OLTP系统
  • 点查询(如通过主键或唯一索引查询)为主的应用
  • 对数据分布均匀性要求高的系统
  • 分片数量相对稳定的环境

三、范围分片

3.1 原理

范围分片键通过将数据按照分片键的值范围划分到不同的分片中。每个分片负责存储特定范围内的数据,数据在逻辑上保持有序。

markdown 复制代码
如:订单ID 1-1000000 存储在分片1
    订单ID 1000001-2000000 存储在分片2
    ...

3.2 SpringBoot实现

使用ShardingSphere-JDBC实现范围分片:

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1,ds2,ds3
      # 数据源配置...
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..3}.t_order
            database-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: order-id-range
        sharding-algorithms:
          order-id-range:
            type: RANGE
            props:
              strategy: standard
              range-lower: 0,1000000,2000000,3000000
              range-upper: 999999,1999999,2999999,3999999

自定义更复杂的范围分片算法:

kotlin 复制代码
@Component
public class CustomRangeShardingAlgorithm implements StandardShardingAlgorithm<Long> {
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
        Long orderId = shardingValue.getValue();
        for (String targetName : availableTargetNames) {
            if (targetName.endsWith("0") && orderId <= 1000000) {
                return targetName;
            } else if (targetName.endsWith("1") && orderId > 1000000 && orderId <= 2000000) {
                return targetName;
            } else if (targetName.endsWith("2") && orderId > 2000000 && orderId <= 3000000) {
                return targetName;
            } else if (targetName.endsWith("3") && orderId > 3000000) {
                return targetName;
            }
        }
        throw new UnsupportedOperationException("No available target name for order id: " + orderId);
    }

    // 其他必要的方法实现...
}

3.3 优缺点分析

优点:

  1. 支持范围查询:同一范围的数据存储在同一分片,范围查询效率高
  2. 数据局部性好:相关数据聚集在一起,提高查询效率
  3. 易于理解和维护:分片规则直观,数据分布清晰

缺点:

  1. 数据倾斜风险:如果数据分布不均,可能导致某些分片负载过重
  2. 热点分片问题:新数据往往落在最新范围的分片上,造成访问热点
  3. 分片边界固定:预先定义的范围边界难以动态调整

3.4 适用场景

  • 时间序列数据或自增ID数据的存储
  • 范围查询频繁的应用
  • 历史数据访问频率低但需要保留的系统
  • 可以预测数据增长模式的业务

四、复合分片

4.1 原理

复合分片键使用多个字段的组合作为分片依据,提供更精细和灵活的分片控制。可以同时考虑多个业务维度,使分片更贴合业务特性。

常见的复合分片策略包括:

  • 多字段哈希组合
  • 一级字段范围+二级字段哈希
  • 字段组合后再应用分片算法

4.2 SpringBoot实现

使用ShardingSphere-JDBC实现复合分片:

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1,ds2,ds3
      # 数据源配置...
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..3}.t_order
            database-strategy:
              complex:
                sharding-columns: user_id,order_date
                sharding-algorithm-name: complex-algorithm
        sharding-algorithms:
          complex-algorithm:
            type: CLASS_BASED
            props:
              strategy: COMPLEX
              algorithmClassName: com.example.CustomComplexShardingAlgorithm

自定义复合分片算法:

dart 复制代码
@Component
public class CustomComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
    
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, 
                                        ComplexKeysShardingValue<Comparable<?>> shardingValue) {
        Map<String, Collection<Comparable<?>>> columnNameAndShardingValuesMap = 
            shardingValue.getColumnNameAndShardingValuesMap();
        
        Collection<Comparable<?>> userIds = columnNameAndShardingValuesMap.get("user_id");
        Collection<Comparable<?>> orderDates = columnNameAndShardingValuesMap.get("order_date");
        
        List<String> result = new ArrayList<>();
        
        for (Comparable<?> userId : userIds) {
            for (Comparable<?> orderDate : orderDates) {
                // 使用用户ID和订单日期的组合确定分片
                LocalDate date = (LocalDate) orderDate;
                long hash = (long) userId * 31 + date.getYear() * 12 + date.getMonthValue();
                int shardingKey = (int) (hash % 4);  // 分4个片
                
                for (String targetName : availableTargetNames) {
                    if (targetName.endsWith(String.valueOf(shardingKey))) {
                        result.add(targetName);
                    }
                }
            }
        }
        
        return result;
    }
}

实体类定义:

kotlin 复制代码
@Entity
@Table(name = "t_order")
public class Order {
    @Id
    private Long orderId;
    private Long userId;  // 复合分片键之一
    private LocalDate orderDate;  // 复合分片键之一
    private BigDecimal amount;
    private String status;
    // getters and setters
}

4.3 优缺点分析

优点:

  1. 分片更精细:可以结合多个业务维度进行数据分布
  2. 数据分布更均衡:减少单维度分片可能带来的数据倾斜
  3. 查询灵活性:支持多种条件的查询优化
  4. 更贴合业务:可以根据实际业务特性定制分片策略

缺点:

  1. 实现复杂:需要自定义复杂的分片算法
  2. 维护困难:分片逻辑复杂,难以理解和维护
  3. 查询成本高:如果查询条件不包含所有分片键,可能需要查询多个分片
  4. 测试难度大:需要全面测试各种查询场景

4.4 适用场景

  • 单一分片键无法满足均衡性要求的系统
  • 多维度查询频繁的应用
  • 数据有明显多维度特性的业务
  • 需要精细化控制数据分布的场景

五、时间序列分片

5.1 原理

时间序列分片键使用时间相关字段(如创建时间、交易日期)作为分片依据,通常将特定时间段的数据存储在同一分片中。这种方式特别适合具有明显时间属性的数据,如日志、订单、交易记录等。

常见的时间分片策略包括:

  • 按年/月/周/日分片
  • 时间窗口滚动分片
  • 时间+其他字段组合分片

5.2 SpringBoot实现

使用ShardingSphere-JDBC实现时间序列分片:

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1,ds2,ds3
      # 数据源配置...
    rules:
      sharding:
        tables:
          t_order:
            actual-data-nodes: ds${0..3}.t_order
            database-strategy:
              standard:
                sharding-column: create_time
                sharding-algorithm-name: time-sharding
        sharding-algorithms:
          time-sharding:
            type: INTERVAL
            props:
              datetime-pattern: yyyy-MM-dd HH:mm:ss
              datetime-lower: 2023-01-01 00:00:00
              datetime-upper: 2023-12-31 23:59:59
              sharding-suffix-pattern: yyyyMM
              datetime-interval-amount: 3
              datetime-interval-unit: MONTHS

自定义更复杂的时间分片算法:

ini 复制代码
@Component
public class QuarterlyShardingAlgorithm implements StandardShardingAlgorithm<Date> {
    
    private static final Map<Integer, Integer> QUARTER_MAP = new HashMap<>();
    
    static {
        // 将月份映射到季度
        QUARTER_MAP.put(1, 1);
        QUARTER_MAP.put(2, 1);
        QUARTER_MAP.put(3, 1);
        QUARTER_MAP.put(4, 2);
        QUARTER_MAP.put(5, 2);
        QUARTER_MAP.put(6, 2);
        QUARTER_MAP.put(7, 3);
        QUARTER_MAP.put(8, 3);
        QUARTER_MAP.put(9, 3);
        QUARTER_MAP.put(10, 4);
        QUARTER_MAP.put(11, 4);
        QUARTER_MAP.put(12, 4);
    }
    
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Date> shardingValue) {
        Date date = shardingValue.getValue();
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(date);
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH) + 1;
        int quarter = QUARTER_MAP.get(month);
        
        // 将分片策略设计为按季度循环到不同数据源
        // 例如:2023Q1->ds0, 2023Q2->ds1, 2023Q3->ds2, 2023Q4->ds3, 2024Q1->ds0, ...
        int shardIndex = ((year - 2023) * 4 + quarter - 1) % 4;
        
        for (String targetName : availableTargetNames) {
            if (targetName.endsWith(String.valueOf(shardIndex))) {
                return targetName;
            }
        }
        
        throw new UnsupportedOperationException("No available target name for date: " + date);
    }
    
    // 其他必要的方法实现...
}

实体类定义:

kotlin 复制代码
@Entity
@Table(name = "t_order")
public class Order {
    @Id
    private Long orderId;
    private Long userId;
    @Temporal(TemporalType.TIMESTAMP)
    private Date createTime;  // 时间分片键
    private BigDecimal amount;
    private String status;
    // getters and setters
}

5.3 优缺点分析

优点:

  1. 数据生命周期管理:便于实现数据的生命周期管理和归档
  2. 高效的时间范围查询:同一时间段的数据位于同一分片,时间范围查询高效
  3. 分片扩展自然:随着时间推移自然扩展到新分片
  4. 冷热数据分离:可以针对不同时间段的数据采用不同的存储策略

缺点:

  1. 数据分布不均:如果时间分布不均匀,可能导致分片数据量差异大
  2. 写入热点问题:当前时间段的分片成为写入热点
  3. 历史分片访问少:旧分片利用率低但仍占用资源
  4. 跨时间段查询复杂:大范围的时间查询可能需要访问多个分片

5.4 适用场景

  • 具有明显时间属性和时间局部性的数据
  • 日志、订单、交易等时间序列数据
  • 需要定期归档历史数据的系统
  • 查询通常限定在特定时间范围内的应用

六、分片策略对比

6.2 适用场景分析

分片类型 数据分布均衡性 查询效率 扩展性 实现复杂度 最适合场景
哈希分片 点查询高,范围查询低 点查询为主的OLTP系统
范围分片 中低 范围查询高,点查询中 范围查询频繁的系统
复合分片 多维查询高,单维查询中 多维查询和复杂业务场景
时间序列分片 时间范围查询高 时间相关数据和归档需求

6.3 扩容影响分析

分片类型 增加分片的数据迁移量 查询路由改变 应用改造复杂度
哈希分片 高(约50%数据需迁移)
范围分片 低(仅需调整边界数据)
复合分片 中高(取决于算法设计)
时间序列分片 低(仅影响新数据)

七、总结

在实际应用中,应根据业务特点、查询模式、性能需求和扩展预期来选择最合适的分片策略。

无论选择哪种分片策略,都应保持分片逻辑的简洁性和可维护性,并在系统设计初期就考虑未来的扩展需求。

通过合理选择分片键,结合SpringBoot和ShardingSphere等工具,可以构建出既满足业务需求又具备良好扩展性的应用。

相关推荐
FlyWIHTSKY2 分钟前
idea中push拒绝,merge,rebase的区别
java·ide·intellij-idea
Code季风7 分钟前
深入实战 —— Protobuf 的序列化与反序列化详解(Go + Java 示例)
java·后端·学习·rpc·golang·go
深栈解码19 分钟前
OpenIM 源码深度解析系列(十二):群聊读扩散机制场景解析
后端
篱笆院的狗24 分钟前
Spring Boot 工程启动以后,我希望将数据库中已有的固定内容,打入到 Redis 缓存中,请问如何处理?
数据库·spring boot·缓存
MrWho不迷糊30 分钟前
模板方法与工厂模式实践——一套通用交易执行模型
后端·设计模式
我想说一句31 分钟前
WEUI Uploader源码学习笔记:从CSS到Stylus
前端·后端
Leslie_Lei31 分钟前
【pdf】Java代码生成PDF
java·pdf
武子康32 分钟前
大数据-18 Flume HelloWorld 实现Source Channel Sink 控制台流式收集
大数据·后端·apache flume
OnlyLowG37 分钟前
头痛的旧技术:低版本dubbo接入otel、低版本Logstash接入otel
后端
工呈士1 小时前
HTTP 请求方法与状态码
前端·后端·面试