共计 3615 个字符,预计需要花费 10 分钟才能阅读完成。
背景
Timestream 模型是针对时序场景设计的特有模型,可以让用户快速完成业务代码的开发,实现相关业务需求。但是,如果业务系统不仅想实现基础的相关业务功能,还要达到最佳的性能,并且兼顾到未来的扩展性的话,就不是一件特别容易的事情。
本文会以共享汽车管理平台为例,介绍一系列的 timestream 最佳设计和使用,给业务设计和使用提供一些参考。关于共享汽车管理平台的场景,细节请参考:《基于 Tablestore 的共享汽车管理平台》。
场景和模型简介
在共享汽车管理平台这个场景中,主要是对车辆的状态轨迹监控、车俩元数据以及订单元数据进行管理。另外,还会对相关的数据进行计算分析并存储相关结果:
- 车辆状态轨迹 :记录了车辆的状态监控,比如车速、位置、续航等数据,另外还需要记录车辆行驶过程中的违章记录,比如:是否超速、是否闯红灯等等;
- 车辆元数据 :记录车辆的基本属性信息,便于用户进行车辆检索,比如:车型、车牌、颜色等;
- 订单元数据 :订单相关信息记录,包含行程的起止时间、车辆、用户、费用等信息
业务主要是对上面三部分数据进行查询和检索,满足业务场景的需求。其中车辆元数据以及状态轨迹数据是典型的时序序列,可以很方便的映射到 Timestream 模型中。
下图是数据模型的映射:
下面介绍一下模型设计的细节以及设计中需要注意的一些优化点,这些优化点对于业务功能以及性能上都有一定的提升。
业务模型设计
在 Timestream 模型中,主要包含了元数据和数据点两部分数据,分别使用一个元数据表以及若干个数据表进行存储。下面介绍这两类数据在存储设计的关键点。
元数据表设计
在共享汽车这个场景下,元数据表主要存储两类数据:车辆的基本信息、车辆的最近状态数据(位置、续航、状态、违章统计等),业务会根据各类信息进行多条件的组合查询符合条件的车辆。
如上图所示,Timestream 的元数据表会通过多元索引来提供丰富的数据检索能力。在 Timestream 模型的元数据中,包含了 name、tags、attributes 三类数据,其中 name、tags 默认会提供数据检索能力,attributes 则需要在创建 Meta 表的时候指定需要索引的 attributes 字段以及相关信息,默认 attributes 并不支持检索。
需要注意的是,目前并不支持动态修改 Meta 表的索引字段,所以最好能在设计之初能够考虑到当前以及未来的功能需求,下面介绍一下相关信息是如何映射到模型以及相关的设计。
name 设计
name 字段的选取是很关键的,是数据检索性能的一个重要影响因素,不同的 name 字段设计可能会导致查询延时相差一个数量级。name 字段的选取建议满足以下条件:
- 绝大多数查询场景都会对该字段进行精确查询
- 该字段单个取值下的最大记录数不宜过多,比如说不超过一千万条记录
在共享汽车管理平台这个场景下,管理的是各个平台的车辆,而在车辆检索的时候,一定会指定的条件是平台的名字,并且某个平台的车辆其实也不会太多,一般也就百万量级,所以这里可以将平台作为 name。
tags 设计
在 Timestream 模型中,Name 和 Tags 可以唯一确定某个元数据,在这个场景中,唯一确定某辆车的信息是:平台、车辆 ID,其中平台是 name,所以,tags 中只需要存储 ID 即可。
tags 设计需要注意:
- tags 的总长度尽可能的短,只把唯一确定主体的信息放到 tag 中,其余信息均放到 attributes 中
- tag 只支持 string 类型的数据,如果业务字段是数值类型,需要将其转成 string 进行存储
attributes 设计
attributes 是主体的可变属性,也可以用来存储主体的非唯一属性。在这个场景中,车辆的基本信息以及当前状态都是用 attributes 来进行存储。attributes 设计关键点:
- 创建 meta 表的时候需要指定有检索需求的 attributes 以及相关属性,默认 attributes 是不支持索引的
- 数值型数据尽可能使用 int 来存储,attribute 支持多类型的数据,但在数据检索过程中,int 类型的数据检索效率比 string 类型高的多,建议使用 int,索引类型为 LONG
- 考虑未来系统的扩展性,可以预留一列作为扩展字段,索引类型为 KEYWORD,并且是 Array
索引创建示例代码:
public void createMetaTable() {
db.createMetaTable(Arrays.asList(new AttributeIndexSchema("地区", AttributeIndexSchema.Type.KEYWORD),
...
// 数值类型索引
new AttributeIndexSchema("座位", AttributeIndexSchema.Type.LONG),
...
// 扩展字段,数组类型索引
new AttributeIndexSchema("配置 1", AttributeIndexSchema.Type.KEYWORD).isArray()));
数据表设计
Timestream 可以支持多个数据表的存储,来满足不同的业务场景需求。另外,为了能够利用底层引擎所做的性能优化,我们推荐 append 的写入方式,即不会对已有数据进行修改,所以在以下场景中,我们建议业务将数据分到不同数据表中进行存储:
- 数据精度不同 ,特别是在监控场景下,这个需求更加突出。按数据精度分表便于后续数据的查询,如果查询长周期的数据可以去查询低精度的表,减少查询的数据量,提高查询效率
- 需要对数据进行加工处理 ,也就是会对数据进行更新,建议将处理之后的数据写到另外一张表中
在共享汽车这个场景中,需要对车辆的状态轨迹数据进行流式处理,比如检测是否超速等违章、车辆状态轨迹是否异常等,然后将处理之后的数据写到另外一张表中,提供给业务进行查询。
sdk 使用
前面介绍了业务模型设计需要注意的地方,对业务功能拓展能力以及性能都有一定的提升。下面介绍一下 timestream sdk 使用的一些性能优化点。
数据写入
元数据
元数据写入支持两种方式:put 和 update。其中 put 会删除老的记录,并且插入一个全新行;update 则是对原有记录的部分 attributes 进行更新。建议尽量使用 Put 的方式进行写入。
示例代码:
public void writeMeta() {TimestreamIdentifier identifier = new TimestreamIdentifier.Builder("* 滴")
.addTag("ID", carNo)
.build();
TimestreamMeta meta = new TimestreamMeta(identifier)
.addAttribute("地区", "杭州")
.addAttribute("座位", 4)
...
.addAttribute("状态", "闲置");
// 插入车辆信息
metaWriter.put(meta);
}
数据点
数据点写入也提供了两种方式:同步和异步。其中异步接口底层是通过 TableStoreWriter 进行异步写入,其写入吞吐能力更高,对写入延时不是特别敏感的业务建议使用异步方式。
示例代码:
public void writeData() {TimestreamIdentifier identifier = new TimestreamIdentifier.Builder("* 滴")
.addTag("ID", carNo)
.build();
Point point = new Point.Builder(1546272000, TimeUnit.SECONDS)
.addField("位置", "30.1457580736,120.0563192368")
.addField("车速", 30)
.addField("续航", 100)
.build();
// 异步写入
dataWriter.asyncWrite(identifier, point);
}
数据查询
元数据查询
元数据查询的时候,建议指定需要返回的列名。如果没有显示指定列名的话,会去读主表以获取完整的信息,这样每一行元数据都会反查一次主表,查询性能会更差一些。
示例代码:
Filter filter = Name.equal("* 滴");
// 用 selectAttributes 指定需要返回的 attributes
Iterator<TimestreamMeta> iter = metaTable.filter(filter)
.selectAttributes("座位", "状态")
.fetchAll();
总结
本文介绍了 Tablestore Timestream 在模型设计以及 sdk 使用中的一些优化点,对于业务现有功能实现、未来拓展以及性能提升都有很好的作用,更加细节的 timestream 使用请参考《Timestream 模型》。
本文作者:lyan094
阅读原文
本文为云栖社区原创内容,未经允许不得转载。