《Redis入门指南》之基本的数据结构

《redis入门指南》这本书入门不错,值得推荐,每一章都有些干活。这里分别加以总结,并增加了自己的一些理解。

常见数据结构

字符串类型

这是最常见的key-value的结构,但是现在的no-sql远不止于此:

我们来看看四个使用字符串类型的场景:

  • INCR num: 进行文章的访问pv统计;
  • INCR num: 生成自增ID,类似于mysql的auto_increment;
  • SET key value:存储序列化的json或其他数据结构;
  • GETBIT key offset: 通过bit位来记录大批量用户的单个状态,如性别,方便快速读取和修改,占用空间小,性能高。必须采用预分配+用户id映射,避免不必要的内存消耗;

散列类型

散列类型略微复杂一些,对应的是mysql中的列数据,但是有对比,才有伤害:

假设双方都要存储一张Person表,具有如下的属性:

  • 性别
  • 身高
  • 拥有车的品牌

这就尴尬了,不是每个人都有车,哪来品牌呢?mysql一定要有冗余的字段,但是redis的散列类型就不需要。
hset richwang sex male height 180 carbrand bmw
hset poolgeorage set male height 170

我们再来看看散列常见的两个场景:

  • HKEYS key | HVALS key | HLEN key : 不在使用结构化的数据来进行key-value存储,而是分字段更灵活的存储,方便单独的修改和更新,每次不需要全量json的更新
  • HEXISTS key | HSETNX: 存储某一类映射关系,确保一一对应,比如person和身份证,一旦添加之后,无法用相同的身份证再次添加。

再提一句,散列是redis中唯一支持数据类型嵌套的类型。

列表类型

列表类型主要是进行数组的描述,一般常见的操作就是向列表两端添加元素,或者获得列表的某一个片段。

从实现上来讲的话:双向链表实现,头尾插入非常方便,头尾获取也非常快,但是对中间元素的读取和写入都比较慢,所以在使用的时候也应该尤其注意这一点。

另外还有一个值得注意的性能指标,LLEN命令会直接读取现成算好的长度,而不是像mysql那样select count(*)进行遍历数据表。

下面来看看列表类型的场景,还是很多的:

  • 新鲜事Feeds,对于一个SNS而言,新鲜事的场景很多,用户每次打开,总是要拉取朋友的新鲜事。对于每个用户而言,使用一个列表存储用户能看到的动态十分方便。每次只要给出当前的index,就可以拉取其新鲜事,而且头尾存储,性能很高。
  • 日志记录:将日志记录到列表中,方便进行头尾查看。
  • ID列表,通过固定顺序将id存储列表类型,使用 LRANGE numbers 0 2 进行分页读取,缺点在于已有顺序不太方便调整。结合活动中心的案例,如果是一枪头发布,使用这个方案没有问题,但是如果活动有上线下线的过程,那么最好还是不要使用这个。
  • 队列循环:RPOPLPUSH source destination,如果source和dest相同的话,那就是一个循环。非常适合进行url的监控,循环的同时还可以不断的加入新的网址。整个系统容易扩展,允许多个客户端同时处理队列;
  • 评论列表:如果评论一经发布不可修改,那么也比较适合序列化之后,存入列表,进行插入和拉取。

集合类型

对于那些元素不可重复的场景,集合类型是必不可少的。很多时候在一些场景下,容易把集合和列表类型混用,所以最好对其两者特性都能够有所了解。集合的主要应用场景一个是不重复,另一个是集合之间的交集、并集、差集。

一个非常常见的场景就是标签的存储。使用两个集合类型:
集合1:标签集合
SADD labels teacher students worker engineer
集合2:标签下属人群集合
SADD teacher tom ted lily

通过这样的设计可以实现如下的几个功能特性:

  • 通过labels集合保存所有的不重复的标签,方便添加、拉取和删除
  • 不同的标签对应了不同的下属人群,那么标签之间非常常见的交集、并集、差集操作,都可以很方便的利用集合的SDIFF DINTER SUNION来实现

针对第二种情况,如果是使用mysql存储的话,就会涉及到一个非常复杂的join的过程:

1
SELECT p.name FROM person_labels pl, person p, labels l WHERE pl.label_id=l.label_id AND (l.label_name IN (‘teacher’, ‘worker’)) AND p.person_id=pt.person_id GROUP BY p.person_id HAVING COUNT(p.person_id)=3;

所以在这种场景之下,redis的集合类型还是非常的实用的。

有序集合列表

有序集合列表我个人感觉应该是为了互联网的榜单而生,它将每个元素都关联了一个分数。从上文中知道,redis中的列表类型使用了双向链表,获取开头和结尾的元素都是比较容易的,而有序集合则使用了散列表和跳跃表,其中跳跃表就是为了实现查找的log(N)效率而进行的实现。有了优点,不可避免的,它比列表类型更加费内存一些。

我们来看一个文章按照点击量来排序的实例,新建一个有序集合article:pageview,其中存储的是articleId,而其score就是文章的点击量。
每有一篇文章增加了点击,我们就可以使用ZINCRBY article:pageview 1 $articleId,来进行对应的操作。
按照访问量来获取所有的文章列表,就可以直接使用ZRANGE article:pageview 0 -1 withscores 加以实现。

另外一个比较常见的应用就是按照score归档数据,比如score定义为时间戳,那么就可以使用ZREVRANGEBYSCORE articles 六月的timestamp 五月的timestamp limit 0 3 这样就可以获得在六月和五月之间的前三篇文章了。

同时有序集合也是支持一些集合的操作的。举一个例子,上海市教育局想看看黄浦区的教育水平如何,选取了大同中学和大境中学两个学校,分别对应了两个有序列表:datongScore和dajingScore,分别存储其每个学科的平均分。学科的id是完全一样的。那么想看看黄浦区的学科平均分,但是由于两个学校的考题难度不一样,又需要一定的加权处理的时候,这时候就需要执行:
ZINTERSCORE huangpuScore 2 datongScore dajingScore WEIGHTS 1 0.8

再获取一下其中的结果:
ZRANGE huangpuScore 0 -1 WITHSCORES 即可获得想要的结果。

热评文章