前言:最近打算趁着还没入职做个项目,其中我一意孤行打算采用dynamodb作为数据库,导致其他后台开发人员对数据库操作部分理解困难,所以在此总结一下我到目前为止的学习到的知识。

目前打算分为四篇,第一篇介绍DynamoDB的基本概念,第二篇介绍如何用一张表来表示整个数据库,第三篇介绍一下java操作dynamoDB的方法,第四篇介绍操作dynamodb的分页和一些技巧。写的不对还请大佬斧正。

前置知识:最好后mysql传统数据库的知识,方便理解相似的概念。

什么是DynamoDB

这里直接引用官方原话:

Amazon DynamoDB 是一种完全托管的 NoSQL 数据库服务,提供快速且可预测的性能,同时还能够实现无缝扩展。使用 DynamoDB,您可以免除操作和扩展分布式数据库的管理工作负担,因而无需担心硬件预置、设置和配置、复制、软件修补或集群扩展等问题。
使用 DynamoDB,您可以创建数据库表来存储和检索任意量级的数据,并提供任意级别的请求流量。您可以扩展或缩减您的表的吞吐容量,而不会导致停机或性能下降。此外,您还可以使用 AWS 管理控制台来监控资源使用情况和各种性能指标。

那么什么时候采用DynamoDB,什么时候采用Mysql呢?据我了解,dynamoDB好处如下:

  1. 对于海量数据,Dynamodb表现要好。由于Dynamodb是nosql,也就意味着没有传统数据库的诸多限制,在海量数据的方面无论是查询还是存储都比传统数据库要高。
  2. 数据库的schema方便调整。传统数据库的一个重大的问题就是调整表的结构,调整表结构会引起一些列的问题,甚至有重新设计数据库的可能,而对于nosql来说,因为其是通过文档进行存储的,所有没有严格意义上的schema,所以当后期进行扩展时,影响较小。

相对来说,dynamodb缺点也十分明显,在没有传统数据库限制的同时,代价是其丧失了很多重要功能,比如说外键,表连接,数据有效性检查等等。总而言之,如果项目极度依赖表和表之间的关系,且关系及其复杂,那么建议用传统mysql,否则可以尝试dynamodb。

正文

dynamodb基本概念

getItem、query和scan

这三个操作都是查询操作,效率分别是:getItem > query > scan

getItem是根据primary key进行插叙,可以理解为通过primary key在hashMap上查询,速度是最快的,缺点是必须知道primary key且只能查询单个,使用情况相对较少。

scan是全表扫描,是最慢的一个,理论上能不用就不用,只有实在走投无路才考虑全表扫描。

query是最常见的方式,在dynamoDB的使用中,我们唯一的目的就是写出高效的查询query。

分区键:partition key

由于这是一个入门教程,所以我尽量用我的理解来讲述这些基本概念,最后给出官方解释:

首先我们来假设这么一个场景,学校来了1000个学生,现在我们要把这1000个学生分成10个班,并且,我们要能快速根据身份证号找到其中的任何一个学生在哪个班级以及个人信息,怎么办呢?或者说怎么分班呢?


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_list根据某个字段排序


在这里,我们来定义一下学生所携带的信息,一个学生有身份证号、姓名、性别以及出生年月日:

{    姓名: 颜小四    性别: 男    身份证号: 123456789    出生年月日: 19950101}

当然,最容易的一个方案是每个班分100人,然后每个班记录班里学生的身份证号,然后查找的时候看这个身份证号究竟在哪个班。但是这个方案的问题也十分明显:首先你需要记录每个学生在哪个班,其次再查找班级的时候,最坏情况下你需要将每个班的花名册都看一遍。

那么一个更优的方案是什么呢?我们采取hash的方式来进行分班,现在,我们取hash函数为f(x)=身份证%10

那么对于身份证为 123456789的学生来说,他的班级为 123456789 % 10 = 9班

那么对于身份证为 100000000的学生来说,他的班级为 100000000% 10 = 0班(10班)

以此类推,这个时候,如果一个人过来说,请你查一下身份证号为938493023的学生在哪个班。你就能很快的知道他在3班。

为什么讲这个例子呢?这就引出了Dynamodb中的第一个概念:分区键(partition key),partition key的缩写是pk,在官方文档中我一直以为是primary key,但其实不是的。分区键的意义就是将数据进行分区。上面那个例子中,班级就是分区,学生就是数据,假设我们有1亿的数据量,在不分区的情况下进行查找那么我们就需要查找1亿的数据量,但是假设我们有10个分区(partition),那么每个分区的数据量就是1000万,查询速度将显著提高。

这里引出第二个问题:如果分区键选的不好会怎么样

还是上面那个例子,这次我们不使用身份证,我们用年份模10作为hash函数:f(x)=出生年份%10

我们知道,同一批入学的学生出生年份不会差的太大,比如我出生1995,那么大部分学生也一定是出生于1995年附近,那么最后结果很有可能是这个情况

班级1:0人

班级2:0人

班级3: 10人 (1993%10 == 3)

班级4: 90人 (1994%10 == 4)

班级5: 400人 (1995%10 == 5)

班级6: 400人 (1996%10 == 6)

班级7: 90人 (1997%10 == 7)

班级8: 10人 (1998%10 == 8)

班级9: 0人

班级10: 0人

可以看到,大部分学生会集中在班级5和班级6,也就意味着数据分布相当不均匀,极端情况下有可能所有学生都在一个班,那么分区的作用也就没用了,这也是为什么官方推荐使用UUID作为分区键的原因,使用UUID能最大限度地保证数据均匀分布在不同分区中。

