表格存储(Tablestore)入门指南
内容简介
表格存储(Tablestore)是阿里云自研的 NoSQL 多模型数据库,提供海量结构化数据存储以及快速的查询和分析服务。表格存储的分布式存储和强大的索引引擎能够提供 PB 级存储、千万 TPS 以及毫秒级延迟的服务能力。
本文将以一个简单的学生成绩查询demo为例,向大家演示如何使用Tablestore提供的Java SDK实现一个表格数据的导入、查询(主键索引、二级索引和多元索引)和导出。
demo中表格(table)的存储信息格式如下:
ID(主键) | 班级(主键) | 姓名(主键) | 语文 | 数学 | 英语 |
---|---|---|---|---|---|
0 | 初三1班 | 云知 | 99 | 99 | 99 |
主要实现的功能如下:
- 创建学生成绩的数据表格;
- 添加学生成绩信息;
- 查询学生成绩信息;
- 更新学生成绩信息;
- 删除学生成绩信息;
- 查询特定班级的学生成绩信息(二级索引);
- 查询多科目特定分段的学生成绩信息(多元索引);
- 导出新增学生成绩信息(通道服务)。
一、配置开发环境
Tablestore的Java SDK开发环境非常简单,主要包括:
* 了解并开通阿里云表格存储服务
* 创建AccessKey
* 配置Java环境(建议:JDK 6及以上版本)
* 使用Maven项目加入依赖项
<dependencies>
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>tablestore</artifactId>
<version>4.12.1</version>
</dependency>
</dependencies>
二、表格的一般操作
1、获取实例的控制权限
首先创建实例,实例是表格存储资源管理的基础单元,表格存储对应用程序的访问控制和资源计量都在实例级别完成。
实例创建成功后,可以通过“SyncClient client”获取实例控制权柄,然后通过client实现对实例的各种控制。
/**
* 以下信息可由阿里云控制台获取,依次为:
* endPoint(实例访问地址)
* accessId(AccessKey ID)
* accessKey(Access Key Secret)
* instanceName(实例名称,用户创建实例时指定)
*/
private static final String endPoint = "https://xxxx.cn-hangzhou.ots.aliyuncs.com";
private static final String accessId = "LTAIxxxebxxxKIxx";
private static final String accessKey = "5P7xxxMm1xxxhHPxxxXnNxxxXscxxx";
private static final String instanceName = "instance-1";
SyncClient client = new SyncClient(endPoint, accessId, accessKey, instanceName);
2、创建学生成绩的数据表格
在创建学生成绩的数据表格时需要描述表的结构信息(TableMeta)和配置信息(TableOptions)。
public static void creatTable(){
System.out.println("开始创建表格。");
//描述表的结构信息
TableMeta tableMeta = new TableMeta(tableName);
//添加主键列
tableMeta.addPrimaryKeyColumn(new PrimaryKeySchema("ID", PrimaryKeyType.INTEGER));
tableMeta.addPrimaryKeyColumn(new PrimaryKeySchema("班级", PrimaryKeyType.INTEGER));
tableMeta.addPrimaryKeyColumn(new PrimaryKeySchema("姓名", PrimaryKeyType.STRING));
//添加属性列
tableMeta.addDefinedColumn(new DefinedColumnSchema("语文", DefinedColumnType.INTEGER));
tableMeta.addDefinedColumn(new DefinedColumnSchema("数学", DefinedColumnType.INTEGER));
tableMeta.addDefinedColumn(new DefinedColumnSchema("英语", DefinedColumnType.INTEGER));
int timeToLive = -1; // 数据的过期时间, 单位秒, -1代表永不过期. 假如设置过期时间为一年, 即为 365 * 24 * 3600.
int maxVersions = 1; // 最大保存版本数, maxVersions大于1时, 无法使用二级索引和多元索引功能.
//描述表的配置信息
TableOptions tableOptions = new TableOptions(timeToLive, maxVersions);
//将表的结构信息和配置信息封装到一个request里
CreateTableRequest request = new CreateTableRequest(tableMeta, tableOptions);
//创建表格
try {
client.createTable(request);
System.out.println("创建表格成功。");
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
}
3、添加学生成绩信息
PutRow 接口用于插入一行数据,如果原来该行已经存在,会覆盖原来的一行。
明确主键信息和属性信息,我们便可以使用PutRow接口将该信息添加入表中。
public static void creatRow(){
//描述主键信息
PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
primaryKeyBuilder.addPrimaryKeyColumn("ID", PrimaryKeyValue.fromLong(3));
primaryKeyBuilder.addPrimaryKeyColumn("班级", PrimaryKeyValue.fromLong("初三2班"));
primaryKeyBuilder.addPrimaryKeyColumn("姓名", PrimaryKeyValue.fromLong("云知"));
PrimaryKey primaryKey = primaryKeyBuilder.build();
RowPutChange rowPutChange = new RowPutChange(tableName, primaryKey);
//描述属性信息
rowPutChange.addColumn(new Column("语文", ColumnValue.fromLong(rand.nextInt(99))));
rowPutChange.addColumn(new Column("数学", ColumnValue.fromLong(rand.nextInt(99))));
rowPutChange.addColumn(new Column("英语", ColumnValue.fromLong(rand.nextInt(99))));
//添加新数据到表格
try {
client.putRow(new PutRowRequest(rowPutChange));
System.out.println("添加数据成功。");
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
}
4、查询学生成绩信息
查询单条学生成绩信息
通过描述主键信息,可以在数据库中唯一的确认一条学生信息。
以此主键为索引,可以使用 GetRow 接口读取一行数据。
在描述主键信息时,必须保证主键信息的完整性(即,包含所有主键的对应值),因为只有这样才能保证信息在表内是唯一的。
public static void retrieveRow() {
//构建主键
PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
primaryKeyBuilder.addPrimaryKeyColumn("ID", PrimaryKeyValue.fromLong(3));
primaryKeyBuilder.addPrimaryKeyColumn("班级", PrimaryKeyValue.fromLong("初三2班"));
primaryKeyBuilder.addPrimaryKeyColumn("姓名", PrimaryKeyValue.fromLong("云知"));
PrimaryKey primaryKey = primaryKeyBuilder.build();
//读取一行
SingleRowQueryCriteria rowQueryCriteria = new SingleRowQueryCriteria(tableName, primaryKey);
//设置读取最新版本
rowQueryCriteria.setMaxVersions(1);
try {
GetRowResponse getRowResponse = client.getRow(new GetRowRequest(rowQueryCriteria));
Row row = getRowResponse.getRow();
System.out.println("查询单条数据成功。");
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
}
查询多条学生成绩信息
由于主键是有序的,因此可以通过构建Min主键和Max主键,在表内唯一的限定一部分学生信息并将其返回。
下面这个例子,是返回“ID”在50-100之间的学生信息。
public static void retrieveRangeRow() {
//构建Min主键
PrimaryKeyBuilder primaryMinKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
primaryMinKeyBuilder.addPrimaryKeyColumn("ID", PrimaryKeyValue.fromLong(50));
primaryMinKeyBuilder.addPrimaryKeyColumn("班级", PrimaryKeyValue.INF_MIN);
primaryMinKeyBuilder.addPrimaryKeyColumn("姓名", PrimaryKeyValue.INF_MIN);
PrimaryKey primaryMinKey = primaryMinKeyBuilder.build();
//构建Max主键
PrimaryKeyBuilder primaryMaxKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
primaryMaxKeyBuilder.addPrimaryKeyColumn("ID", PrimaryKeyValue.fromLong(101));
primaryMaxKeyBuilder.addPrimaryKeyColumn("班级", PrimaryKeyValue.INF_MAX);
primaryMaxKeyBuilder.addPrimaryKeyColumn("姓名", PrimaryKeyValue.INF_MAX);
PrimaryKey primaryMaxKey = primaryMaxKeyBuilder.build();
RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(tableName);
rangeRowQueryCriteria.setInclusiveStartPrimaryKey(primaryMinKey);
rangeRowQueryCriteria.setExclusiveEndPrimaryKey(primaryMaxKey);
rangeRowQueryCriteria.setMaxVersions(1);
try {
GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
List<Row> rows = getRangeResponse.getRows();
System.out.println("查询多条数据成功。");
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
}
5、修改学生成绩信息
准确描述主键信息后,我们可以定位到数据表内的唯一数据信息,并修改它。
UpdateRow 接口用于更新一行数据,如果原行不存在,会新写入一行。
public static void updataRow(){
//构建主键
PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
primaryKeyBuilder.addPrimaryKeyColumn("ID", PrimaryKeyValue.fromLong(3));
primaryKeyBuilder.addPrimaryKeyColumn("班级", PrimaryKeyValue.fromLong("初三2班"));
primaryKeyBuilder.addPrimaryKeyColumn("姓名", PrimaryKeyValue.fromLong("云知"));
PrimaryKey primaryKey = primaryKeyBuilder.build();
RowPutChange rowPutChange = new RowPutChange(tableName, primaryKey);
//构建属性列
rowPutChange.addColumn(new Column("语文", ColumnValue.fromLong(rand.nextInt(100))));
rowPutChange.addColumn(new Column("数学", ColumnValue.fromLong(rand.nextInt(100))));
rowPutChange.addColumn(new Column("英语", ColumnValue.fromLong(rand.nextInt(100))));
//添加新数据到表格
try {
client.updateRow(new UpdateRowRequest(rowUpdateChange));
System.out.println("更新数据成功。");
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
}
6、删除学生成绩信息
准确描述主键信息后,我们可以定位到数据表内的唯一数据信息,并使用DeleteRow 接口删除它。
private static void deleteRow(SyncClient client, String pkValue) {
//构建主键
PrimaryKeyBuilder primaryKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
primaryKeyBuilder.addPrimaryKeyColumn("ID", PrimaryKeyValue.fromLong(3));
primaryKeyBuilder.addPrimaryKeyColumn("班级", PrimaryKeyValue.fromLong("初三2班"));
primaryKeyBuilder.addPrimaryKeyColumn("姓名", PrimaryKeyValue.fromLong("云知"));
PrimaryKey primaryKey = primaryKeyBuilder.build();
RowDeleteChange rowDeleteChange = new RowDeleteChange(tableName, primaryKey);
try {
GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
client.deleteRow(new DeleteRowRequest(rowDeleteChange));
}
7、删除表
public static void deleteTable(){
DeleteTableRequest request = new DeleteTableRequest(tableName);
try {
client.deleteTable(request);
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
}
三、二级索引
在我们的demo中,我们使用 “ID+班级+姓名”这三个主键来唯一的确定一条学生成绩信息。
由于主键是顺序存储在数据库中的,所以我们可以使用retrieveRangeRow()函数,通过在其中设置“Min主键”和“Max主键”的方式,对ID进行范围查询。
但是当我们期望获取某个范围的班级的时候,因为“班级”并不是主键存储的第一索引,这个思路便失效了。
Tablestore提供了二级索引,以便于解决这个问题。
二级索引可以提供一份以新的关键字(原表的主键和属性皆可)为第一索引的数据表,方便用户使用范围查询去查询这个关键字对应的数据信息。
1、创建二级索引
创建二级索引主要是声明一个新的主键,下面的例子就是将“班级”声明为新的二级索引的主键,最终形成“班级+ID+姓名”的索引表。
public static void createIndex() {
System.out.println("indexName = "+indexName);
IndexMeta indexMeta = new IndexMeta(indexName); // 要创建的索引表名称。
indexMeta.addPrimaryKeyColumn("班级"); // 为索引表添加主键列。
CreateIndexRequest request = new CreateIndexRequest(tableName, indexMeta, true);
try {
client.createIndex(request);
System.out.println("创建索引成功。");
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
}
2、查询特定班级的学生成绩信息
以下使用新声明的二级索引查询“班级”为初二1班、初二2班和初二3班的学生成绩信息。
思路同基本的范围查询相同:声明Min,Max主键值,然后在二级索引表中查询到该范围内的所有信息的主键值,再返回到原表查询完整的学生信息即可。
public static void getRangeRow(){
System.out.println("开始查询数据。");
//构建Min主键
PrimaryKeyBuilder primaryLKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
primaryLKeyBuilder.addPrimaryKeyColumn("班级", PrimaryKeyValue.fromString("初二1班"));
primaryLKeyBuilder.addPrimaryKeyColumn("ID", PrimaryKeyValue.INF_MIN);
primaryLKeyBuilder.addPrimaryKeyColumn("姓名", PrimaryKeyValue.INF_MIN);
PrimaryKey startPrimaryKey = primaryLKeyBuilder.build();
//构造Max间主键
PrimaryKeyBuilder primaryRKeyBuilder = PrimaryKeyBuilder.createPrimaryKeyBuilder();
primaryRKeyBuilder.addPrimaryKeyColumn("班级", PrimaryKeyValue.fromString("初二3班"));
primaryRKeyBuilder.addPrimaryKeyColumn("ID", PrimaryKeyValue.INF_MAX);
primaryRKeyBuilder.addPrimaryKeyColumn("姓名", PrimaryKeyValue.INF_MAX);
PrimaryKey endPrimaryKey = primaryRKeyBuilder.build();
RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(indexName);
rangeRowQueryCriteria.setInclusiveStartPrimaryKey(startPrimaryKey);
rangeRowQueryCriteria.setExclusiveEndPrimaryKey(endPrimaryKey);
rangeRowQueryCriteria.setMaxVersions(1);
try {
GetRangeResponse getRangeResponse = client.getRange(new GetRangeRequest(rangeRowQueryCriteria));
List<Row> rows = getRangeResponse.getRows();
System.out.println("查询到的行数"+rows.size());
for (int i = 0; i < rows.size(); i++) {
long index = rows.get(i).getPrimaryKey().getPrimaryKeyColumn("ID").getValue().asLong();
Fmt.PrintRow(rows.get(i));
}
} catch (TableStoreException e) {
System.err.println("操作失败,详情:" + e.getMessage());
System.err.println("Request ID:" + e.getRequestId());
} catch (ClientException e) {
System.err.println("请求失败,详情:" + e.getMessage());
}
}
3、删除二级索引
我们可以通过表名字和二级索引名来唯一的确定一个二级索引,然后通过DeleteIndexRequest(String tableName, String indexName)函数,将其删除。
public static void deleteIndex() {
DeleteIndexRequest request = new DeleteIndexRequest(tableName, indexName); // 要删除的索引表及主表名
client.deleteIndex(request);
}
四、多元索引
当我们要对多个字段进行联合查询的时候,就很难通过前边的思路找到一个切实可行的方案。
因此Tablestore提供了多元索引,可以让我们轻松实现多种高效的索引结构解决大数据的复杂查询难题。
下面,我们将使用多元索引去实现对三科成绩同时限定区间的查询(例如,三科成绩都大于60分的人数),更多的实现可以查阅官方多元索引开发指南
1、创建多元索引
我们可以在一张表上创建多个多元索引,在创建多元索引时可以指定索引名和索引结构。
public static void createSearchIndex(){
CreateSearchIndexRequest request = new CreateSearchIndexRequest();
request.setTableName(tableName);
request.setIndexName(searchIndexName);
IndexSchema indexSchema = new IndexSchema();
indexSchema.setFieldSchemas(Arrays.asList(
new FieldSchema("语文", FieldType.LONG).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("数学", FieldType.LONG).setIndex(true).setEnableSortAndAgg(true),
new FieldSchema("英语", FieldType.LONG).setIndex(true).setEnableSortAndAgg(true)));
request.setIndexSchema(indexSchema);
client.createSearchIndex(request);
System.out.println("创建SearchIndex成功。");
}
2、查询多科目特定分段的学生成绩信息
我们使用BoolQuery进行多条件组合查询,在其中包含了三个子查询条件:rangeChineseQuery,rangeMathQuery和rangeEnglishQuery。
将他们封装到BoolQuery中形成一个组合查询条件,查询三个成绩均大于60分的学生成绩信息。
public static void boolSearchQuery(){
RangeQuery rangeChineseQuery = new RangeQuery(); // 设置查询类型为RangeQuery
rangeChineseQuery.setFieldName("语文"); // 设置针对哪个字段
rangeChineseQuery.greaterThan(ColumnValue.fromLong(60)); // 设置语文成绩大于60
RangeQuery rangeMathQuery = new RangeQuery(); // 设置查询类型为RangeQuery
rangeMathQuery.setFieldName("数学"); // 设置针对哪个字段
rangeMathQuery.greaterThan(ColumnValue.fromLong(60)); // 设置数学成绩大于60
RangeQuery rangeEnglishQuery = new RangeQuery(); // 设置查询类型为RangeQuery
rangeEnglishQuery.setFieldName("英语"); // 设置针对哪个字段
rangeEnglishQuery.greaterThan(ColumnValue.fromLong(60)); // 设置英语成绩大于60
SearchQuery searchQuery = new SearchQuery();
BoolQuery boolQuery = new BoolQuery();
boolQuery.setMustQueries(Arrays.asList(rangeChineseQuery, rangeMathQuery, rangeEnglishQuery));
searchQuery.setQuery(boolQuery);
searchQuery.setGetTotalCount(true);
SearchRequest searchRequest = new SearchRequest(tableName, searchIndexName, searchQuery);
SearchResponse resp = client.search(searchRequest);
System.out.println("Row: " + resp.getRows());
}
3、删除多元索引
我们可以通过表名字和多元索引名来唯一的确定一个多元索引,然后通过clinet.deleteSearchIndex(DeleteSearchIndexRequest request)函数,将其删除。
public static void deleteSearchIndex() {
DeleteSearchIndexRequest request = new DeleteSearchIndexRequest();
request.setTableName(tableName);
request.setIndexName(searchIndexName);
client.deleteSearchIndex(request);
System.out.println("删除SearchIndex成功。");
}
五、通道服务
1、创建通道
通道的类型主要分为三类:全量(BaseData)、增量(Stream)和全量加增量(BaseAndStream)。
我们可以通过设置通道类型,限定通道获取数据的范围。
TunnelClient tunnelClient = new TunnelClient(endPoint, accessId, accessKey, instanceName);
public static void createTunnel() {
CreateTunnelRequest request = new CreateTunnelRequest(tableName, tunnelName, TunnelType.BaseData);
CreateTunnelResponse resp = tunnelClient.createTunnel(request);
System.out.println("RequestId: " + resp.getRequestId());
System.out.println("TunnelId: " + resp.getTunnelId());
}
2、导出学生信息
用户调用printAllRow()函数可以开启一个通道服务,使得通道自动调用process()函数内的逻辑。
public static class PrintProcessor implements IChannelProcessor {
@Override
public void process(ProcessRecordsInput input) {
System.out.println("Default record processor, would print records:");
System.out.println(input.getRecords());
try {
// Mock Record Process.
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void shutdown() {
System.out.println("Mock shutdown");
}
}
public static void printAllRow(){
ListTunnelRequest request = new ListTunnelRequest(tableName);
ListTunnelResponse resp = tunnelClient.listTunnel(request);
// tunnelId会用于后续TunnelWorker的初始化, 该值同样可以通过ListTunnel或者DescribeTunnel获取。
String tunnelId = resp.getTunnelInfos().get(0).getTunnelId();
System.out.println(tunnelId);
TunnelWorkerConfig config = new TunnelWorkerConfig(new PrintProcessor());
TunnelWorker worker = new TunnelWorker(tunnelId, client, config);
try {
worker.connectAndWorking();
} catch (Exception e) {
e.printStackTrace();
worker.shutdown();
client.shutdown();
}
}
3、删除通道
public static void deleteTunnel() {
DeleteTunnelRequest request = new DeleteTunnelRequest(tableName, tunnelName);
DeleteTunnelResponse resp = tunnelClient.deleteTunnel(request);
System.out.println("RequestId: " + resp.getRequestId());
}