一.Redis的使用场景
(1)缓存
合理使用缓存不仅可以加快数据的访问速度,而且能有效降低后端数据源的压力。Redis提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略
(2)排行榜系统
排行榜系统几乎存在于所有网站,例如按照热度排名的排行榜、按照发布时间的排行榜、按照各种复杂维度计算出的排行榜,Redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统。
功能 | 有 序集合(Sorted Set) | 列表(List) |
---|---|---|
排序 | 按分数排序 | 按插入顺序 |
适用场景 | 排行榜、评分系统 | 消息队列、操作日志、固定长度的记录 |
数据结构 | 跳表 | 双端链表 |
操作复杂度 | 插入/删除:O(log(N)),取出:O(log(N)) | 插入/删除:O(1),取出:O(N)(范围查询) |
其他特性 | 按分数范围查询、高效的排名计算 | 双端操作(头尾插入和删除) |
选择使用有序集合还是列表,取决于具体的应用场景和需求:
- 有序集合:适合需要按分数排序、排名计算的场景,如排行榜、评分系统。
- 列表:适合需要维护数据插入顺序、实现消息队列、固定长度记录的场景,如最新消息列表、任务队列。
(3)计数器应用
计数器在网站中的作用至关重要,例如视频网站有播放数、电商网站有浏览数,为了保证数据的实时性,每一次播放和浏览都要加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
(4)社交网络
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适合保存这种数据,Redis提供的数据结构可以相对比较容易地实现这些功能。
(5)消息队列系统
消息队列系统可以说是一个大型网站的必备基础组件,因为其具有业务解耦、非实时业务削峰等特性。Redis提供了发布订阅共鞥你和阻塞队列的功能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功能基本可以满足。
二.Redis常用数据结构及底层实现
1.字符串
其他几种数据结构都是在字符串类型基础上构建的,字符串类型的值可以是字符串、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB
字符串的内部编码
字符串内部编码有3种:
int:8个字节的长整型,embstr:小于等于39个字节的字符串,raw:大于39个字节的字符串
使用场景
(1)缓存功能
(2)计数
Redis提供了自增incr、自减decr、incrby自增指定数字、自减指定数字decrby、自增浮点数incrbyfloat
(3)共享session
为了解决分布式web服务器用户每一次访问都需要重新登陆的问题,可以使用Redis将用户的Session进行集中管理。
(4)限速
许多应用处于安全考虑,会在每次进行登陆时让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次,此功能可以使用redis实现。
上述就是利用redis实现了限速功能,例如一些网站限制一个IP地址不能再一秒钟内访问超过n次也可以采用类似思路。
2.哈希
哈希类型是指键值本身是一个键值对结构,形如value={{field1,value1},...{fieldN,valueN}}
设置值: hset key field value
获取值: hget key field
删除值: hdel key field1 field2 ...支持批量删除
计算field个数: hlen key
批量获取 hmget field1 field2
批量设置 hmset key field value field2 value2
自增 hincr key field
内部编码
内部编码有两种:ziplist(压缩列表)、hashtable(哈希表)
**ziplist:**当哈希类型元素个数小于配置(默认512个)、同时所有值都小于配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀
只要违反上面两个要求中的一个,就会使用hashtable实现
hashtable: 当哈希类型无法满足ziplist条件时,此时ziplist的读写效率会下降,所以Redis会使用hashtable作为哈希的内部实现,hashtable的读写时间复杂度为O(1)
使用场景
关系型数据表记录如果用哈希类型存储,相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的id定义为键后缀,多对field-value对应每个用户的属性
目前为止,我们已经能够用三种方法缓存用户信息
(1)原生字符串类型:每个属性一个键
set user:1:name tom
优点:简单直观,每个属性都支持更新操作
缺点:占用过多的键,内存占用量较大,同时用户内聚性比较差,所以此种方案一般不会在生产环境中使用
(2)序列化字符串类型:将用户信息序列化后用一个键保存
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提供内存的使用效率
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中
(3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保存
hmset user:1 name tom age 23 city beijing
优点:简单直观,如果使用合理可以减少内存空间的使用
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。
3.列表
列表类型是用来存储多个有序的字符串,有序指的是插入顺序。
在Redis中可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。
列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
rpush key value1 value2
lpush key value1 value2
向某个元素前或者后插入元素
linsert listkey before b java 在列表的元素b前插入java
linsert listkey after b java
获取指定范围内的元素列表
lrange key start end
获取列表指定索引下标的元素
lindex key index
删除
从列表左侧弹出元素 lpop key
从列表右侧弹出元素 rpop key
阻塞操作
blpop key timeout 其中timeout表示阻塞时间
brpop key timeout
也支持多个键的阻塞弹出,从左到右遍历有一个键弹出,客户端就会立刻返回
blpop key1 key2 timeout
brpop key1 key2 timeout
内部编码
列表类型的编码有两种:ziplist和linkedlist(链表)
同样地,当元素个数超过512个或者某个元素超过64字节时,内部编码会变为linkedlist
在Redis3.2中又提供了quicklist内部编码,它结合了ziplist和linkedlist两者的优势。
使用场景
(1)消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的"抢"列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
(2)文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不单是有序的,同时支持按照索引范围获取元素。
lpush+lpop = Stack
lpush+rpop = Queue
lpush+ltrim((截取列表) = Capped Collection(有限集合)
lpush+brpop = Message Queue(消息队列)
4.集合
集合类型也是用来保存多个字符串元素,但和列表类型不一样的是,集合种不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。
Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题
sadd key element1 elemetn2
srem key element1 elment2
从集合中随机弹出元素spop key
集合间操作
交集 sinter key1 key2 ...
并集 sunion key1 key2...
差集 sdiff key1 key2
内部编码
内部编码有两种:intset(整数集合)、hashtable(哈希表)
intset:当集合元素都是整数且元素个数小于默认配置512个时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用
hashtable:当某个元素不为整数或元素个数超过512个,Redis会使用hashtable作为集合的内部实现
使用场景
集合比较典型的使用场景是标签。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻感兴趣,这些兴趣点就是标签。有了这些数据,就可以得到喜欢同一个标签的认,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,通常会为网站带来更多的收益。
5.有序集合
有序集合它保留了集合不能有重复成员的特性,但不同的是有序集合中的元素可以排序。它会给每个元素设置一个分数作为排序的依据。
有序集合的设计允许多个成员具有相同的分数
zadd key score member
zrank key member 计算成员的排名(排名从0开始)
zrange key start end (start end是分值)
内部编码
ziplist和skiplist(跳跃表)两种
当有序集合的元素个数超过128个或者某个元素大于64个字节,内部编码会变为skiplist
使用场景
有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。
三.Redis的其他功能
Redis提供的5种数据结构已经足够强大,但除此之外,Redis还提供了慢查询分析、Redis Shell、Pipeline提高客户端性能、事务与Lua、Bitmaps、HyperLogLog、发布订阅、GEO等附加功能。
四、Redis内部编码方式
Redis 内部使用了多种数据结构来存储不同类型的数据,以优化性能和内存使用。以下是 Redis 内部编码的数据结构及其原理的详细介绍:
- 简单动态字符串(SDS)
SDS 是 Redis 用来存储字符串的内部数据结构,相比于 C 语言的 char*,SDS 提供了更高效和安全的字符串操作。
c
struct sdshdr {
int len; // 已使用空间长度
int free; // 剩余可用空间长度
char buf[]; // 实际存储字符串的数组
};
特点:
- 常数复杂度获取字符串长度
- 杜绝缓冲区溢出:C字符串不记录自身长度,容易造成缓冲区溢出,比如在执行拼接操作时,若dest目标字符串没有分配足够的内存,就会产生缓冲区溢出。而SDS API需要对SDS进行修改时,会先检查SDS的空间是否满足修改所需的要求,如果不满足会自动修改SDS的空间大小。
- 减少修改字符串时带来的内存重分配次数
(1)预分配空间:在字符串扩展时,SDS 会预分配一些额外的空间,减少了重复分配和内存拷贝的次数。
(2)惰性空间释放:SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。通过惰性空间释放策略,SDS避免了缩短字符串时所需的内存重分配操作,并为将来可能有的增长操作提供了优化。与此同时,SDS也提供了相应的API在我们真正需要时释放SDS的未使用空间,所以不必担心因惰性空间释放造成内存浪费。 - 二进制安全:使用二进制安全的SDS,而不是C字符串,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。
- 兼容部分C字符串函数:虽然SDS的API是二进制安全的,但它们一样遵循C字符串以空字符结尾的惯例,这是为了让那些保存文本数据的SDS可以重用一部分<string.h>库定义的函数。
2.ziplist
3.linkedlist
4.哈希表
Redis采用了链式哈希来解决哈希冲突,链式哈希的局限性很明显,随着链表的长度的增加,查询这一位置上的数据的耗时就会增加。要想解决这一问题,就要进行rehash,也就是对哈希表的大小进行扩展。
Redis使用dict结构体表示哈希表,不过实际使用哈希表时,在dict结构体内定义了两个哈希表(ht[2])
refresh操作:
(1)先给没有分配空间的哈希表2分配空间,一般会比哈希表1大2倍
(2)将哈希表1的数据迁移到哈希表2中
(3)迁移完成后,哈希表1空间被释放,并被哈希表2设置为哈希表1,然后在哈希表2新创建一个空白的哈希表,为下次refresh做准备
渐进式refresh:
为了避免refresh在数据迁移过程中,因拷贝数据的耗时,影响Redis性能的情况。所以Redis采用了渐进式rehash ,也就时将数据的迁移工作不再是一次性迁移完成,而是分多次迁移。
Redis的渐进式rehash是一种用于扩展哈希表容量的机制,它允许在扩容期间继续处理命令,而不会阻塞客户端的请求。这个机制使得Redis在处理大数据集时能够保持高性能和可用性。
具体步骤如下:
(1) 初始哈希表:Redis使用一个固定大小的哈希表来存储数据,当哈希表的负载因子(键值对数量 / 哈希表大小)达到一定阈值时,就会触发扩容操作。
(2)分配新哈希表:在扩容时,Redis会为哈希表分配一个新的更大的空间。此时,Redis会保持同时使用两个哈希表:旧哈希表和新哈希表。
(3)迁移数据:在渐进式rehash期间,Redis会逐步将旧哈希表中的键值对逐个迁移到新哈希表中。为了避免一次性大规模迁移造成服务器阻塞,Redis将迁移操作分解为多个小步骤,每次处理一部分键值对。
(4)并发处理:在迁移过程中,Redis仍然可以处理客户端的读取和写入请求。对于读取操作,Redis首先在新哈希表中查找键值对,如果没有找到,则会继续在旧哈希表中查找。对于写入操作,Redis会将新的键值对插入到新哈希表中,不会影响旧哈希表。
迁移完成:当所有键值对都成功迁移到新哈希表后,渐进式rehash完成。此时,Redis会停止使用旧哈希表,并释放旧哈希表占用的内存。
5.quckedlist
6.skiplist
Redis只有在zset对象的底层实现用到了跳表,跳表的优势是能支持平均O(logN)复杂度的节点查找。与平衡二叉树相当,但实现起来比平衡二叉树简单,且性能稳定。
Zset对象是唯一一个同时使用了两个数据结构来实现的Redis对象,这两个数据结构一个是跳表,一个是哈希表。这样做的好处是既能进行高效的范围查找,也能进行高效单点查询。
Zset能支持范围查询是因为它的数据结构采用了跳表,而又能以常数复杂度获取元素权重,这是因为它同时采用了哈希表进行索引。
跳表结构设计
跳表是在链表基础上改进过来的,实现了一种多层 的有序链表
跳表节点的查询过程:
五、Redis使用的过期删除策略是什么?
Redis是可以对key设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
当我们查询到一个key时,Redis首先会检查key是否存在于过期字典中:
- 如果不存在,则正常读取键值
- 如果存在,则会获取该key的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该key已过期。
Redis使用的过期策略是惰性删除+定期删除这两种策略配合使用
(1)什么是惰性删除策略?
惰性删除的做法是,不主动删除过期键,每次从数据库中访问key时,都检测key是否过期,如果过期删除该key.
惰性删除策略优点:
因为每次访问时,才会检查key是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除对CPU时间最友好。
缺点:
如果一个key已经过期,只要这个过期key一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以惰性删除策略对内存不友好。
(2)什么是定期删除策略?
定期删除策略的做法是,每隔一段时间随机从数据库中取出一定数量的key进行检查,并删除其中的过期key.
Redis 的定期删除的流程:
从过期字典中随机抽取 20 个 key;
检查这 20 个 key 是否过期,并删除已过期的 key;
如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
可以看到,定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。
优点:
通过限制删除操作执行的时长和频率,来减少删除操作对CPU的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
缺点:
难以确定删除操作执行的时间和频率。如果执行的太频繁,就会对CPU不友好;如果执行的太少,那又和惰性删除一样了,过期key占用的内存不会及时得到释放。
两种策略都有各自的优缺点,所以Redis选择这两种策略配合使用,以求在合理使用CPU时间和避免内存浪费之间取得平衡。
转载于小林coding https://mp.weixin.qq.com/s/3Bm1h_oEi4X4b_RIldUekw