附上DynamoDB对分区键设计的建议:


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_字段_02


排序键:sort key

再来看另外一个例子,现在我们有很多学生很多次考试的成绩,以其中一个学生为例:

{    身份证: 123456789    考试: 英语四级    成绩: 300    时间: 2015-6}{    身份证: 123456789    考试: 英语四级    成绩: 420    时间: 2015-12}{    身份证: 123456789    考试: 英语四级    成绩: 421    时间: 2016-6}...省略若干{    身份证: 123456789    考试: 英语四级    成绩: 310    时间: 2019-6}{    身份证: 123456789    考试: 英语四级    成绩: 429    时间: 2019-12}

由于分区键只能有一个,所以我们还是以身份证号作为分区键,现在,我们想知道:身份证号为123456789的这名考生,有多少次的成绩是在420-425之间的?

在没有排序的情况下,我们需要拿到这名考生的所有考试成绩,然后一一比对哪些是在420-425之间的,泛化一点,时间复杂度是O(n)的。然而,如果我们将这些数据根据分数大小进行排序存储,那么我们就能通过二分查找的方式找到哪些考试分数在420-425分之间,泛化一点,时间复杂度为log(n)。


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_list根据某个字段排序_03


通俗一点说,sort key就是:在partition key相同的情况下数据根据哪个字段进行排序。

关于sort key的设计

了解到sort key的作用之后,我当即有个疑问,那就是:如果我想根据多个属性进行排序怎么办?举个例子,现在一个学生有三门成绩:语文、数学、英语。现在我要对所有学生的成绩进行排序,排序规则为:先根据语文成绩进行排序,语文成绩相同的情况下根据数学成绩进行排序,数学成绩相同的情况下根据英文成绩排序:


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_字段_04


理想的最终排序效果

这种情况下应该如何设计呢?一开始的时候,我以为需要采用多个字段当sort key的形式,后来发现理解错了,现在我们能确定:数据库中的数据就是根据sort key这个字段进行排序的,也就是说,我们需要设计一个sort key这个字段,根据sort key这个字段排序之后,得到上表的顺序。于是,我们可以用List作为sort key,list中存放三个数据:语文、数学、英语。根据list的比较原则,当第一个数据一样时比较第二个数据,第二个数据一样时比较第三个数据,反之只要不一样,就会把小的放在前面,这和我们的需求一模一样:


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_数据_05


你也许还有疑问,如果我有时要根据语文数学英语排序,有时要根据英语数学语文排序,怎么办呢?这就涉及到GSI的用处了,GSI会讲到。

当然,sort key的作用远不止于此,以我项目中用到的sort key作为例子:

一个大学有若干学生,每个学生有所在的学院、专业、班级


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_字段_06


如果你有疑问:为什么之前说partition key最好是uuid,现在partition key却是一个固定字段,这个会在GSI的作用中进行解释。

现在,我需要查询测绘1402班的所有同学,query应该怎么写呢?其实很简单:

query: partition key = "中南大学" and sort key = "地信院#测绘#1402"

问题来了,那如果我要查询GIS专业下所有的学生呢?很明显,这里就不能用精确查找了,而是需要进行范围查,dynamoDB提供了一个叫begins_with的操作,这样就简单了,我们只需要找到以"地信院#GIS#"开头的数据就行了:

query: partition key = "中南大学" and sort key begin_with "地信院#GIS#"

同理,如果我们要查询地信院下的所有学生,可以使用如下查询方式:

query: partition key = "中南大学" and sort key begin_with "地信院#"

可以看到,设计一个好的sort key,可以方便我们做非常多的范围查询。

Global Search Index(GSI)

接下来讨论dynamoDB中的一个重要概念:GSI(全局搜索索引)

什么是GSI呢,为什么要有GSI呢?我们来看下面一个例子:

考虑一个论坛,论坛帖子存储了以下信息:帖子ID(UUID),发帖人ID(UUID),发帖时间,发帖内容,数据大体如下:


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_字段_07


这个时候,如果我想拿到用户User#333的最近发帖怎么办呢?你可能会想,如果现在能用发帖人ID作为partition key,发帖时间作为sort key就好了,因为这样的话,表就会变成下面的样子:


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_数据_08


这样我们只需要填写发帖人ID,就能得到发帖人最近发帖(倒序一下)。没错,GSI就是做的这个工作,创建GSI时和创建表一样,也需要提供partition key和sort key,不过这是GSI的partition key和 sort key,我一般称之为gsi-pk和gsi-sk。gsi的作用就是让你能通过自己的方式实现调整表的结构,从而实现高效查询。

GSI基本原理如下图


java list 对象 根据某个字段分组求某个字段的和 list根据某个字段排序_字段_09


在通过我们希望的方式创建索引,对索引进行排序,由于索引指向的实际数据,那么可以变相认为我们对数据按我们的要求进行了重排序。

最后,回答之前的问题:

问题1:在考试的例子中,如果需要按英语数学语文的方式排序怎么办?

答:新建一个字段,类型为list,[英语分数,数学分数,语文分数],新建一个GSI,以该字段作为GSI的sort key即可(相当于重新调整表的结构)

问题2:在查找某个专业下所有学生的例子中,为什么用一个固定字段(“中南大学”)为partition key而不是UUID。

答:那张表是创建GSI之后的结果(学校名为gsi-pk,学院#专业#班级为gsi-sk),由于分区只作用于物理分区,而对于索引没有意义,所以对于索引来说,gsi的partitionkey可以为任意字段,只要能够满足要求即可。