表操作最佳实践

更新时间:2025-03-19 01:40:32

通过本文您可以了解关于表操作的最佳实践。

主键设计

表格存储会根据表的分区键将表的数据自动切分成多个分区,每个分区调度到一台服务节点上。分区键的值是最小的分区单位,相同的分区键值下的数据无法再做切分。

为了防止某一个分区键值的数据成为访问热点造成单机服务能力达到上限,应用程序需要让数据和访问量的分布尽可能均匀。

表格存储会对表中的行按主键进行排序,合理设计主键可以使数据在分区上的分布更均匀,从而能够充分利用表格存储水平扩展的特点。

选取分区键时,建议遵循以下几个原则:

  • 单个分区键值中的数据不宜过大,建议不超过 10 GB。

    说明

    单个分区键的总数据量不超过 10 GB 是为了避免访问热点,而不是数据存储的限制。

  • 一张表内不同分区键值中的数据在逻辑上是独立的。

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

分区键设计

通过拼接方式使用分区键

建议表格存储中表的同一分区键值下的数据量大小不超过 10 GB。如果您表中单个分区键值的所有行总数据量可能超过 10 GB,则在设计表时可以将原来的多个主键列拼接成分区键。

当表中单个分区键值的所有行的数据量总大小可能超过 10 GB 时,可以将多个主键列拼接成分区键,以避免单分区键值的数据量大小限制。在拼接分区键时需要注意以下事项:

  • 选取需要拼接的多个主键列,必须能有效地将原来表中相同的分区键值的记录,变成拥有不同分区键值的记录。

  • 拼接 Integer 类型主键列时可以在高位补 0,保持记录的顺序一致。

  • 选取连接符时需要考虑连接符对新的分区键的字典序的影响,选取比所有可用字符都小的连接符是一个比较安全的选择。

在分区键中加入哈希前缀

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

在分区键中加入哈希前缀存在的问题是原来连续的数据会被打散,无法再使用 GetRange 操作读取一段范围内逻辑上连续的数据。

数据操作

并行写入数据

表格存储的表会被切分为多个分区,这些分区分散在多个表格存储的服务器上。

当有一批按主键顺序排列的数据要上传到表格存储中时,如果按顺序写入数据,则可能会导致写入压力集中在某个分区中,而其他的分区处于空闲状态,无法有效利用预留读写吞吐量,影响数据导入速度。此时,您可以采取以下任意措施来提升导入数据的速率:

  • 将原始数据顺序打乱后再进行导入,以保证写入数据均匀地分配在各个分区上。

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

区分冷数据和热数据

数据一般具有时效性,可能存在近期产生的数据访问较多,较早产生的数据几乎不再被访问的情况。较早产生的数据逐渐成为冷数据,但是仍然占用存储空间。

如果表中存在大量冷数据会导致数据访问压力不均匀,从而使表上配置的预留读写吞吐量无法被充分利用。

为了解决这个问题,您可以用不同的表来区分冷热数据,并设置不同的预留读写吞吐量。

场景案例

通过具体场景案例介绍表设计的过程。

场景介绍

有一张表中存储了某大学内所有学生使用学生卡消费的记录。假设主键列有学生卡 ID(CardID)、商家 ID(SellerID)、消费终端 ID(DeviceID)和订单号(OrderNumber)。同时具有如下规则:

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

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

  • 在每一个消费终端上产生的每一笔消费记录都有一个 OrderNumber。一个消费终端产生的 OrderNumber 是唯一的,但是在全局范围内 OrderNumber 不唯一。例如,不同的消费终端有可能产生两条完全不同的消费记录,但是它们的 OrderNumber 相同。

  • 同一个消费终端产生的 OrderNumber 按时间排序,新的消费记录比老的消费记录拥有更大的 OrderNumber。

  • 每笔消费记录均会被实时写入这张表中。

