DyanmoDB 使用经验
DynamoDB 是什么?
DynamoDB(以下简称 DDB)是 Amazon AWS 提供的 NoSQL 云服务,完全托管在 Amazon 的云服务器上, 用户不需要也无法接触程序和数据本身。 根据官方介绍,DDB 拥有 Schema Free 、高性能、高可靠性、分布式以及扩展性等特点, 支持最终一致性或者强一致性,能够降低运维的负责性并且支持灵活的弹性吞吐设置。
数据模型
DDB 基本数据模型包括 Table、Item、Attribute。拿关系型数据库(RDB)作比较, Table 及数据表,Item 对应一行数据以及列名的组合,Attribute 则是 Item 数据中的任一项 列名-数据对。相较之下,DDB 的数据模型和 MongoDB 中的 collection、json、k-v 对更为接近。
创建 Table 时需要预设主键,主键类型可以是 Hash Key,或者 Hash key 和 Range Key 复合形式, 主键也相当于索引(如果不是的话)。
数据类型
DDB 支持的数据类型相比 RDB 并不多:Binary、Number、String、List、Set 和 Map(可嵌套), Set 分 Binary Set、Number Set、String Set,也就是说并不支持混合类型的 Set, Map 类型可嵌套,类似 JSON 或者 Python 字典。
主键设计
主键包括 Hash 和 Range 两种 key,正如其名 Hash key 指 hash 唯一,只用于定位; Range key 则拥有范围概念,可用于排序。
Hash 和 Range key 都可以是 String、Number 或者 Binary 类型。
Table 主键只可以是 Hash key 或者 Hash key + Range key,使用两个 Hash key 作为唯一区分是不允许的。
二级索引
DDB 的二级索引有两种:全局二级索引(Global Secondary Index)和局部二级索引(Local Secondary Index)。 要了解两者的区别,首先要理解 DDB 的存储方式,DDB 通过 Hash key 的方式强制将数据分区, 虽然作为用户感受不到,但这正是 DDB 保证高性能的关键。也就是说数据会根据 hash 的结果存储在不同区域。
GSI:其哈希和范围键可以与表上的哈希和范围键不同的索引。GSI 被视为“全局”,是因为对索引进行的查询可以跨表中所有分区的所有数据。
LSI:哈希键与表相同、但是范围键不同的索引。LSI 的“本地”含义在于,其每个分区的范围都限定为具有相同哈希键的表分区。
GSI 和 LSI 都支持稀疏索引。
与 Python 交互
AWS 提供了 boto 和 boto3 两套 Python 库,后者正在开发中,功能尚不完全。 前者则代码庞大同时对 Python 3 的支持也尚不健全,相对来说更推荐使用前者。
boto 提供了两套 DynamoDB 接口:boto.dynamodb
和 boto.dynamodb2
, 前者主要针对旧版本的 DynamoDB 服务,新启动的 DynamoDB 服务应该使用 DynamoDB v2。
基本配置
创建用户(组) 获取 API key 和 Secret Key 环境设置
定义连接
创建用户(组)并获取 Access Key Id - Secret Access Key
获取 key 和 secret 之后有两种设置方法:环境变量或者连接参数。
设置 Access 环境变量可通过 aws configure
设置或者手动修改 ~/.aws/credentials
文件,两者效果是相同的。
或者在连接中设置参数:
import boto
conn = boto.dynamodb2.connect_to_region('ap-northeast-1',
aws_access_key_id='YOUR_KEY',
aws_secret_access_key='YOUR_SECRET')
表操作
表操作注意包括增删表、增删表的索引和投影、调整表的吞吐量。 注意表的 key 和 schema 一旦设置则不可修改。
获取表对象
table = Table('table_name', connection=conn)
这一步并没有向服务器发起请求,因此即使使用不存在的表名也不会报错。
或者你也可以详细地定义表的属性:
from boto.dynamodb2.fields import HashKey, RangeKey, GlobalAllIndex
from boto.dynamodb2.table import Table
from boto.dynamodb2.types import NUMBER
users = Table('users', schema=[
HashKey('username'),
RangeKey('last_name'),
], global_indexes=[
GlobalAllIndex('EverythingIndex', parts=[
HashKey('account_type'),
])
])
获表信息
table.describe()
修改表属性
table.update(throughput=dict(read=READ, write=WRITE))
注意:表名、主键、Shema、本地二级索引一旦建立都无法修改和删除!
数据操作
数据是 item,其相关操作都必定与 table 相关,对于数据的操作依旧是常见的增(put_item()
)、 删(delete_item()
)、改(put_item(data={...}, overwrite=True)
)、查(query_2()
和 count()
)。DDB 也支持 count 计数,不过 count 并不具备强一致性,DDB 会每 6 个小时更新一次。 对于实时计数要求比较强的可以根据情况使用 scan()
(数据很少时),query_count()
(需要合适的索引),或者使用其它数据库或表来独立计数。
批量操作
DDB 支持批量读(batch_get
)和批量写(batch_write
):
with table.batch_write() as batch:
batch.put_item(data={...})
写操作可以混合增、删、改:
with users.batch_write() as batch:
batch.put_item(data={...})
batch.put_item(data={...})
batch.delete_item(username='janedoe', last_name='Doe')
Query & Scan
scan()
操作非常耗费资源,慎用。使用时注意使用max_page_size
和limit
限制。scan()
和query_2()
操作的返回值是 ResultSet 对象(生成器),惰性。
Query
query()
方法已弃用!新建数据库应该使用query_2()
,query()
方法只是为了保持兼容性。Query 操作必须包含索引中所有键,否则会报错!
DDB 查询的过滤类型并不像 RDB 那样丰富,仅支持:
eq
:相等lte
:小于等于lt
:小于gte
:大于等于gt
:大于beginswith
:以 xx 字符开始between
:在区间 a 和 b 之间
查询命令的拼接方式类似于 Django ORM。
Query 操作必须指定索引,默认是主键,你可以也指定 Index 名称:
# 最近一小时注册的用户
recent = users.query_2(
account_type__eq='standard_user',
date_joined__between=[0, time.time() - (60 * 60)], # 这里使用 ``gte`` 会更方便,使用 ``between`` 是出于演示目的
index='DateJoinedIndex'
)
query_count
query_count()
的操作类似于 query_2()
,不过仅仅返回符合条件的 Item 数目。
Scan
scan()
会便利整个 Table,因此时间复杂度是 O(n),并且非常耗费系统资源。 即使如此 scan
也仅仅保证最终一致性,慎用!
和 query()
类似,scan()
也支持过滤操作:
eq
:相同ne
:不相同lte
:小于等于lt
:小于gte
:大于等于gt
:大于nnull
:存在null
:不存在contains
:包含ncontains
:不包含beginswith
:以 xx 字符开始in
:值是列表 l 中之一between
:值在区间 a 和 b 之间
限制
- 单个 Item 大小限制为 400k。官方建议大数据块切片存储,在应用层整合。
- boto 单次请求大小为 1M。请求数据(
query_2()
或者scan()
)较多时应设置max_page_size
。 - 表总数限制为 256。不把它当作 Redis hash 结果来用应该不会超出这个限制。
- 表 Schema 一旦确定不可更改。对于 Hash Key、Range Key 以外的 Attribute 建议在应用层管理。
- LSI 建立后不可更改。但是可以增删 GSI。
- GSI 会异步地复制 Table 中的内容,不受应用控制,因此 GSI 只能保证最终一致性。
吐槽
- 接口的行为不统一,比如
conn.create_table
与Table.create(..., connection=conn)
的参数列表并不一致; - 功能限制比较多;
- GitHub Issue 提问不一定能得到反馈,有问题还是建议在 StackOverflow 上提问。
测试
搭建本地环境
DynamoDB 提供了 DynamoDBLocal 用于在本地模拟 DynamoDB 行为。 本地连接:
from boto.dynamodb2.layer1 import DynamoDBConnection
# Connect to DynamoDB Local
conn = DynamoDBConnection(
host='localhost',
port=8000,
aws_access_key_id='anything',
aws_secret_access_key='anything',
is_secure=False)
# List all local tables
tables = conn.list_tables()
使用 Mock
收费
DynamoDB 的收费和存储、读取流量相关,与 Table 数无关,但是需要考虑每个 Table 读写只是占用一个单位。
日本机房需要支付额外的消费税房。
最佳实践
(To be continued ……)
与其它 NoSQL 数据库的比较
Redis
DDB 和 Redis 相比之下差异较大,因此不太有可比性:
- Redis 的数据结构更简单,整体上只是个大号的字典(Hash);
- Redis 是内存数据库,因此容量很难扩展,但是速度要快得多;
- Redis 支持 sorted set、ordered list 数据结构,也支持交集这样的操作,有时会很有用;
- Redis 支持 Expire time,非常适合倒计时;
Riak
Riak 是 DynamoDB 的开源实现,同样支持 HTTP 协议操作,在使用上会非常接近 DynamoDB。
MongoDB
虽然没有开发上的渊源,但是两者在数据结构上相似度还是很高的:
- 两者都是 Schema Free;
- 两者都拥有很好的分布式、扩展性支持;
- MongoDB 以
_id
为主键,类似 DDB 的 Hash key; - MongoDB 的 Collection 可对应 DDB 的 Table,而 DDB 的 Item 结构看起来也类似 MongoDB 的文档;
- 两者都支持可嵌套的 Map(JSON)数据结构;
但是 MongoDB 在查询上支持更强大的索引,这点更接近于 RDB。