【Redis】Redis之上篇

2022-03-31 10:56:27 浏览数 (1)

为什么使用Redis

缓存本质上是解决速度不一致问题。

redis还可以做分布式锁、消息队列等,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件(如zookpeer等)代替,并不是非要使用redis。因此,这个问题主要从性能和并发两个角度着手:

(1) 性能:

耗时特别久且结果不频繁变动的SQL,就特别适合将运行结果放入缓存。

(2) 并发:

在大并发的情况下,请求数据库并发量过大会出现异常。需要使用redis减压,让redis解决一部分热点数据的请求。

定义

RedisRemote DictionaryServer)

Redis是C语言开发的一个开源(遵从BSD协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。它是一种NoSQL(not-only sql,泛指非关系型数据库)数据库。

Redis提供的数据结构类型

1. 类型

字符串(String),列表(List),散列(Hash),集合(Set),有序集合(Sorted Set),Bitmaps等。

在Redis中,数据结构类型的关键概念有:key、field、value。

key可以理解为表名、引用等,对数据进行操作时,key起一个定位作用。

而Redis的数据结构类型是针对key的value来说的,例如,string是指的key的value是string;list是指key的value是一个list;hash是指key的filed和value是一个<filed, value>的map类型。

2. 使用

(1) String

代码语言:javascript复制
SET  key  value  [EX seconds]  [PX milliseconds]  [NX|XX]
SETNX  key  value
GETSET  key  value
GET  key
INCRBY  key  increment
DECRBY  key  number

value不仅是String,也可以是数字。String类型是二进制安全的,意思是Redis的String类型可以包含任何数据,比如jpg图片或者序列化的对象。String类型的值最大能存储512M。

(2) Hash

代码语言:javascript复制
HSET  key  field  value
HGET  key  field
HGETALL  key
HKEYS  key
HVALS  key
HDEL  key  field  [field ...]

Redis Hash是一个键值<field, value>对集合,特别适合用于存储结构化对象(field即属性)。

(3) List

代码语言:javascript复制
LPUSH  key  value  [value ...]
RPUSH  key  value  [value ...]
LPOP  key
RPOP  key
LRANGE  key  start  stop
LLEN  key

list列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)。

(4) Set

代码语言:javascript复制
SADD  key  member  [member ...]
SREM  key  member  [member ...]
SCARD  key
SISMEMBER  key  member
SPOP  key  [count]
SMEMBERS  key

Redis的Set是string类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

(5) ZSet

代码语言:javascript复制
ZADD  key  [NX|XX]  [CH]  [INCR]  score  member  [score member ...]
ZCOUNT  key  min  max
ZSCORE  key  member

Zset和Set一样也是String类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。Redis正是通过分数来为集合中的成员进行从小到大的排序。ZSet的成员是唯一的,但分数(Score)却可以重复。

3. 实现(参考:https://my.oschina.net/liughDevelop/blog/2236771)

一个例子

一个例子了解Redis的5种数据结构类型在内存中的数据模型:

当我们执行set hello world命令时,会有以下数据模型:

(1) dictEntry:Redis给每个key-value键值对分配一个dictEntry,里面有着key和val的指针,next指向下一个dictEntry形成链表,这个指针可以将多个哈希值相同的键值对链接在一起,由此来解决哈希冲突问题(链地址法)。

(2) sds:键key“hello”是以SDS(简单动态字符串)存储,后面详细介绍。

(3) redisObject:值val“world”存储在redisObject中。实际上,redis常用5种类型都是以redisObject来存储的;而redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。

redisObject对象非常重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都需要redisObject支持。这样设计的好处是,可以针对不同的使用场景,对5种常用类型设置多种不同的数据结构实现,从而优化对象在不同场景下的使用效率。

内存分配效率

无论是dictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储。jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。比如jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。

前面说过,Redis每个对象由一个redisObject结构表示,它的ptr指针指向底层实现的数据结构,而数据结构由encoding属性决定。比如我们执行以下命令得到存储“hello”对应的编码:

代码语言:javascript复制
>set hello world
OK
>object encoding hello
"embstr"

实现数据结构类型

Redis数据类型

String

List

Hash

Set

ZSet

底层数据结构

数组

双向链表

二维结构第一维度:数组第二维度:链表

Hash

Hash 跳跃表

Redis所有的数据结构类型的内存结构如下:

(1) String

字符串对象的底层实现可以是int、raw、embstr。embstr编码是通过调用一次内存分配函数来分配一块连续的空间,而raw需要调用两次。

int编码字符串对象和embstr编码字符串对象在一定条件下会转化为raw编码字符串对象。embstr:<=39字节的字符串。int:8个字节的长整型。raw:大于39个字节的字符串。

简单动态字符串(SDS),这种结构更像C 的String或者Java的ArrayList<Character>,长度动态可变。sds(simple dynamic string)相对c的改进:

1) 常数复杂度获取字符串长度:因为SDS在len属性中记录了长度。

