Redis基础数据结构

  • string(字符串)
  • list(列表)
  • hash (字典)
  • set (集合)
  • zset (有序集合)

各数据结构简介

Redis的所有存储都是key-value形式的,数据结构是指value值的类型。
Redis的所有结构都可以设置过期时间,过期时间以容器为单位。
Redis容器型数据结构(List , Hash, Set, zSet)都遵循两条规则:

  • create if not exists 如果容器不存在,就创建一个
  • drop if no elements 如果容器没有元素,立即删除,释放内存

1.String
String由字符数组组成,redis的实现类似于java的ArrayList,由冗余字段来减少内存的频繁分配。当String大小小于1MB时,扩容容量加倍。当String大小大于1MB时,每次只会增加1MB(MAX = 512MB)

如果value值是一个整数,redis还提供了自增操作(incr)具体细节之后再谈论。

2.List
Redis的列表相当于Java的LinkedList,内部实现使用双向链表而不是数组,当链表弹出最后一个元素时,数据结构被删除,内存被回收。

如果在深入,会发现redis底层使用了一个ZipList(压缩列表)和 QuickList(快速链表),当Redis数据量小的时候,Redis采用ZipList进行存储。ZipList是一块连续的存储空间,以此来节约链表存储双向指针的额外开销。当数据量达到阈值时,Redis会采用QuickList进行存储,QuickList是多个ZipList组成的链表,每一块ZipList只保存一份双向链表指针。(具体细节之后再谈论)

使用场景:异步队列

3.Hash
相当于Java的hashMap,它是无序字典,同样采取了数组+链表的结构。不同的是,Redis的字典值只能是字符串,并且当Redis进行扩容时也和java有区别。java扩容hashMap是个非常耗时的操作,Redis为了追求高性能,不能堵塞服务,采用了渐进式rehash策略。当Redis进行rehash操作时,会保留新旧两个hash结构,查询时同时查询两个hash,当旧hash数据全部移植到新hash时,redis释放旧hash的内存,由新hash取而代之

使用场景:存储用户信息,每个字段单独存取。

4.Set
同java中hashSet相同,内部实现是一个特殊的字典,字典中所有的value的值都是null

5.zSet
zset结构时Redis中最有特色的结构。zset本身可以理解为一个set集合,但是又对set集合中的每一个value赋予一个score,代表value的权重。它的内部实现是一种叫做“跳跃列表”的数据结构。

使用场景:成绩排序、时间排序

– 关于跳跃列表实现原理,之后会详谈

分布式锁

分布式系统经常面临着并发问题,为防止并发问题造成的数据错乱就要使用分布式。redis分布式锁使用的非常广泛,下面就来了解分布式锁的原理:

分布式锁的奥义
分布式锁的本质就是再redis里占一个“坑”, 当其他进程来占坑时发现已经被别人占了,就只能放弃或者稍后再试。

占坑一般使用setnx(set if not exist)指令,只允许被一个客户占坑,用完再调用del指令释放掉。

然而,计算机的世界总是会出一些奇奇怪怪的问题:当一个线程执行过程中突然挂掉了,没来得及释放坑位,就会陷入死锁,锁永远得不到释放,于是我们想到了对锁加一个过期时间。

但是随之而来的,当对锁加入过期时间后,若程序未在过期时间内执行完毕,锁会自动释放。我在网上查的解决锁超时问题的相关资料大多方法是启动守护线程,通过Lua脚本重设超时时间,这其中有着复杂的逻辑判断以及守护线程随主线程的结束而销毁。关于解决锁超时这个问题还值得我们大家的思考。

现在,我们面临另外一个问题:如果分布式锁加锁失败了怎么办?

一般情况下,我们有以下三种策略:

  • 直接抛出异常,通知用户稍后重试
  • sleep,一会重试
  • 将请求转移至延时队列

延时队列

我们都习惯RabbitMq和KafKa作为消息队列中间件,如果我们只有一组消费者,我们可以使用redis简化消息队列的操作。当然,redis不是专业的消息队列,它没有那么多的高级特性,没有ack的保证,无法确定消息的可靠性。

redis的list最经常用来做异步队列。可以使用lpop和rpop操作出队列。如果客服端轮询的时候发现redis队列为空怎么办?我们可以用sleep来解决这个问题。但是是使用sleep后还会有问题:队列的延迟增大。我们可以使用Redis的阻塞读(blpop/brpop)。

上面介绍了异步队列,那么延时队列应该怎么实现呢?我们通常采用Redis的zSet数据结构来实现。我们将消息设置为zset的value,到期处理时间为score,然后用多线程轮询zset到期任务来处理。同时用zerm方法保证任务的唯一属主。

位图

有时,我们会遇到大量的布尔值存储,如果我们使用常规方式存储将占用大量的空间,这对服务器的压力是巨大的。这个时候,我们就可以用Redis的位图数据结构。

位图不是一种特殊的数据结构,它其实就是普通的字符串,我们可以通过getbit/sitbit对字符串对应的byte数组进行操作。Redis的位数组(字符串对应的byte数组)是自动扩充的,当设置的偏移位置超出了现有的内容范围,redis将自动进行零扩充。

位图可以通过bitcount指令和bitpos指令进行统计,但统计的范围只能是8的倍数。还有魔术指令bitfield。关于指令的使用方法,这里就不再赘述了。

Redis的非准确数据结构

HyperLogLog

如果现在有一个网站,万恶的产品必须让你统计点击数量和有效访问数量(PV and UV),如果只统计点击量,那非常好办,只需要设置一个Redis计数器,不断的incrby就可以了。但是UV不同,同一个ID每天只能统计一次有效访问数量,这就需要去重。也许我们可以用set,天然去重的数据结构,但是一旦访问量过大,set中将保存数以千万个用户ID的时候,服务器最终会撑不住的。这是,就用到了标题上的高级数据结构HyperLogLog,它提供了pfadd、pfcount指令,一个用来增加计数,一个用来获取计数。HyperLogLog原理比较复杂,笔者也没过多的了解,这里就略过了。

布隆过滤器

布隆过滤器可以理解为不那么准确的set结构,它判断不在集合中的一定不存在,判断在集合中的不一定存在。我们可以通过参数设置来减小它的误差,当然,误差设置的越小,需要的空间也就越大。