Z 轴扩展:数据分区

X 轴复制了服务实例,Y 轴拆分了业务功能,但数据还是存在一起。当数据量增长到单机无法存储时,X 轴和 Y 轴都无能为力。Z 轴扩展,就是解决「数据量太大」问题的方案。

什么是 Z 轴扩展

Z 轴扩展是按数据属性对数据进行分区(Sharding)。不同的数据子集存储在不同的分片中,每个分片存储一部分数据,负载分散到多个存储节点。

flowchart TB
    subgraph Router["请求路由器"]
        LB["负载均衡器"]
        Router["分片路由"]
    end

    subgraph Shards["Z 轴:数据分区"]
        Shard1["分片 1<br/>用户 1-1000万"]
        Shard2["分片 2<br/>用户 1000万-2000万"]
        Shard3["分片 N<br/>用户 N亿+"]
    end

    LB --> Router
    Router --> Shard1
    Router --> Shard2
    Router --> Shard3

与 X 轴的区别:X 轴每个节点有完整数据,只是处理不同请求;Z 轴每个节点只有部分数据。

按数据属性分区

Z 轴的核心是选择正确的「分区维度」。这个维度叫分片键(Shard Key),数据按分片键的值决定归属哪个分片。

常见分片键

用户 ID 哈希:最常用的分片键。用户请求总是携带用户 ID,按用户 ID 路由,天然实现用户数据隔离。

用户
@Service
public class UserShardRouter {

    private final int shardCount = 4;

    public int getShardIndex(Long userId) {
        return (int) (userId % shardCount);
    }

    public String getShardName(Long userId) {
        int index = getShardIndex(userId);
        return "shard_" + index;
    }

    public UserData getUserData(Long userId) {
        String shardName = getShardName(userId);
        return shardTemplate.getShard(shardName).getUserData(userId);
    }
}

地区/地域:适合有地域属性的业务,如用户按省份分区、订单按发货仓库分区。

时间/日期:适合时间序列数据,如日志、监控数据、交易记录。按月或按年分区。

字母前缀:按用户名的首字母分区,实现相对均匀的分布。

分片键选择原则

分片键的选择直接影响系统性能和数据分布均匀度。

原则说明
高基数分片键的取值范围要大,确保能拆成足够多的分片
数据均匀避免热点数据集中在某个分片
访问局部性同一实体的相关数据在同一个分片
业务适配符合业务访问模式

错误示例:按性别分两个分片——数据不均匀不说,每次查询几乎都要跨分片。

正确示例:按用户 ID 分片——高基数、数据分布均匀、查询总是定位到单个分片。

分片键设计

分片键一旦确定,修改代价极高。需要仔细设计。

单分片键 vs 复合分片键

单分片键:用一个字段作为分片键。简单,但可能无法满足复杂查询需求。

复合分片键:用多个字段组合作为分片键。例如 region_userid,既能按地区路由,又能区分用户。

复合分片键路由
@Service
public class OrderShardRouter {

    public String getShardKey(String region, Long orderId) {
        return region + "_" + (orderId % 100);
    }

    public Order getOrder(String region, Long orderId) {
        String shardKey = getShardKey(region, orderId);
        return shardTemplate.getShard(shardKey).getOrder(orderId);
    }
}

分片键与查询模式

分片键的选择应该匹配最常见的查询模式。

如果查询总是带用户 IDuser_id 是合适的分片键。

如果查询总是带时间范围create_timeyear_month 是合适的分片键。

如果查询条件多样(有时按用户、有时按地区):考虑两个方案——维护多个分片键的索引,或接受跨分片查询的性能损耗。

分片键修改的代价

分片键修改意味着数据需要重新分布。这是一个灾难性的操作:

  • 需要迁移所有历史数据
  • 迁移期间服务需要继续运行
  • 迁移完成后需要验证数据一致性

解决方案是在初期预留足够的分片数,并使用「逻辑分片 + 物理分片」的二层架构:

逻辑分片到物理分片的映射
@Service
public class ShardManager {

    // 逻辑分片数,预留余量,未来可以扩展
    private static final int LOGICAL_SHARDS = 1024;

    // 物理节点数
    private volatile int physicalNodes = 4;

    // 一致性哈希环
    private final ConsistentHashRing hashRing = new ConsistentHashRing();

    public void initPhysicalNodes(List<String> nodes) {
        hashRing.addNodes(nodes);
    }

    // 逻辑分片数固定,通过增加物理节点实现扩容
    public String getPhysicalNode(String logicalShardKey) {
        return hashRing.getNode(logicalShardKey);
    }
}

适用场景

Z 轴扩展适合特定的数据问题,不是所有数据都需要分片。

适合 Z 轴扩展的场景

数据量巨大:单表数据超过千万级、单机磁盘容量不足。MySQL 单表建议控制在千万以内,MongoDB 单分片建议控制在 TB 级以内。

单用户数据隔离:每个用户只访问自己的数据,按用户 ID 分片效率最高。

合规要求:数据按地区隔离(如金融行业的属地化要求)。

性能瓶颈在存储:数据库成为系统瓶颈,水平扩展服务实例无效。

不适合 Z 轴扩展的场景

数据量中等:几百万数据,加索引、加缓存就能解决,没必要引入分片复杂性。

跨实体查询多:如果大部分查询需要跨多个分片聚合,分片的优势会被抵消。

业务早期:业务模型还不稳定,分片键选错后修改成本极高。

团队能力不足:分片带来运维复杂性(数据迁移、跨分片查询、热点问题),需要配套工具和经验。

Z 轴扩展的代价

Z 轴扩展解决了数据量问题,但引入了新的复杂性。

跨分片查询:单次查询可能需要访问多个分片,然后归并结果。性能损耗严重。

分片间平衡:数据增长不均匀时,部分分片可能成为热点,需要重新平衡。

分布式事务:涉及多个分片的数据变更,需要分布式事务机制(如 Seata)。

运维复杂度:备份、恢复、监控都需要考虑分片维度。

X/Y/Z 三轴对比

维度问题域扩展方式数据分布复杂度
X 轴请求量增长水平复制实例完整副本
Y 轴业务复杂度功能拆分服务服务独立
Z 轴数据量增长数据分区部分数据

常见误区

误区一:过早分片

分片是最后手段,不是首选方案。单机数据库能支持的数据量远比你想象的大(合理的 SQL、合适的索引、充足的内存),不应该一上来就分片。

误区二:分片键选择随意

分片键影响数据分布和查询性能。选错分片键可能导致热点分片、跨分片查询,性能反而下降。

误区三:分片数固定不变

业务增长,分片数必然要调整。选择支持动态扩容的分片方案,避免未来数据迁移的痛苦。

误区四:忽视分片带来的查询问题

分片后,跨分片的查询(JOIN、聚合、分页)变得极其复杂。在决定分片前,评估你的查询模式是否适合分片。

延伸思考

Z 轴扩展的核心挑战是「数据分布」和「查询路由」。好的分片设计应该让数据均匀分布、查询路由简单。

实践中,Z 轴通常与 X/Y 轴组合使用。先按 Y 轴拆服务,每个服务的数据库再按 Z 轴分片。例如:用户服务拆出来,用户库按用户 ID 分 4 个分片;订单服务拆出来,订单库按用户 ID 分 8 个分片。

理解每个轴的边界和代价,才能做出正确的架构决策。Z 轴不是银弹,它解决了一类问题,但带来了另一类问题。在决定 Z 轴之前,问自己:数据量真的大到单机存不下了吗?有没有其他方案(归档、冷热分离、索引优化)?