阿里云表格存储
十年河东,十年河西,莫欺少年穷
学无止境,精益求精
接入表格存储tablestore宽表模型注意事项
1. 一个表只能建4个主键(其中一个要作为分区键),并且主键的顺序要注意,不然后面查询数据会查不到。
2. 第一列主键作为分区键。分区键相同的数据会存放在同一个分区内,所以相同分区键下的数据尽量不要超过10 GB,否则会导致单分区过大,无法分裂。
3. 当使用GetRange扫描的数据量较大时,表格存储每次请求仅会扫描一次(行数大于5000或者大小大于4 MB停止扫描),超过限制的数据不会继续返回,需要通过翻页继续获取后面的数据
4. nextStartPrimaryKey: 根据返回结果中的nextStartPrimaryKey判断数据是否全部读取。当返回结果中nextStartPrimaryKey不为空时,可以使用此返回值作为下一次GetRange操作的起始点继续读取数据。当返回结果中nextStartPrimaryKey为空,表示读取范围内的数据全部返回。
5. GetRange操作遵循最左匹配原则,读取数据时,依次比较第一主键列到第四主键列。例如数据表的主键包括PK1、PK2、PK3三个主键列,读取数据时,优先比较PK1是否在开始主键与结束主键的范围内,如果PK1在设置的主键范围内,- 则不会再比较其他的主键,返回在PK1主键范围内的数据;如果PK1在设置的主键边界上,则继续比较PK2是否在开始主键与结束主键的范围内,以此类推。关于范围查询原理的更多信息,请参见GetRange范围查询详解。
GetRange范围查询详解链接:https://developer.aliyun.com/article/742095?spm=a2c4g.11186623.0.0.87da5f86e53d3C
6.GetRange操作可能在如下情况停止执行并返回数据。
- 扫描的行数据大小之和达到4 MB。 - 扫描的行数等于5000。 - 返回的行数等于最大返回行数。 - 当前剩余的预留读吞吐量已全部使用,余量不足以读取下一条数据。 - 当使用GetRange扫描的数据量较大时,表格存储每次请求仅会扫描一次(行数大于5000或者大小大于4 MB停止扫描),超过限制的数据不会继续返回,需要通过翻页继续获取后面的数据。
7. 如果需要获取写入数据后系统自动生成的自增列的值,将ReturnType设置为RT_PK,可以在数据写入成功后返回自增列的值。
表格存储说明
SQL 还是 NoSQL?
NoSQL 是一个用于描述高扩展高性能的非关系数据库的术语。 NoSQL 数据库提供的 schemafree 数据模型能够让应用无需预先定义表结构,适应业务的多元化发展,而对超大数据规模和高并发的支持让 NoSQL 数据库得到了广泛的应用。
SQL 与 NoSQL 数据库对比
关系型数据库 | NoSQL 数据库 | |
---|---|---|
数据模型 | 关系模型对数据进行了规范化,严格的定义了表、列、索引、表之间的关系及其他数据库元素,使一张数据表的所有数据具有相同的结构。 | 非关系(NoSQL)数据库一般不会对表的结构进行严格的定义,一般使用分区键及键值来检索值、列集或者半结构化数据。 |
ACID | 传统关系型数据库支持由 ACID (原子性、一致性、隔离性和持久性)定义的一组属性。其原子性体现在一个事务"全部成功或者全部失败",即完全执行成功或完全不执行某项事务。一致性表示数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。隔离性要求并发事务应分别执行,互不干扰。持久性即一旦事务提交后,它所做的修改将会永久的保存在数据库上,即使出现宕机也不会丢失。 | 为了获得更为灵活的可水平扩展的数据模型, NoSQL 数据库通常会放弃传统关系数据库的部分 ACID 属性。凭借这些特性,NoSQL数据库可用来克服一系列包括性能瓶颈、可扩展性、运营复杂性以及不断增加的管理和支持成本的问题,这也让 NoSQL 数据库成了传统关系型数据库在面临海量数据及高并发挑战时的最佳选择。 |
性能 | 性能一般取决于磁盘子系统、数据集大小以及查询优化、索引和表结构。 | 写性能通常受限于磁盘子系统,读性能则受限于结果集的大小 |
扩展 | 进行纵向扩展最简单的方式是使用更快的CPU、磁盘等硬件设备。要获得跨分布式系统的关系表,就需要增加使用成本及技术复杂度。 | 能够利用低成本硬件的分布式集群进行横向扩展,从而在不增加延迟的前提下提高吞吐量和数据规模。 |
API | 对存储和检索数据的请求由符合结构化查询语言 (SQL) 的查询来传达。这些查询由关系数据库系统来解析和执行。 | 应用开发人员可以使用NoSQL数据库开放的 API 轻松存储和检索数据。通过分区键及键值,应用可以查找键值对、列集或者半结构化数据。 |
为什么使用表格存储?
表格存储(TableStore)是 NoSQL 数据库的一种,提供海量 NoSQL 数据存储错误,支持 schemafree 的数据模型,提供单行级别的事务,服务端自动对数据进行分区和负载均衡,让单表数据从 GB 到 TB 再到 PB__,访问并发从__0__至__百万__都无需繁琐的扩容流程,写性能在 __TB 及 PB 级数据规模都能保持在单个毫秒,读性能只依赖结果数据集,而不受数据量的影响。
所以相比 OLTP(联机事务处理)场景,表格存储更适用于 Web 规模级应用程序,包括社交网络、游戏、媒体共享和 IoT(物联网)、日志监控等场景。
访问数据库
与传统关系型数据库不同,客户端都是通过http协议来访问表格存储。
客户端使用 Restful API 通过 HTTP 数据包来访问表格存储,表格存储服务端会对数据报文中的签名信息进行验证,详细请参考使用表格存储的 API,如果使用官网的SDK,则只需要提供所访问的表格存储endpoint、实例、AK信息即可调用SDK的接口对数据进行操作。
表格存储通过客户端来访问数据,客户端初始化方式如下:
public static string Endpoint = "https://xxxx.cn-shanghai.ots.aliyuncs.com"; public static string InstanceName = "xxx"; /// <summary> /// 获取阿里云ots客户端 /// </summary> /// <returns></returns> public static OTSClient GetOTSClient() { var config = new OTSClientConfig(Endpoint, AliyunParm.AccessKey, AliyunParm.AccessSecret, InstanceName); // 禁止输出日志,默认是打开的。 config.OTSDebugLogHandler = null; config.OTSErrorLogHandler = null; // 使用OTSClientConfig创建一个OtsClient对象。 var otsClient = new OTSClient(config); return otsClient; }
客户端初始化需要填入如下参数:
endPoint:访问的表格存储实例的url地址
accessKeyId:访问表格存储所使用的accessKeyId
accessKeySecret:访问表格存储的所使用的accessKeySecret
instanceName:访问的表格存储的实例名称
创建表
表是关系数据库和表格存储中的基本数据结构。关系数据库创建表时就需要定义完整的数据结构。相比之下,表格存储的数据表则只需要定义主键信息。
SQL
使用 CREATE TABLE 语句创建表,如以下示例所示。
CREATE TABLE UserHistory ( user_id VARCHAR(20) NOT NULL, time_stamp INT NOT NULL, item_id VARCHAR(50), behavior_type VARCHAR(10), behavior_amount DOUBLE, behavior_count INT, content VARCHAR(100), PRIMARY KEY(UserId, TimeStamp) );
此表的主键包含 pk0 和 time_stamp,在创建数据表是必须严格的定义所有的主键和属性列,如有需要,需要使用 ALTER TABLE 语句更改这些定义。
表格存储
使用表格存储创建数据表并指定如下参数,如下所示:
public CreateTableResponse CreateTable(string TableName) { var otsClient = GetOTSClient(); var primaryKeySchema = new PrimaryKeySchema(); primaryKeySchema.Add("user_id", ColumnValueType.String); primaryKeySchema.Add("time_stamp", ColumnValueType.Integer); //通过表名和主键列的schema创建一个tableMeta。 var tableMeta = new TableMeta(TableName, primaryKeySchema); //设置预留读吞吐量为0,预留写吞吐量为0。 var reservedThroughput = new CapacityUnit(0, 0); try { //构造CreateTableRequest对象。 var request = new CreateTableRequest(tableMeta, reservedThroughput); //allowUpdate为false时,表示禁止UpdateRow相关更新写入操作。 request.TableOptions.AllowUpdate = false; //数据生命周期至少为86400秒(一天)或-1(数据永不过期)。 request.TableOptions.TimeToLive = -1; //调用client的CreateTable接口,如果没有抛出异常,则说明执行成功。 return otsClient.CreateTable(request); } //如果抛出异常,则说明失败,处理异常。 catch (Exception ex) { Console.WriteLine("Create table failed, exception:{0}", ex.Message); } return null; }
此表的主键包含 user_id 和 time_stamp,需要提供的参数主要有:
- TABLE_NAME:表名称
- PrimaryKeySchema:主键的名称及类型
- timeToLive:数据表的数据过期时间
写入数据
SQL
关系数据库中,表是一个由行和列组成的二维数据结构,可以使用 INSERT 语句向表中添加行:
INSERT INTO UserHistory ( user_id, time_stamp, item_id, behavior_type, behavior_amount, behavior_count, content) VALUES( '10100', 1479265526, 'cell_phone', 'share', 4.9, 78, 'The phone is quit good!' );
表格存储
使用表格存储中可以使用 PutRow 接口插入一行数据:
public static void InsertData(string tableName) { var otsClient = GetOTSClient(); //定义行的主键,必须与创建表时的TableMeta中定义的一致。 var primaryKey = new PrimaryKey(); primaryKey.Add("user_id", new ColumnValue("10086")); primaryKey.Add("time_stamp", new ColumnValue(1629160713)); //定义要写入该行的属性列。 var attribute = new AttributeColumns(); attribute.Add("item_id", new ColumnValue("item_id")); attribute.Add("behavior_type", new ColumnValue("behavior_type")); attribute.Add("behavior_amount", new ColumnValue(1.3)); attribute.Add("behavior_count", new ColumnValue(88)); attribute.Add("content", new ColumnValue("内容")); try { //构造插入数据的请求对象,RowExistenceExpectation.IGNORE表示无论此行是否存在均会插入新数据。 var request = new PutRowRequest(tableName, new Condition(RowExistenceExpectation.IGNORE), primaryKey, attribute); //调用PutRow接口插入数据。 otsClient.PutRow(request); //如果没有抛出异常,则说明执行成功。 Console.WriteLine("Put row succeeded."); } catch (Exception ex) { //如果抛出异常,则说明执行失败,处理异常。 Console.WriteLine("Put row failed, exception:{0}", ex.Message); } }
使用 PutRow 接口需要了解一下几个关键事项:
- 除了表名 TABLE_NAME 和主键 primaryKey,属性列和类型可以在写入时定义
- 多行数据的同名属性列也可以使用不同的类型
- 大多数 SQL 数据库是面向事务的,当发出 INSERT 语句时,只有 COMMIT 之后对数据的修改才是永久性的。使用表格存储,当表格存储通过 HTTP 200 状态码(OK)进行回复时, PutRow 写入的数据已经被持久化到所有备份。
- 多条记录的插入可以使用 BatchWriteRow 接口,可以大大提高数据的写入速度。
检索数据
SQL
SQL SELECT 语句可以查询关键列、非关键列或任意组合。WHERE 子句确定返回的行,如以下示例所示:
// 根据主键查询一行 SELECT * FROM UserHistory WHERE user_id = '10100' AND time_stamp = 1479265526; // 查询某个user_id下的所有数据 SELECT * FROM UserHistory WHERE user_id = '10100'; // 根据某个user_id下的某段时间的所有记录 SELECT * FROM UserHistory WHERE user_id = '10100' and time_stamp > 1478660726 AND time_stamp < 1479265526; // 查询某个user_id所有收藏的记录 SELECT * FROM UserHistory WHERE user_id = '10100' AND behavior_type = 'collect';
表格存储
表格存储中的数据查询接口可以以类似的方式检索数据,单行查询 GetRow 和范围查询 GetRange能够提供对存储数据物理位置的快速高效访问,查询的性能只受到结果数据集大小的影响,不会受到表中数据总量大小的影响。
提供完整的主键信息,可以使用 GetRow 快速查询这行数据。
// SELECT * FROM UserHistory WHERE user_id = '10100' AND time_stamp = 1479265526 // 设置主键信息 PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); primaryKeyBuilder.addPrimaryKeyColumn('user_id', PrimaryKeyValue.fromString("10100")); primaryKeyBuilder.addPrimaryKeyColumn('time_stamp', PrimaryKeyValue.fromLong(1479265526)); PrimaryKey primaryKey = primaryKeyBuilder.build(); // 读一行 SingleRowQueryCriteria criteria = new SingleRowQueryCriteria(TABLE_NAME, primaryKey); // 设置读取最新版本 criteria.setMaxVersions(1); GetRowResponse getRowResponse = client.getRow(new GetRowRequest(criteria));
使用 GetRange 对某个 user_id 下所有的数据进行查询:
// 等同于 SELECT * FROM UserHistory WHERE user_id = '10100' RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(TABLE_NAME); // 设置起始主键 PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); primaryKeyBuilder.addPrimaryKeyColumn("user_id", PrimaryKeyValue.fromString("10100")); primaryKeyBuilder.addPrimaryKeyColumn("time_stamp", PrimaryKeyValue.INF_MIN); rangeRowQueryCriteria.setInclusiveStartPrimaryKey(primaryKeyBuilder.build()); // 设置结束主键 primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); primaryKeyBuilder.addPrimaryKeyColumn("user_id", PrimaryKeyValue.fromString("10100")); primaryKeyBuilder.addPrimaryKeyColumn("time_stamp", PrimaryKeyValue.INF_MAX); rangeRowQueryCriteria.setExclusiveEndPrimaryKey(primaryKeyBuilder.build()); // 设置读取最新版本 rangeRowQueryCriteria.setMaxVersions(1); // 默认读取所有的属性列 GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
需要了解的几个关键事项:
- GetRange 需要指定所有主键的起始范围,但每个主键的范围并不是 AND 的关系,而是以第一个主键到最后一个主键为顺序,优先比较前面的主键,当前面的主键在 GetRange 起止主键范围内,则该条数据就会被读取出来。比如两个主键起止范围为('a',5)~('c',10),由于 'a' < 'b' < 'c' ,所以主键为('b', 4)的数据也符合要求。
- INF_MIN 和 INF_MAX 为 GetRange 操作专用类型,分别表示最小值和最大值。
- GetRange 支持 limit 及 direction 来控制结果集行数和读取的顺序。
- 为了防止网络延迟, GetRange 对一次返回的结果集进行了限制,需要对 Response 中的 next_start_primary_key 进行判断,为空时表示结果已经全部返回,不为空需要继续读取。
- GetRange 支持过滤器功能。
- 表格存储支持数据多版本功能,在使用 GetRow 和 GetRange 接口时可以指定读取属性列的历史版本范围。
更多 GetRange 信息也可以参考:表格存储数据模型和查询操作
使用 GetRange 对某个 user_id 下某段时间范围的所有的数据进行查询:
RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(TABLE_NAME); // 设置起始主键 PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); primaryKeyBuilder.addPrimaryKeyColumn("user_id", PrimaryKeyValue.fromString("10100")); primaryKeyBuilder.addPrimaryKeyColumn("time_stamp", PrimaryKeyValue.fromLong(1478660726)); rangeRowQueryCriteria.setInclusiveStartPrimaryKey(primaryKeyBuilder.build()); // 设置结束主键 primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); primaryKeyBuilder.addPrimaryKeyColumn("user_id", PrimaryKeyValue.fromString("10100")); primaryKeyBuilder.addPrimaryKeyColumn("time_stamp", PrimaryKeyValue.fromLong(1479265526)); rangeRowQueryCriteria.setExclusiveEndPrimaryKey(primaryKeyBuilder.build()); // 设置读取最新版本 rangeRowQueryCriteria.setMaxVersions(1); // 默认读取所有的属性列 GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
该查询等同于
SELECT * FROM UserHistory WHERE user_id = '10100' AND time_stamp > 1478660726 AND time_stamp < 1479265526;
如果需要继续对属性列做条件查询,可以使用 过滤器功能, 如下查询某个 user_id 下某所有的收藏记录:
RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(TABLE_NAME); // 设置起始主键 PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); primaryKeyBuilder.addPrimaryKeyColumn("user_id", PrimaryKeyValue.fromString("10100")); primaryKeyBuilder.addPrimaryKeyColumn("time_stamp", PrimaryKeyValue.INF_MIN); rangeRowQueryCriteria.setInclusiveStartPrimaryKey(primaryKeyBuilder.build()); // 设置结束主键 primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); primaryKeyBuilder.addPrimaryKeyColumn("user_id", PrimaryKeyValue.fromString("10100")); primaryKeyBuilder.addPrimaryKeyColumn("time_stamp", PrimaryKeyValue.INF_MAX); rangeRowQueryCriteria.setExclusiveEndPrimaryKey(primaryKeyBuilder.build()); // 设置属性列的过滤条件: behavior_type = 'collect' SingleColumnValueFilter filter = new SingleColumnValueFilter("behavior_type", SingleColumnValueFilter.CompareOperator.EQUAL, ColumnValue.fromString("collect")); // 表格存储是 schemafree 模型,有些行不包括属性列 behavior_type // 设置为 false 表示如果该行没有属性列 behavior_type,则不满足条件条件 filter.setPassIfMissing(false); rangeRowQueryCriteria.setFilter(filter); // 设置读取最新版本 rangeRowQueryCriteria.setMaxVersions(1); // 默认读取所有的属性列 GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
该查询等同于
SELECT * FROM UserHistory WHERE user_id = '10100' AND behavior_type = 'collect';
需要了解的几个关键事项:
- 过滤器 Filter 可以支持最多10个条件组合,可以用于 GetRow、BatchGetRow 和 GetRange 接口中。
- 过滤器 Filter 是对GetRange的数据在服务端进行过滤,并不会减少磁盘的IO次数,但是能够有效的降低网络传输流量。
- 良好的主键设计能够大大提交范围查询的效率。
更新数据
SQL
关系数据库中,可以使用 UPDATE 语句对表中的一行或者多行记录进行修改:
UPDATE UserHistory SET behavior_type = 'collect' WHERE user_id = '10100' AND time_stamp = 1479265526 AND behavior_count > 4.0;
表格存储
使用表格存储中可以使用 UpdateRow 接口更新一行数据:
使用UpdateRow接口需要了解一下几个关键事项:
- DeleteRow 需要指定表名 TABLE_NAME 和全部的主键 primaryKey ,更新的列可以存在或者不存在
- UpdateRow 只会修改这一行中需要修改的列,PutRow则会使用新的数据覆盖原来整行的数据
- 更新可以设置两种条件检查:行存在检查和条件检查
- SQL中 UPDATE 会对满足 WHERE 的所有记录做更新,表格存储的 UpdateRow 只更新指定主键的一行数据,条件检查也只针对本行的主键列或者属性列做检查。
- 多条记录的更新可以使用 BatchWriteRow 接口,可以大大提高数据的写入速度
删除表
SQL
关系数据库中,可以使用 DROP TABLE语句来删除不再需要的数据表
drop table tableName
表格存储
使用表格存储中可以使用 DeleteTable 接口删除数据表:
public static void DeleteOtsTable(string tableName) { var request = new DeleteTableRequest(tableName); try { GetOTSClient().DeleteTable(request); } catch (Exception ex) { Console.WriteLine("Delete table failed, exception:{0}", ex.Message); } }
参考:https://www.cnblogs.com/xudong5273/p/17640499.html
详情见阿里云NetSDK:https://help.aliyun.com/zh/tablestore/developer-reference/dotnet-sdk/?spm=a2c4g.11186623.0.0.41747189nhEkFd