表设计最佳实践

更新时间:
复制 MD 格式

表格存储按分区键范围自动将数据切分到多个服务节点。分区键选择不当会导致热点、数据倾斜或扩展瓶颈。本文介绍主键设计、属性列设计和分表规划的最佳实践,帮助设计高性能、可扩展的数据表结构。

主键设计

表格存储根据分区键(第一列主键列)的范围将数据自动切分为多个分区,分布在不同的服务节点上。主键设计的核心目标是让数据和访问压力均匀分散,避免热点。

数据散列

在分布式数据系统中,数据分布不均匀会导致以下问题:

  • 读写能力受限于单个分区,存在明显瓶颈。

  • 热点数据分布不均导致长尾效应,拖慢整体处理速度。

  • 热点分区成为整个业务链路的瓶颈,影响上下游系统。

数据均匀分布在多个分区时,读写压力分散到各分区,每个请求仅覆盖局部数据,可通过增加机器资源实现水平扩展。

说明

如果业务负载较低(TPS/QPS 在 1000 以下、数据量在 10 GB 以内且不会大幅增长),热点问题的影响较小,单分区即可支撑。但架构设计上不应依赖单分区能力。

常见热点问题与解决方法

以监控场景为例:数据表存储每台机器在每个时间点的指标值,主键设计如下:

Timestamp(分区键)

MachineIp

问题

1718000001

10.10.0.1

每次写入都追加到表的末尾(最后一个分区),产生尾部热点

1718000002

10.10.0.2

1718000003

10.10.0.1

由于数据按分区键范围分区,所有写入都集中在最后一个分区,导致尾部热点,无法通过分片分裂来平衡负载。

方法一:调整分区键顺序

将 MachineIp 放到主键第一列,Timestamp 放到第二列。调整后主键为 MachineIp = "10.10.0.1", Timestamp = 1718000001,写入压力按机器维度分散到不同分区。类似的分区键选择如 UserIdDeviceIdOrderId 等业务上天然分散的字段,都是常见做法。

说明

如果某个 IP 段的写入量集中在同一分区,表格存储会自动将该分区切分,将压力分散到多个分区上。

方法二:拼接 MD5 前缀

对 MachineIp 计算 MD5,取前 4 位十六进制字符拼接到 IP 前面作为分区键。例如 10.10.0.1 的 MD5 前 4 位为 a1b2,拼接后分区键变为 a1b2,10.10.0.1。这种方式打散了 IP 段的顺序性,同时十六进制前缀有利于系统进行预分区。

方法三:全局有序需求的替代方案

如果业务需要按时间全局有序查询,但又不希望产生尾部热点,可以采用以下替代方案:

  • 局部有序:将分散字段放在第一列,同一分散字段下的数据仍按时间有序。例如在某个 MachineIp 下按 Timestamp 有序。

  • 分桶写入:将时间对 N 取模作为第一列主键(0 到 N-1),时间作为第二列。例如使用 16 个桶,写入时分区键为 timestamp % 16 = 3,读取时并行查询 16 个桶后合并。桶数越多,压力越分散,但读取时需查询的桶也越多。

  • 多元索引:数据表使用均匀散列方式写入,通过多元索引实现按时间等字段的有序查询。多元索引自动将数据散列到多个分片,查询时自动合并结果。

主键列长度与数据量约束

  • 同一分区键值下的数据量建议控制在 10 GB 以内(无硬性限制),因为相同分区键值的行无法进一步切分。

  • 主键列长度限制为 1 KB,使用较短的主键有利于提升查询速度。

分区键设计原则

分区键是数据表的第一列主键列。表格存储根据分区键的范围将数据自动切分为多个分区,每个分区调度到不同的服务节点。分区键设计需要满足以下原则:

  • 同一分区键值下的数据量不超过 10 GB。

  • 不同分区键值中的数据在逻辑上独立。

  • 访问压力不集中在小范围连续的分区键值中。

通过拼接方式使用分区键

如果单个分区键值的所有行总数据量可能超过 10 GB,建议将多个主键列拼接为一个新的分区键,以分散数据。拼接时需注意以下规则:

  • 拼接后的分区键必须能有效地将原来相同分区键值的记录拆分为不同分区键值的记录。

  • 拼接整数类型主键列时在高位补 0,保持排序一致。例如 OrderNumber = 123 补零后为 00000123,确保字典序与数值序一致。

  • 连接符应选择 ASCII 值小于所有可用数据字符的字符,避免影响新分区键的字典序。具体选择取决于业务数据的字符集。例如,假设 SellerID 的取值包含数字和字母(如 a100a1001),不同连接符对排序的影响如下:

    连接符

    拼接后的排序

    原因

    结果

    :

    000054:a1001 排在 000054:a100: 前面

    : 的 ASCII 值大于数字,a1001 在第 5 位比较 1 < :

    排序错误

    ,

    000054,a100, 排在 000054,a1001 前面

    , 的 ASCII 值小于所有数字和字母,字典序与原始数据顺序一致

    排序正确

在分区键中加入哈希前缀

如果必须使用顺序增长的列作为分区键,建议在分区键前加入哈希前缀,使相邻的数据在表中随机分布,保证访问压力均匀。

说明

加入哈希前缀后,原来连续的数据会被打散,无法再使用范围读取操作读取逻辑上连续的数据。如果需要范围查询,可以使用多元索引替代。

