1 工作原理
Amazon DynamoDB 是一种完全托管式、无服务器的 NoSQL 键值数据库,旨在运行任何规模的高性能应用程序。
DynamoDB 提供内置安全性、连续备份、自动多区域复制、内存缓存和数据导入和导出工具。
可以以非常低的延迟支持 任何级别的负载 ,同时有着较完善的权限管理系统。
编辑
2 使用优势
- 简化运维:全托管模式,减少创建、维护数据库的开销;跨区域的分区存储,数据更安全;
- 自动扩容:数据量的增长对数据库性能没有太多影响
- 灵活的表结构:只需要定义分区键即可正常使用,随时增加减少字段
- 事件流:流结合 Lambda 可以非常方便的实现一些功能
3 基本使用
DynamoDB 的数据集合是一张表,表里的每一条数据就是一个文档,
文档中可以存储基本类型的数据(BOOLEAN, NUMBER, STRING)或者一些复杂的数据(MAP, LIST, SET),可以嵌套较为深的层级。
在创建一张表的时候,
需要指定一个基本类型的属性值作为 PK
再指定一个基本类型的属性值作为 SK
PK 的作用是将数据分散到不同的分区(Partition)里,构建无序的哈希索引
SK的作用是将同一个分区中的数据按照一定的顺序排列起来,便于查找
SK 中可以使用
==, <, >=, <=, begins with, between, contains, in
等函数来进行较为丰富的查询操作PK 必须使用准备的值进行查询
3.1 基本结构
- 表(Table)
- 文档(Document)
- 属性(Attribute)
- 分区键(Partition Key/Hash Key, 记为 PK,必需,数据类型必须是基本类型)
- 排序键(Sort Key/Range Key, 记为 SK,可选)
- 数据类型:
- 基本类型: number, string, binary, Boolean, and null
- 文档: list, map
- 集合: set
3.2 索引
DynamoDB 共有两种索引:
1 本地二级索引(Local Secondary Index,简称 LSI)
2 全局二级索引(Global Secondary Index,简称 GSI)
3.2.1 本地二级索引 (LSI)
本地二级索引和原表共用一份 Partition, 使用同一个 PK, 共用这一个 PK 的存储空间,上限为 10GB
LSI 提供了不同的筛选方式和排序方式,选用不同的属性作为 SK 可以提高查询的灵活性。
3.2.2 全局二级索引 (GSI)
全局二级索引相当于一个全新的表,除了与原表共用同一个表名之外,GSI 使用了新的 Partition Key 和 Sort Key, 有自己独立的 Partition, 计算读写数量时也与原表相互独立。
GSI 会在原表发生改变的时候,通过流(Stream)将更新同步到 GSI 中。一个表可以建立 20 个全局二级索引。
推荐使用全局二级索引,避免本地二级索引引起的读写容量的竞争,同时可以重新定义 PK 和 SK,支持更多的查询模式
3.3 一致性
DynamoDB 是数据库云服务,其在设计中着重考虑了数据的 可用性和 分区容忍性。从 CAP(Consistency, Availability, Partitioning)理论的角度来讲,DynamoDB 是一个 AP 的数据库。
在 DynamoDB 中,共有两个部分会涉及到读一致性的问题:主表读和GSI读。
主表读过程会产生一致性问题的原因是 DynamoDB 的 Partition 是有 三个副本的(实际为可用区)
默认情况下写入到两个副本中即认为写入成功,因此在高频读取时也有一定的概率读到未完成写入的数据。
可以通过配置 GetItem, Scan 等的操作一致性为 强一致性来避免这个问题,
但可能会带来更高的延迟(10ms级别)和吞吐量开销(强一致性为最终一致性吞吐量开销的二倍)。
GSI 读写会产生一致性的原因是 GSI 和主表实际是 两个不同的存储,写入到主表的数据会通过流同步到GSI,这个过程会存在一定的延时(10ms级别),因此在读取 GSI 中的数据时,不能设置为强一致性。
3.4 DynamoDB流
DynamoDB流是一项可选功能,它用于捕获DynamoDB表中的数据修改事件。有关这些事件的数据将按事件发生的顺序近乎实时的出现在流中。
每个事件由一条流记录表示,若对表启用了流,每当以下事件之一 发生时,DynamoDB流都会写入一条流记录:
- 如果向表中添加了新项目,流将捕获整个项目的映像(包括其所有属性)
- 如果更新了项目,流将捕获项目中任何已修改属性的"之前"和"之后"映像
- 如果从表中删除了项目,流将在整个项目被删除前捕获其映像
- 每条流记录还包含表名称、事件时间戳和其他元数据。流记录的有效事件为24小时,过此事件后记录将被自动删除
启用 DynamoDB Stream 之后,可以使用 Lambda 接收 Stream 并将数据填充到
- ElasticSearch 中便于进行复杂的查询,
- 或者同步数据
- 或者进行一些聚合计算
均可以通过 DynamoDB Stream 方便的完成。
3.5 表结构设计
DynamoDB 是需要提前考虑好一些查询方式,才能更好的设计表结构,否则可能会在使用的过程中遇到一些不便。
DynamoDB 限于底层结构设计,进行表扫描(Scan)的操作非常耗时,对于较大的表来说基本上是 不可接受的时间消耗,因此需要根据 DynamoDB 的特点对表结构进行巧妙的设计,以实现需要的查询组合。
因此,设计 DynamoDB 时:
第一步:是列举出可能的查询方式。当有了一组查询方式之后,便可以开始进行表结构的设计。
3.5.1 基本查询
在设计出查询方式之后,便可以优先关注使用量最多,查询方式最简单的查询需要。以下为主表中几种常见的查询方式:
- 使用 PK 直接查询:可以查出该分区键下的所有数据,适合于 一对多关系。例如查询一个用户下的多个订单记录,这时 PK 为用户的 ID
- 使用 PK + 完整 SK 进行查询:如果一个 PK 下的数据比较多,可以结合 SK 来实现更为精确的查询。例如查询一个用户下的一个特定的订单,这时 PK 为用户的 ID,SK 为订单的ID。
- 使用 PK + SK (==, <, >=, <=, begins with, between, contains, in) 进行查询:如果一个 PK 下数据比较多,同时每一类 SK 上有通用的前缀,可以用
begins with
来进行查询。例如查询用户的订单使用用户 ID 和 SK 前缀order:
进行查询。
3.5.2 索引重载
AWS 的 Principal Technologist, Rick Houlihan 曾说,一个设计良好的 DynamoDB 应该只有一张表。
索引重载是使用 RDS 的用户最容易感到不适应的一部分
这意味着属性名不一定是一个有实际含义的名称,其内容也可能包含多个种类。
这显然与之前对 RDS 数据的认识完全不一样,RDS 倾向于细粒度的划分每个实体,为实体及实体之间的关系建立相关的表。
如果一个应用只有一张表,那么用户、订单、商品、发票等数据会共用一个表定义,表的结构可能类似下面这个样子:
编辑
可以使用 #User#{ID} 和 #PRODUCT#{ID}这样的方式在 ID 中带上该条记录的类型
SK 也可以采用类似的设计方式,这样可以更好的辨识一条数据的类型,另一方面当 PK 在 GSI 中作为 SK 时,可以使用
begins with
查出同一类的数据。因为 DynamoDB 可以自动扩容,所设计好这一个表之后,便可以方便的对该表创建 GSI,开启备份,使用同样的查询、数据插入的命令来完成各种各样的操作,同时因为使用同一个读写容量,所以会减少读写容量的浪费。
新建不同的表来存储这些内容,其实新建的表与该表也不会太大的差异,使用同一张表反而更加方便。
在使用 PK 和 SK 进行 Query 之后,还可以使用 Filter 进行数据的筛选,这些筛选只能使用在基础数据类型上面,
因此如果存储 list, map, set 时可能会简化一些操作,但在筛选时遇到一些问题,可以利用 sk 的
begins with
查询功能,将数据拆分开来方便筛选。
3.5.3 稀疏索引
创建 GSI 时,会重新选择一个基础类型的字段作为主键,如果部分数据并不包含这个字段,那么这些数据就不会被添加到这个 GSI 索引中来。
因此可以利用这个特性为不同的查询模式来创建不同 GSI,以减少 GSI 中数据的数量。
3.5.4 组合键
组合键是一个非常有用的方式
如果有一些字段必须复合起来使用,并且会作出相对复杂的筛选条件,那么便可以考虑采用组合键来处理。如以下示例:
编辑
希望达成类似
SELECT * FROM Game WHERE Opponent = 'Bob' ORDER BY DATE DESC FILTER ON Status='PENDING'
的查询,如果 Bob 的数据有上千条,但 IN_PROGRESS 的数量只有一条,那使用 Query + Filter 的结果是需要查到这千条数据,再从中过滤出需要的数据来。
但如果使用组合键 StatusDate = Status + Date, 如下示例:
编辑
可以直接查到需要的这两条数据,也可以继续使用 Filter 再进行过滤。
同时会有一些特殊的要求,在不使用组合键时,如果进行查询会是一个相当大的挑战。
3.5.5 分级数据
与组合键类型,分级数据意味着该字段中的数据不是一个简单的数据,也会是一个拼起来的数据
最简单的例子是地域信息:比如四川省成都市,可以存储为
#SICHUAN#CHENGDU
,如果后面有多少级均可以继续接在后面。通过这种方式便把一个层级很深的数据转化成了一个简单的数据,同时使用
begins with
便可以查某个区划下的所有信息。
4 本地开发
DynamoDB 是 AWS 的云服务,可以使用 AWS 提供的 aws-cli, 通过命令行来完成常见的表操作及数据操作。
最为常用的功能是结合其它命令一起进行查询及数据分析,或者清除表中的数据。
同时,如果在开发过程中需要使用 DynamoDB,依然需要付出相应的服务费用
使用 DynamoDB Local 可以减少这部分开销。
DynamoDB Local
如果在本地进行开发,可以使用 DynamoDB Local,
- 通过 DynamoDBLocal 的 jar 包直接运行
- 使用 Docker 运行 DynamoDB 的镜像
命令为
docker run -p 8000:8000 -v $(pwd)/data:/data/ amazon/dynamodb-local:1.11.477 -jar DynamoDBLocal.jar -sharedDb -dbPath /data
可以使用 DynamoDB GUI 作为图形化的管理工具或者直接使用 aws-cli 加上参数,进行管理。
--endpoint-url http://localhost:8000
CLI代码示例:
aws dynamodb create-table \ --table-name music \ --attribute-definitions \ AttributeName=artist,AttributeType=S \ AttributeName=song_title,AttributeType=S \ --key-schema \ AttributeName=artist,KeyType=HASH \ AttributeName=song_title,KeyType=RANGE \ --provisioned-throughput \ ReadCapacityUnits=10,WriteCapacityUnits=5 aws dynamodb describe-table --table-name music | grep TableStatus aws dynamodb put-item \ --table-name music \ --item \ '{"artist": {"S": "No One You Know"}, "song_title": {"S": "Call Me Today"}, "album_title": {"S": "Somewhat Famous"}, "awards": {"N": "1"}}' aws dynamodb get-item --consistent-read \ --table-name music \ --key '{ "artist": {"S": "Acme Band"}, "song_title": {"S": "Happy Day"}}' aws dynamodb update-item \ --table-name music \ --key '{ "artist": {"S": "Acme Band"}, "song_title": {"S": "Happy Day"}}' \ --update-expression "SET album_title = :newval" \ --expression-attribute-values '{":newval":{"S":"Updated Album Title"}}' \ --return-values ALL_NEW aws dynamodb update-table \ --table-name music \ --attribute-definitions AttributeName=album_title,AttributeType=S \ --global-secondary-index-updates \ "[{\"Create\":{\"IndexName\": \"album_title-index\",\"KeySchema\":[{\"AttributeName\":\"album_title\",\"KeyType\":\"HASH\"}], \ \"ProvisionedThroughput\": {\"ReadCapacityUnits\": 10, \"WriteCapacityUnits\": 5 },\"Projection\":{\"ProjectionType\":\"ALL\"}}}]" aws dynamodb query \ --table-name music \ --index-name album_title-index \ --key-condition-expression "album_title = :name" \ --expression-attribute-values '{":name":{"S":"Somewhat Famous"}}' aws dynamodb query \ --table-name music \ --key-condition-expression "artist = :name" \ --expression-attribute-values '{":name":{"S":"Acme Band"}}' aws dynamodb scan --table-name music --filter-expression 'attribute_exists(artist)' aws dynamodb delete-table --table-name music // 使用 jq 获取某个字段并取得不重复值 aws dynamodb scan \ --table-name music \ --index-name business-index \ --projection-expression some_data \ | jq '.Items[] | .data.S' | sort --unique
5 总结
1. Truncate 操作:DynamoDB 不支持 Truncate 操作,
最简单的办法是删表重建即可;
如果需要删掉一部分数据,
- 可以写脚本用 scan 查出 PK 的列表逐个进行删除;
- 还可以设置表的过期时间,让这批数据定期失效即可。
2. JavaScript 有两个类库:一种使用了 DynamoDB Json
其中包括了数据的类型,需要调用相关的 marshal 和 unmarshal 方法来转换成标准的 Json
3.批量操作:DynamoDB 中批量操作有 25 的数据量限制,部分情况下需要手动分批分成多个请求来处理,部分类库提供了自动分批的功能
4.空值操作:DynamoDB 无法写入空值、空字符串,会直接忽略相应字段
5.查询:非基本类型的数据可以当作 String 进行查询