范围分片

范围分片是最直觉的分片方式——按数据的某个连续区间划分分片。时间序列数据按月份分、用户数据按 ID 区间分。这种策略简单直观,适合特定类型的数据。

按 ID 区间划分

最典型的范围分片策略。每个分片负责一段连续的 ID 区间。

flowchart LR
    subgraph Range_Sharding["范围分片"]
        App["应用"]
        App --> Router["路由层"]

        Router -->|ID 1-1000万| DB1["节点 1"]
        Router -->|ID 1000万-2000万| DB2["节点 2"]
        Router -->|ID 2000万-3000万| DB3["节点 3"]
    end
范围分片路由
@Service
public class RangeShardRouter {

    private static final long SHARD_SIZE = 10_000_000L; // 每个分片 1000 万

    public int getShardIndex(long id) {
        return (int) (id / SHARD_SIZE);
    }

    public DataSource getDataSource(long id) {
        int shardIndex = getShardIndex(id);
        return shardManager.getDataSource(shardIndex);
    }

    public List<Long> getShardIndices(long startId, long endId) {
        int startIndex = (int) (startId / SHARD_SIZE);
        int endIndex = (int) (endId / SHARD_SIZE);

        List<Long> indices = new ArrayList<>();
        for (int i = startIndex; i <= endIndex; i++) {
            indices.add((long) i);
        }
        return indices;
    }
}

优点:范围查询高效

范围分片最大的优势是范围查询效率高。如果查询条件包含分片键,可以直接定位到特定分片。

范围查询示例
@Service
public class OrderService {

    @Autowired
    private RangeShardRouter router;

    // 高效的范围查询
    public List<Order> getOrdersByIdRange(long startId, long endId) {
        List<Long> shardIndices = router.getShardIndices(startId, endId);

        return shardIndices.stream()
                .map(shardIndex -> {
                    DataSource ds = shardManager.getDataSource(shardIndex.intValue());
                    return jdbcTemplate.queryForList(
                        ds,
                        "SELECT * FROM orders WHERE id BETWEEN ? AND ?",
                        startId, endId
                    );
                })
                .flatMap(List::stream)
                .collect(Collectors.toList());
    }
}

典型的范围查询场景

  • 分页查询:LIMIT 100 OFFSET 1000,可以直接定位分片
  • 时间范围查询:如果分片键是时间,按月分片后查询某月数据只需访问一个分片
  • ID 区间查询:直接定位到区间所在分片

缺点:热点数据不均匀

范围分片的致命弱点是容易产生热点。

热点问题

新注册的用户、刚发布的商品、最近的订单——这些「热数据」往往集中在最新的分片。

假设按月份分片:

  • 1 月数据写入分片 1
  • 2 月数据写入分片 2
  • ...
  • 12 月数据写入分片 12

大促期间(如 11 月、12 月)的订单全部写入最新分片,导致这个分片负载远超其他分片。

不均匀问题

业务增长不均匀时,分片数据量差异巨大。

假设用户 ID 是自增的:

  • 早期用户(ID 1-100 万)活跃度低,数据量小
  • 当前用户(ID 1 亿-1.1 亿)活跃度高,数据量大

范围分片会让新分片数据量远超旧分片。

分片数据量不均匀示例
public class DataDistribution {

    public static void main(String[] args) {
        // 模拟数据分布
        long[] shardDataSize = {
            100_000,   // 分片 0: 早期用户,少量数据
            150_000,   // 分片 1
            200_000,   // 分片 2
            500_000,   // 分片 3
            1_000_000, // 分片 4
            5_000_000, // 分片 5: 当前用户,数据量大
            8_000_000, // 分片 6: 当前用户,接近上限
        };

        // 分片 6 数据量是分片 0 的 80 倍!
    }
}

热点解决方案

预创建分片:提前创建大量空分片,让数据分散写入。适合持续增长的数据。

预创建分片策略
@Service
public class PreShardManager {

    private static final int PRE_SHARD_COUNT = 1024;

    public int getShardIndex(long id) {
        return (int) (id % PRE_SHARD_COUNT);
    }
}

热点数据隔离:识别热点分片后,把热点数据迁移到独立的高性能分片。

结合时间分片:对于持续增长的数据,可以结合范围分片(按时间)和哈希分片(如按用户 ID 哈希后范围分片),让数据分布更均匀。

适用场景

范围分片不是万能的,它有明确的适用场景。

适合范围分片的场景

时间序列数据:日志、监控数据、交易记录。时间范围查询是主要访问模式,按时间分片效率最高。

时间序列分片
@Service
public class TimeSeriesShardRouter {

    public String getShardKey(LocalDate date) {
        // 按月分片
        return String.format("%d-%02d", date.getYear(), date.getMonthValue());
    }

    public List<String> getShardsInRange(LocalDate start, LocalDate end) {
        List<String> shards = new ArrayList<>();
        LocalDate current = start;
        while (!current.isAfter(end)) {
            shards.add(getShardKey(current));
            current = current.plusMonths(1);
        }
        return shards;
    }
}

读多写少的数据:日志、归档数据。写入集中在最新分片,读取可能涉及多个历史分片,范围分片不影响读取性能。

有明确边界的数据:如行政区划、学号、工号。这些数据的范围是天然确定的。

不适合范围分片的场景

写入热点的数据:如大促订单、实时消息。写入集中在最新分片,热点问题严重。

需要均匀分布的数据:如用户数据、业务数据。选择哈希分片更合适。

查询条件多样的数据:如果查询不总是带分片键,范围分片的优势无法发挥。

范围分片的配置示例

ShardingSphere
schemaName: app_db

dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:3306/ds_0
    username: root
    password:

rules:
- sharding:
    tables:
      orders:
        actualDataNodes: ds_$->{0..3}
        tableStrategy:
          standard:
            shardingColumn: order_id
            shardingAlgorithmName: orders_range
        keyGenerateStrategy:
          column: order_id
          keyGeneratorName: snowflake
    shardingAlgorithms:
      orders_range:
        type: INTERVAL
        props:
          sharding-column: order_id
          logical-table-prefix: orders_
          sharding-length: 10000000
          datetime-lower: 2020-01-01
          datetime-upper: 2030-01-01
          datetime-interval-unit: MONTH
          datetime-interval-amount: 1

常见误区

误区一:分片区间设置固定不变

业务在增长,分片区间应该动态调整。设置过小的区间会导致分片过多,设置过大的区间会导致数据不均匀。

误区二:忽视热点分片

范围分片的热点问题不可避免。应该在监控系统上关注各分片的负载差异,及时处理热点。

误区三:范围分片不需要容量规划

范围分片更需要容量规划。每个分片的存储上限决定了区间的划分,需要根据业务增速预留余量。

延伸思考

范围分片的核心价值是「让范围查询高效」。这个价值在特定场景下无可替代——时间序列数据、读写分离的数据归档。

但它也有明确的代价:热点和不均匀。选择范围分片前,先问自己:

  • 我的业务查询,主要是范围查询吗?
  • 数据增长模式是均匀的还是集中的?
  • 能接受热点分片带来的问题吗?

如果答案是肯定的,范围分片是一个好选择。如果否,考虑哈希分片或其他策略。