属性列设计

  • 控制行宽度:表格存储支持宽行(最多数十万个属性列),但一次性读取超宽行可能超时。原则上不建议超过万列,可通过指定列名或分页方式读取。

  • 属性列大小限制:单个属性列值不超过 2 MB。超过该限制的数据需要拆分成多列存储,或使用对象存储 OSS进行存储。

  • 按访问频率拆分表:当行的属性列较多且各列的访问频率差异较大时,将高频和低频属性列分别存储在不同的表中。例如商品管理系统中,商品数量和商品价格访问频率高,商品简介(大文本)访问频率低,可以拆分为两张表。

  • 压缩大文本:较大的属性列文本建议压缩后以二进制类型存储,节省存储空间并降低读写计算开销。

  • 货币和价格使用 Long 类型:表格存储不支持 BigDecimal 类型。涉及金额等需要精确计算的字段,推荐使用 Long 类型存储最小单位值(例如 5 元 3 角 2 分存储为 53200),避免 Double 类型的精度损失。

分表与容量规划

分表存储

推荐单个多元索引的数据量控制在 200 亿行以内。如果数据规模超过 200 亿行,请联系表格存储技术支持进行分表评估和设计。

例如某用户最大的日志表当前为 61 亿行,年增长 21 亿行,3 至 5 年内不会超过 200 亿行,因此无需分表。当存量数据较大且增长速度快时,应提前规划分表策略。

冷热数据分离

数据通常具有时效性:近期数据访问频率高,较早的数据逐渐成为冷数据。冷热数据混存在同一张表中会导致访问压力不均匀,预留读写吞吐量无法充分利用。

建议用不同的表区分冷热数据,并设置不同的预留读写吞吐量。对热数据可建立多元索引支持多维查询,冷数据使用固定维度的二级索引降低成本。多元索引支持数据生命周期(TTL),可以通过索引 TTL 实现冷热数据的自动区分。

大批量数据导入

向表格存储大批量导入数据时,如果按主键顺序写入,写入压力会集中在某个分区,影响导入速度。建议采取以下措施:

  • 将大数据集切分成多个小集合,使用多个工作线程随机选取小集合并行导入。

  • 写入前联系表格存储技术支持进行预分区,使数据写入时即可分散到多个分区。

  • 使用 TableStoreWriter 进行高并发异步写入,自动实现分桶分发和批量提交。

设计案例:学生卡消费记录

以某大学学生卡消费记录系统为例,说明表设计的决策过程。数据表的主键列包括学生卡 ID(CardID)、商家 ID(SellerID)、消费终端 ID(DeviceID)和订单号(OrderNumber)。业务规则如下:

  • 每张学生卡对应一个 CardID,每个商家对应一个 SellerID。

  • 每个消费终端对应一个全局唯一的 DeviceID。

  • OrderNumber 在单个终端内唯一且按时间递增,但全局不唯一。

  • 消费记录实时写入。

主键与分区键选择

首先确定分区键。根据各主键列的特性,对比不同分区方式的效果:

分区方式

分析

CardID 为分区键

每个学生日均消费次数有限,写入压力分散在几万张卡上,数据散列度较好。推荐。

SellerID 为分区键

商家数量少,大部分消费集中在少数商家,容易产生热点,不推荐。

DeviceID 为分区键

终端设备全局唯一,分散度好。但少量高频终端(如热门食堂的刷卡机)仍可能产生局部热点。

OrderNumber 为分区键

OrderNumber 在终端内按时间递增,直接作为分区键会导致尾部热点。需要加入哈希前缀。

设计结论

综合分析,CardID 的数据散列度最好(几万张卡、每人每天几次消费),推荐作为分区键。确定主键顺序为 CardID, DeviceID, SellerID, OrderNumber。对于大多数业务场景,完成主键和分区键的选择后即可满足需求。

进阶优化

基础的分区键选择可能不足以满足性能需求,以下介绍两种常见的进阶优化方式。

场景一:单一分区键数据量过大时,拼接多列为分区键

如果业务要求按 DeviceID 查询并选择其作为分区键,单个 DeviceID 下的数据量可能超过 10 GB。此时可以将 DeviceID、SellerID 和 CardID 三列拼接为新的分区键,将同一终端设备的记录按商家和学生卡进一步拆分。

拼接前

DeviceID(分区键)

SellerID

CardID

OrderNumber

问题

54

10

1001

000001

DeviceID=54 的所有记录在同一分区,长期累积数据量可能超 10 GB

54

20

1002

000002

78

20

1001

000003

拼接后

DeviceID,SellerID,CardID(新分区键)

OrderNumber

效果

54,10,1001

000001

同一终端设备在不同商家和学生卡的消费记录被拆分到不同分区键值下

54,20,1002

000002

78,20,1001

000003

场景二:分区键值顺序增长时,加入哈希前缀

如果业务要求按 OrderNumber 查询并选择其作为分区键,由于 OrderNumber 在终端内按时间递增,会导致尾部热点。此时需要对 OrderNumber 计算 MD5 哈希前缀来打散数据分布:

原始 OrderNumber

MD5 前 4 位

HashOrderNumber(分区键)

000001

e2c8

e2c8,000001

000002

3f79

3f79,000002

000003

a5b1

a5b1,000003

加入哈希前缀后,可以使用相同算法对 OrderNumber 计算哈希前缀得到对应的 HashOrderNumber。但哈希前缀会打散原有顺序,无法再使用范围读取操作读取逻辑上连续的记录。

冷热数据分离

学生卡消费记录具有明显的时效性:近期记录查询频率高,已毕业学生的记录几乎不再访问。建议按以下方式规划冷热数据:

  • 将近期活跃数据(如近 1 年)和历史数据分别存储在不同的表中,设置不同的预留读写吞吐量。

  • 活跃表配置较高的预留吞吐量,历史表配置较低的吞吐量,降低存储成本。

  • 对活跃表建立多元索引支持多维度查询(如按商家、按时间范围),历史表仅保留主键查询即可。

  • 定期将活跃表中超过时效的数据迁移到历史表中。