2) 惰性释放空间:当执行sdstrim(截取字符串)之后,SDS不会立马释放多出来的空间,如果下次再进行拼接字符串操作,且拼接的没有刚才释放的空间大,则那些未使用的空间就会排上用场。通过惰性释放空间避免了特定情况下操作字符串的内存重新分配操作。

3) 杜绝缓冲区溢出:使用C字符串的操作时,如果字符串长度增加而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,redis会先检查sds的空间是否满足所需要求,如果不满足会自动扩充。

(2) List

ziplist(压缩列表)与linkedlist(双端链表)相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。

List对象的底层实现是quickList,quickLis是zipList和linkedList的混合体。它将linkedList按段切分,每一段使用zipList来紧凑存储,多个zipList之间使用双向指针串接起来。

redis链表源码有什么特性:双端、无环、带长度记录。

(3) Hash

Hash对象的底层实现可以是ziplist(压缩列表)或者hashtable(字典或者也叫哈希表)。

Hash对象只有同时满足下面两个条件时,才会使用ziplist(压缩列表):1.哈希中元素数量小于512个;2.哈希中所有键值对的键和值字符串长度都小于64字节。

hashtable哈希表可以实现O(1)复杂度的读写操作,因此效率很高。

hashtable源码可以简化成如下结构:

这个结构类似于JDK7以前的HashMap<String,Object>,当有两个或以上的键被分配到哈希数组的同一个索引上时,会产生哈希冲突。Redis也使用链地址法来解决键冲突。即每个哈希表节点都有一个next指针,多个哈希表节点用next指针构成一个单项链表,链地址法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位。

Redis中的字典使用hashtable作为底层实现的话,每个字典会带有两个哈希表,一个平时使用,另一个仅在rehash(重新散列)时使用。随着对哈希表的操作,键会逐渐增多或减少。为了让哈希表的负载因子维持在一个合理范围内,Redis会对哈希表的大小进行扩展或收缩(rehash),也就是将ht[0] 里面所有的键值对分多次、渐进式的rehash到ht[1] 里。

(4) Set

Set集合对象的底层实现可以是intset(整数集合)或者hashtable(字典或者也叫哈希表)。

intset底层实现为有序,无重复数组保存集合元素。intset这个结构里的整数数组的类型可以是16位的,32位的,64位的。如果数组里所有的整数都是16位长度的,如果新加入一个32位的整数,那么整个16的数组将升级成一个32位的数组。升级可以提升intset的灵活性,又可以节约内存,但不可逆。

(5) ZSet

ZSet有序集合对象底层实现可以是ziplist(压缩列表)或者skiplist(跳跃表)。

当一个有序集合的元素数量比较多或者成员是比较长的字符串时,Redis就使用skiplist(跳跃表)作为ZSet对象的底层实现。

skiplist的查找时间复杂度是LogN,可以和平衡二叉树相当,但实现起来又比它简单。跳跃表(skiplist)是一种有序数据结构,它通过在某个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

redis为什么采用跳表而不是红黑树?

在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。

平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。

从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。

从算法实现难度上来比较,skiplist比平衡树要简单得多。

实现-总结

4. 应用

Redis不适合做什么:不适合大数据规模的存储和冷数据的存储。

适合做什么:

(1) String缓存、限流、计数器、分布式锁、分布式Session

(2) Hash存储用户信息、用户主页访问量、组合查询、单点登录

这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。

博主在做单点登录的时候,就是用这种数据结构存储用户信息,以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果。

(3) List微博关注人时间轴列表、简单消息队列、分页功能

使用List的数据结构,可以做简单的消息队列的功能。

可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。

(4) Set赞、踩、标签、好友关系、全局去重

因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。为什么不用JVM自带的Set进行去重?因为我们的系统一般都是集群部署,使用JVM自带的Set,比较麻烦,难道为了一个做一个全局去重,再起一个公共服务,太麻烦了。

就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能

(5) Zset排行榜、延时任务、范围查找

扩展功能

(1) 提供了键过期功能,还可以用来实现缓存。

(2) 提供了发布/订阅功能,可以用来实现消息系统。

(3) 提供了简单事务功能,能在一定程度上保证事务特性。

(4) 提供了流水线功能,客户端能将一批命令一次性传到Redis,减少了网络开销。

(5) 提供了Lua脚本功能,可以利用Lua创造出新的Redis命令。

0 人点赞