我们都知道MySQL被称为关系型数据库,其他众多存储引擎被称为非关系型数据库,这里要聊的GDB就是其中的一种。说来也讽刺,MySQL被称为关系型数据库,但是实际上处理关联关系并不那么友好。Join语句稍有不慎就是一个慢查询,DBA同学也往往盯着Join语句,常常建议我们能不用就不用。而GDB(Graph Database)的图形结构存储本身,就代表着关联关系,能够很好处理这些问题。
在社区业务中,关系尤其重要,特别是用户与用户之间的关注关系、用户与内容的点赞关系等等。这些信息能代表用户的喜好,我们能使用这些信息让他们找到志同道合者,让他们看到更多喜好的内容。本文中,我们通过几个问题,来聊聊GDB在得物社区亿级别关系链中的实战。
什么是GDB
全称Graph Database图数据库,是一种使用图数据结构进行语义查询的非关系型数据库,使用的「节点」、「有向边」、「属性」来表示和存储数据。我们先通过一张图,来直观感受下,下图中,密密麻麻的每个点就是每一个用户「User」,点与点之间的连线代表着关注关系「Attention」:
我们与MySQL做对比,可以更好的理解图数据中一些名词概念,如下:
数据库名称 | 实体名 | 对象名 | 对象数据 | 查询语法 |
---|---|---|---|---|
GDB | 标签(label) | 节点、边(Node、Edge) | 属性(property) | Cypher |
MySQL | 表(table) | 记录(record) | 字段(field) | SQL |
上表中出现一个相对陌生的词Cypher,这是市面上相对成熟的图数据库Neo4j的专属查询语法,现已经标准化,在各大厂商的图数据库中都有支持。同样,下面我们先对比下相关语法,有个直观感受:
业务语义 | SQL | Cypher |
---|---|---|
查询10个用户 | select * from users limit 10; | match (a:users) return a limit 10; |
查询标签为"明显"的用户 | select t.* from users as tinner join user_tag_relation as utr on utr.userId= t.userIdinner join user_tag as u on u.tagId= utr.tagIdwhere u.tag_name= '明星' | match (t:users)-[utr:usesr_tag_relation]->(u:user_tag) where u.tag_name = '明星' return t |
图例 | ER图: |
图结构: |
上述语法中 (t:users)-[utr:usesr_tag_relation]->(u:user_tag) 和图结构非常相近,把小括号()视为节点,把中括号[]视为边,()-[]->() 就可以很形象的表达存储结构了。
为什么使用GDB
在前言中就有提到,社区的一些业务非常适合采用GDB来实现。以下列举两种常见业务:
-
我关注的人是否赞过这条内容。
-
我关注的人在一定时间之内关注的其他人。
我们分别通过MySQL方案和GDB方案来处理,然后对比一下其中优劣,就能得出本小节的答案。下面采用图的方式来描述示例数据。

我关注的人是否赞过这条内容
一般MySQL方案:
- 查到所有关注用户(现有set缓存):select followUserId from user_follow where userId = {$userId}
- 查询这些用户是否有点赞过某些动态:select * from content_light where userId in {followUserId} and contentId in {contentId}
-
- 需要注意,in查询,在数组较大的情况,索引经常会失效,原因在于MySQL索引策略在判断时如果发现需要查询太多次的索引,可能还不如直接扫表来得快。这也正是DBA同学建议我们in查询不要超过200个的原因。
GDB方案:
- 根据图例写出节点与边的结构即可:match (u1:User) -[:Attention]->(u2:User)-[:Light]->(c:Content) where u1.userId = {userId} and c.contentId in {contentIds} return u2.userId , c.contentId;
方案对比:
在MySQL方案中,当「我关注的用户」较多时(在得物,关注的人大于1000的大有人在),存在慢查风险。在GDB方案中,查询语句简洁明了,并且查询效率也较高,所谓无图无证据,下图为实践数据(无Redis缓存):
我关注的人在24h之内关注的其他人
一般MySQL方案:
- 查到所有关注用户(现有set缓存):select followUserId from user_follow where userId = {$userId}
- user_follow_xx分表查询这些关注用户又关注的其他人
GDB方案:
- 根据图例写出节点与边的结构即可:match (u1:User)-[:Attention]->(u2:User)-[:24HAttention]->(u3:User) return u3;
方案对比:
在MySQL方案中,由于关注数据达到上亿级别,在MySQL中做分表,是必要的处理,但是正是因为这个处理,使得这个业务场景的查询更加麻烦,需要拆开分别调用,复杂度可想而知。在GDB方案中,查询语句简洁明了,查询效率如下图:
如何使用GDB
语法的学习
-
Cypher:neo4j.com/docs/cypher… 更类似于SQL,更形象,便于理解,学习成本较低
-
gremlin:tinkerpop-gremlin.cn/#traversal 更类似于ORM,封装了许许多多链式方法,学习成本较高
遇到的问题&解决
-
- 唯一索引问题:为了确保数据的唯一性,我们通常会设置唯一性约束
创建索引语法:Create CONSTRAINT on (a:User) ASSERT a.userId IS UNIQUE - 在实践中阿里云的GDB索引可能会失效,需要采用特殊语法来更新数据: - - 类似于MySQL的on duplicate key update,来实现存在即更新,不存在即插入:merge (n:User{userId: userId}) on create set n.isAllowLike = isAllowLike on match set n.isAllowLike = $isAllowLike return n.userId as userId
-
- 二级查询效率问题:
尽量不使用二级查询的属性进行排序,可以根据业务将二级查询数量级降低,在结果代码中排序
Badcase示例:match (u:User)-[:Attention]->(c:User)-[a:Attention]->(t:User) where u.userId=userId and a.createTime > {TTFtime} return c.userId as cUserId, t.userId as tUserId order by a.createTime desc
Goodcase实例:match (u:User)-[:Attention]->(c:User)-[a:TTFAttention]->(t:User) where u.userId=$userId return c.userId as cUserId, t.userId as tUserId
未来应用
下面通过一些示例,来扩展下应用场景。
部署依赖图
我们在版本发布的时候,通常会整理发布清单,其中最重要的一环,就是厘清依赖关系,有时候项目众多,依赖关系复杂通过表格等方式往往难以表达清楚。我们通过GDB将依赖关系表达得非常清晰。
下图是某版本部署图,有几点特征:
- 部署图变成了一个「森林」
- 「森林」中不同的「树」是可以独立发布的
- 通过「有向边」可以知道「树」的依赖关系
- 必须从「根节点」开始发布。
用户画像
给自己弄了个用户画像,通过不同用户的画像相似程度,可以找到志同道合的朋友。
知识图谱
想知道《权利的游戏》中各大家族的关系么?这类知识图谱可以非常方便的查到某人的家族关系。
总结
目前社区GDB服务,支持了上亿点和边的关系数据,上百的QPS,平均RT在28ms左右,较好的支持了这部分业务场景。当然GDB肯定也不是万能的,相信这个世界上没有最好的技术,只有最适合当前应用场景的技术。
限于篇幅,这篇文档并没有与大家进行更深入的讨论,欢迎线下探讨。最后,希望这篇文章能给你带来一些灵感与思路,感谢阅读。
如果有帮助,可以留言,也可以关注“得物技术微信”公众号!