亿级别关系链之GDB实战

我们都知道MySQL被称为关系型数据库,其他众多存储引擎被称为非关系型数据库,这里要聊的GDB就是其中的一种。说来也讽刺,MySQL被称为关系型数据库,但是实际上处理关联关系并不那么友好。Join语句稍有不慎就是一个慢查询,DBA同学也往往盯着Join语句,常常建议我们能不用就不用。而GDB(Graph Database)的图形结构存储本身,就代表着关联关系,能够很好处理这些问题。

在社区业务中,关系尤其重要,特别是用户与用户之间的关注关系、用户与内容的点赞关系等等。这些信息能代表用户的喜好,我们能使用这些信息让他们找到志同道合者,让他们看到更多喜好的内容。本文中,我们通过几个问题,来聊聊GDB在得物社区亿级别关系链中的实战。 

什么是GDB

全称Graph Database图数据库,是一种使用图数据结构进行语义查询的非关系型数据库,使用的「节点」、「有向边」、「属性」来表示和存储数据。我们先通过一张图,来直观感受下,下图中,密密麻麻的每个点就是每一个用户「User」,点与点之间的连线代表着关注关系「Attention」:

image.png

我们与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来实现。以下列举两种常见业务:

  1. 我关注的人是否赞过这条内容。

  2. 我关注的人在一定时间之内关注的其他人。

我们分别通过MySQL方案和GDB方案来处理,然后对比一下其中优劣,就能得出本小节的答案。下面采用图的方式来描述示例数据。

image.png

我关注的人是否赞过这条内容

一般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缓存):

image.png

我关注的人在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方案中,查询语句简洁明了,查询效率如下图:

image.png

如何使用GDB

语法的学习

遇到的问题&解决

    1. 唯一索引问题:为了确保数据的唯一性,我们通常会设置唯一性约束

创建索引语法: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

    1. 二级查询效率问题:

尽量不使用二级查询的属性进行排序,可以根据业务将二级查询数量级降低,在结果代码中排序
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将依赖关系表达得非常清晰。

下图是某版本部署图,有几点特征:

  • 部署图变成了一个「森林」
  • 「森林」中不同的「树」是可以独立发布的
  • 通过「有向边」可以知道「树」的依赖关系
  • 必须从「根节点」开始发布。

image.png 用户画像

给自己弄了个用户画像,通过不同用户的画像相似程度,可以找到志同道合的朋友。

image.png

知识图谱

想知道《权利的游戏》中各大家族的关系么?这类知识图谱可以非常方便的查到某人的家族关系。

image.png

总结

目前社区GDB服务,支持了上亿点和边的关系数据,上百的QPS,平均RT在28ms左右,较好的支持了这部分业务场景。当然GDB肯定也不是万能的,相信这个世界上没有最好的技术,只有最适合当前应用场景的技术。

限于篇幅,这篇文档并没有与大家进行更深入的讨论,欢迎线下探讨。最后,希望这篇文章能给你带来一些灵感与思路,感谢阅读。

如果有帮助,可以留言,也可以关注“得物技术微信”公众号!

猜你喜欢

转载自juejin.im/post/7036946890851614728