设计过程

  1. 选择合适的主键作为分区键。

    为高效利用表格存储,在设计表主键时,需要考虑表的分区键设置。不同分区方式说明请参见下表。由下表分析可知,您可以将CardIDDeviceID作为表的分区键,不建议使用 SellerID 和 OrderNumber,然后再根据业务需求设计其余的主键列。

    分区方式

    分析

    分析结论

    分区方式

    分析

    分析结论

    使用 CardID 作为表的分区键

    每天每张卡产生的消费记录数从总体上看是均匀的,每个分区中的访问压力也应该是均匀的。

    使用 CardID 作为表的分区键可以较好地利用预留读写吞吐量资源。

    使用 CardID 作为表的分区键是一个比较好的选择。

    使用 SellerID 作为表的分区键

    由于学校内的商铺数量相对较少,同时一些商铺可能产生大量的消费记录成为热点,这不利于访问压力的均匀分配。

    使用 SellerID 作为表的分区键不是一个好的选择。

    使用 DeviceID 作为表的分区键

    尽管每家商铺的消费记录条数可能相差较大,但是每天每台消费终端上产生的消费记录条数是可预期的。消费终端每天产生的消费记录条数取决于收银员操作的速度,一台消费终端产生的消费记录数是受限的。因此使用 DeviceID 作为表的分区键可以保证访问压力的相对均匀。

    使用 DeviceID 作为表的分区键是一个比较好的选择。

    使用 OrderNumber 作为表的分区键

    由于 OrderNumber 是顺序增长的,因此在同一段时间内产生的消费订单的 OrderNumber 值会集中在一个较小的范围内,这些消费订单记录会集中写入到个别的分区,以致预留读写吞吐量无法得到高效利用。

    如果必须使用 OrderNumber 作为分区键,建议在 OrderNumber 上进行哈希散列,将哈希值作为 OrderNumber 的前缀,保证数据和访问压力的均匀。

    使用 OrderNumber 作为表的分区键不是一个好的选择。

  2. 如果表中单个分区键值的所有行总数据量可能超过 10 GB,请将多个主键列拼接为分区键使用。

    假设学生卡消费记录表的主键为 DeviceID、SellerID、CardID、OrderNumber。其中 DeviceID 为该表的分区键。单个 DeviceID 中所有行总数据量可能超过 10 GB,您可以将 DeviceID、SellerID 和 CardID 拼接作为表的第一个主键列(即分区键)。

    原表的信息如下:

    DeviceID

    SellerID

    CardID

    OrderNumber

    attrs

    DeviceID

    SellerID

    CardID

    OrderNumber

    attrs

    16

    'a100'

    66661

    200001

    ...

    54

    'a100'

    6777

    200003

    ...

    54

    'a1001'

    6777

    200004

    ...

    167

    'a101'

    283408

    200002

    ...

    将 DeviceID、SellerID 和 CardID 拼接为分区键后的表信息如下:

    CombineDeviceIDSellerIDCardID

    OrderNumber

    attrs

    CombineDeviceIDSellerIDCardID

    OrderNumber

    attrs

    '16:a100:66661'

    200001

    ...

    '167:a101:283408'

    200002

    ...

    '54:a1001:6777'

    200004

    ...

    '54:a100:6777'

    200003

    ...

    在原表中,Device=54 的两行是属于分区键值为 54 下的两条消费记录。在新表中,这两条消费记录拥有不同的分区键值。通过拼接多个主键列形成分区键的表减少了单个分区键值下的总数据量。

    在学生卡消费记录表中,由于所有 DeviceID 相同的消费记录其对应的 SellerID 也相同,仅仅拼接 DeviceID 和 SellerID 无法解决单个分区键值数据量过大的问题。因此选择将 DeviceID、SellerID 和 CardID 拼接作为分区键,而不是仅拼接 DeviceID 和 SellerID。

    但是直接拼接主键列时表存在一些问题,例如 DeviceID 是 Integer 类型的主键列,在原表中DeviceID=54 的消费记录在 DeviceID=167 的消费记录前面。将前三列主键列拼接为 String 类型的主键列后,DeviceID=54 的消费记录在 DeviceID=167 的消费记录后面。如果应用程序需要范围读取 DeviceID 在 [15, 100) 之间所有的消费记录,上表的设计信息无法满足使用需求。

    为了应对这种状况,可以通过在 DeviceID 高位补 0。补 0 的个数取决于 DeviceID 最大位数。假设 DeviceID 的取值范围为 [0, 999999],则可以将 DeviceID 高位补 0 到 6 位后再进行拼接。得到的表信息如下:

    CombineDeviceIDSellerIDCardID

    OrderNumber

    attrs

    CombineDeviceIDSellerIDCardID

    OrderNumber

    attrs

    '000016:a100:66661'

    200001

    ...

    '000054:a1001:6777'

    200004

    ...

    '000054:a100:6777'

    200003

    ...

    '000167:a101:283408'

    200002

    ...

    经过高位补 0 后的表依然存在一些问题,例如在原表中 DeviceID=54 的两行、 SellerID='a1001' 的行应该在 SellerID='a100' 的后面。产生这种现象的原因是 '000054:a1001' 的字典序小于 '000054:a100:' ,但是 'a1001' 的字典序大于 'a100' ,连接符 : 影响了字典序。

    在选取连接符时,建议选取比所有可用字符的 ASCII 码都小的字符作为连接符。在该表中, SellerID 的取值为数字、大小写英文字母,经分析发现 , 比所有 SellerID 可用字符的 ASCII 码都小,因此可以使用 , 作为连接符。

    使用 , 拼接后的表信息如下:

    CombineDeviceiDSellerIDCardID

    OrderNumber

    attrs

    CombineDeviceiDSellerIDCardID

    OrderNumber

    attrs

    '000016,a100,66661'

    200001

    ...

    '000054,a100,6777'

    200003

    ...

    '000054,a1001,6777'

    200004

    ...

    '000167,a101,283408'

    200002

    ...

    上表经过拼接形成分区键的表记录顺序和原表的记录顺序一致。

  3. 如果使用的是顺序增长的主键作为分区键,请为分区键拼接哈希前缀。

    由于 OrderNumber 是顺序增长的,消费记录总是被写入最新的 OrderNumber 范围之内,旧的 OrderNumber 不再有写入压力,造成访问压力不均匀的现象,以致预留读/写吞吐量得不到高效利用,因此在表设计时尽量不要使用 OrderNumber 作为表的分区键。

    假设 OrderNumber 是表的分区键,您可以通过对分区键拼接哈希前缀,使相连的 OrderNumber 在表中随机分布,使访问压力分布均匀。

    以 OrderNumber 为分区键的消费记录表如下所示:

    OrderNumber

    DeviceID

    SellerID

    CardID

    attrs

    OrderNumber

    DeviceID

    SellerID

    CardID

    attrs

    200001

    16

    'a100'

    66661

    ...

    200002

    167

    'a101'

    283408

    ...

    200003

    54

    'a100'

    6777

    ...

    200004

    54

    'a1001'

    6777

    ...

    200005

    66

    'b304'

    178994

    ...

    对 OrderNumber 使用 md5 算法计算前缀(您也可以采取其他哈希散列算法),拼接为 HashOrderNumber。由于 md5 算法计算得到的哈希字符串可能过长,因此只需要取前几位即可达到使 OrderNumber 相连的记录在表中随机分布的目的。

    此处以哈希字符串的前 4 位为例介绍拼接哈希前缀的操作。使用拼接后的 HashOrderNumber 作为分区键的消费记录表如下所示:

    HashOrderNumber

    DeviceID

    SellerID

    CardID

    attrs

    HashOrderNumber

    DeviceID

    SellerID

    CardID

    attrs

    '2e38200004'

    54

    'a1001'

    6777

    ...

    'a5a9200003'

    54

    'a100'

    6777

    ...

    'c335200005'

    66

    'b304'

    178994

    ...

    'db6e200002

    167

    'a101'

    283408

    ...

    'ddba200001'

    16

    'a100'

    66661

    ...

    在后续访问消费记录时,使用相同的算法对 OrderNumber 计算哈希前缀,即可得到对应消费记录的 HashOrderNumber。但是使用 HashOrderNumber 作为分区键后无法再使用 GetRange 操作读取一段范围内逻辑上连续的记录。

  4. 根据数据的时效性区分冷热数据存储。

    由于应用程序需要及时地对消费记录进行处理和统计,或者查询最近的消费记录,因此学生卡消费记录表中近期产生的消费记录被访问的可能性较大。但是较早时间的消费记录被查询的可能性不大,这些数据渐渐成为冷数据。另外,表中已毕业学生的卡片不会再产生消费记录。

    假设 CardID 是随着卡片申请时间递增的,以 CardID 作为分区键会导致已毕业学生的 CardID 没有访问压力却被分配到预留读写吞吐量,造成浪费。此时, 您可以将消费记录按月份分表,每个新的自然月使用一张新的表。根据表数据的时效性进行维护。

    • 当月的消费记录表:需要不停地写入新的消费记录,同时有查询操作,您可以为其设置一个较大的预留读写吞吐量来满足访问需求。

    • 前几个月的消费记录表:不再写入新数据或者写入的新数据量较少,查询的请求较多,您可以为其设置较小的预留写吞吐量和较大的预留读吞吐量。

    • 历史超过一年的消费记录表:再被使用的可能性不大,您可以为其设置较小的预留读写吞吐量。

    • 已超出维护年限的消费记录表:不再访问,您可以将数据导出到 OSS 归档或者直接删除数据。

相关文档

如需了解表格存储各场景的应用案例,请参见快速玩转Tablestore入门与实战

  • 本页导读 (1)
  • 主键设计
  • 分区键设计
  • 通过拼接方式使用分区键
  • 在分区键中加入哈希前缀
  • 数据操作
  • 并行写入数据
  • 区分冷数据和热数据
  • 场景案例
  • 场景介绍
  • 设计过程
  • 相关文档
AI助理

点击开启售前

在线咨询服务

你好,我是AI助理

可以解答问题、推荐解决方案等