1、什么是Redis (Remote Dictionary Server)?
Redis 是完全开源免费的, 遵守 BSD 协议, 是一个高性能的 key-value 形式的NoSQL内存数据库;
支持将内存中的数据以快照和日志的形式持久化到硬盘;
支持数据的备份即 master-slave 模式的数据备份;
支持丰富的数据结构
2、redis优势:
1)性能高,速度快
Redis命令执行速度非常快,官方给出的读写性能可以达到10W/秒。为什么会如此之快呢?有以下几个因素:
数据存储在内存中,直接与内存连接。
由相对底层的C语言实现,离操作系统更近。
实现源码很精湛,仅仅几万行代码,简单稳定。
使用了单线程模型,无多线程竞争、锁等问题。
2)丰富的数据结构
Redis与其他的内存数据库不同的是,Redis拥有丰富的数据类型(5种),如字符串、哈希、列表、集合、有序集合。正是因为Redis丰富的数据类型,所有它能应用的场景非常多。
其实还包括其他的数据结构:HyperLogLog、Geo、Pub/Sub,只是暂时没用过。
3)丰富的特性: 除了支持丰富的数据结构外,还支持以下高级功能:
支持键过期功能,可以用来实现定时缓存
支持发布/订阅功能,可以有来实现消息队列
支持事务功能,可以保证多条命令的事务性
支持供管道功能,能够批量处理命令
支持Lua脚本功能。
支持集群分片和数据复制功能
支持内存数据持久化硬盘功能
4)丰富的客户端:各种各种的语言都能接入到Redis,接入包括了所有的主流开发语言。
3、Redis 的几种常见使用方式?
1)Redis 单副本:
采用单个 Redis 节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。
缺点:
a.在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务;
b.高性能受限于单核 CPU的处理能力(Redis 是单线程机制),CPU 为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用 Memcached 替代。
2)Redis 多副本(主从):
采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外
提供服务和读写分离策略。
优点:
高可靠性:支持主从切换,保障服务稳定性;
读写分离策略:能有效应对大并发量的读操作
缺点:
故障恢复复杂:如果没有 RedisHA 系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点;
主库的写能力受到单机的限制,可以考虑分片;
主库的存储能力受到单机的限制,可以考虑 Pika;
3)Redis Sentinel(哨兵):
是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:Redis Sentinel 集群和 Redis 数据集群。其中 Redis Sentinel 集群是由若干 Sentinel 节点组成的分布式集群,
可以实现故障发现、故障自动转移、配置中心和客户端通知。Redis Sentinel 的节点数量要满足 2n+1(n>=1)的奇数个。
优点:
实现自动主从切换;
轻松实现Redis 数据节点的线形扩展,进而突破 Redis 自身单线程瓶颈,可极大满足 Redis 大容量或高性能的业务需求;
可通过 Sentinel 监控一组 Redis 数据节点或多组数据节点;
缺点:
部署相对 Redis 主从模式要复杂一些,原理理解更繁琐;
Redis Sentinel 主要是针对 Redis 数据节点中的主节点的高可用切换,对 Redis 的数据节点做失败判定分为主观下线和客观下线两种,对于 Redis 的从节点有对节点做主观下线操作,并不执行故障转移。
不能解决读写分离问题,实现起来相对复杂。
资源浪费,Redis 数据节点中 slave 节点作为备份节点不提供服务;
4)Redis Cluster
是社区版推出的 Redis 分布式集群解决方案,主要解决 Redis 分布式方面的需求,比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的。
Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。
优点:
数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布;
可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除;
高可用性:部分节点不可用时,集群仍可用。
缺点:
数据通过异步复制,不保证数据的强一致性;
Client 实现复杂;
节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断下线,这种 failover 是没有必要的;
多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况;
Key 批量操作限制,如使用 mset、mget 目前只支持具有相同 slot 值的 Key 执行批量操作。对于映射为不同 slot 值的 Key 由于 Keys 不支持跨 slot 查询,所以执行 mset、mget、sunion 等操作支持不友好。
Key 事务操作支持有限,只支持多 key 在同一节点上的事务操作,当多个 Key 分布于不同的节点上时无法使用事务功能
不支持多数据库空间,单机下的 redis 可以支持到 16 个数据库,集群模式下只能使用 1 个数据库空间,即 db 0
避免产生 hot-key,导致主库节点成为系统的短板
避免产生 big-key,导致网卡撑爆、慢查询等
重试时间应该大于 cluster-node-time 时间
Redis Cluster 不建议使用 pipeline 和 multi-keys 操作,减少 max redirect 产生的场景。
5)Redis 自研
自研的高可用解决方案,主要体现在配置中心、故障探测和 failover 的处理机制上,通常需要根据企业业务的实际线上环境来定制化。某大型互联网公司sn 就在redis 2.8版本上进行修改定制的缓存服务器软件称为zedis,在jedis版本上定制的缓存客户端软件称为sedis
4、redis 的使用场景:
1)缓存:购物车、页面、推荐商品的缓存;
2)商品排行榜:Redis提供的有序集合数据结构的各种复杂的排行榜应用
3)计数统计:比如隐私政策多次取消后,就不再弹框提示;
4)分布式会话:
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,
而是由session服务及内存数据库管理。
5)分布式锁:
分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是
不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
6)社交网络:点赞、踩、关注/被关注、共同好友等,可通过redis 丰富的数据结构来实现。
7)最新列表:
Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
8)消息系统
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,
能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
5、redis 不同数据结构的使用场景:
String:缓存、限流、计数器、分布式锁、分布式Session
Hash:存储用户相关信息(名称、角色、昵称)、用户主页访问量、组合查询
List:微博关注人时间轴列表、简单队列
Set:赞、踩、标签、好友关系
Zset:排行榜
6、redis 单线程操作为什么这么快:
纯内存操作;
单线程操作,避免了频繁的上下文切换;
采用了非阻塞 I/O 多路复用机制;
7、为什么要了解redis的内存模型?
估算 Redis 内存使用量,选择合适的机器配置,可以在满足需求的情况下节约成本;
了解 Redis 内存模型可以选择更合适的数据类型和编码,更好的利用 Redis 内存,优化内存占用;
利于分析解决问题:Redis 出现阻塞、内存占用等问题
8、内存统计查看命令:info memory 会显示如下信息:
used_memory:分配器分配的内存总量(单位字节),其是从redis的角度的到的量
used_memory_rss:Redis 进程占据操作系统的内存,其是从操作系统角度得到的量
mem_fragmentation_ratio:used_memory_rss / used_memory 的比值,存碎片比率(一般大于 1,且该值越大,内存碎片比例越大)
Redis 使用的内存分配器,默认的 jemalloc,在编译时指定;可以是 libc 、jemalloc 或者 tcmalloc
9、redis 内存划分?
1)数据存储占用的内存:在存储对象时,并不是直接将数据扔进内存,而是会对对象进行各种包装:如 RedisObject、SDS 等,该部分内存会统计在 used_memory 中;
2)进程本身运行需要的内存:Redis 主进程本身运行肯定需要占用内存,如代码、常量池等,这部分内存不是由 jemalloc 分配,因此不会统计在 used_memory 中。除了主进程外,Redis 创建的子进程运行也会占用内存,如 Redis 执行 AOF、RDB 重写时创建的子进程。当然,这部分内存不属于 Redis 进程,也不会统计在 used_memory 和 used_memory_rss 中。
3)缓冲内存:缓冲内存包括客户端缓冲区、复制积压缓冲区、AOF 缓冲区等,这部分内存由 jemalloc 分配,因此会统计在 used_memory 中。
a.客户端缓冲区存储客户端连接的输入输出缓冲;
b.复制积压缓冲区用于部分复制功能;
c.AOF 缓冲区用于在进行 AOF 重写时,保存最近的写入命令。
4)内存碎片
a.内存碎片的产生与对数据进行的频繁更改操作、数据的大小差异特点等都有关;
b.与使用的内存分配器也有关系;
c.可通过重启减少内存碎片:因为重启之后,Redis 重新从备份文件中读取数据,在内存中进行重排,为每个数据重新选择合适的内存单元,减小内存碎片。
10、谈谈redis 的数据是如何存储的?
这就要涉及到内存分配器(如 jemalloc)、简单动态字符串(SDS)、5 种对象类型及内部编码、RedisObject;
以key=hello,value=world 为例进行说明:
1)dictEntry:Redis 是 Key-Value 数据库,因此对每个键值对都会有一个 dictEntry,里面存储了指向 Key 和 Value 的指针;next 指向下一个 dictEntry,与本 Key-Value 无关。其中 key(”hello”)并不是直接以字符串存储,而是存储在 SDS 结构中。
2)RedisObject:Value(“world”)既不是直接以字符串存储,也不是像 Key 一样直接存储在 SDS 中,而是存储在 RedisObject 中。实际上,不论 Value 是 5 种类型的哪一种,都是通过 RedisObject 来存储的;而 RedisObject 中的 type 字段指明了 Value 对象的类型,ptr 字段则指向对象所在的地址。不过可以看出,字符串对象虽然经过了 RedisObject 的包装,但仍然需要通过 SDS 存储。
3)jemalloc:Redis 在编译时便会指定内存分配器;内存分配器可以是 libc 、jemalloc 或者 tcmalloc,默认是 jemalloc。jemalloc 作为 Redis 的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc 在 64 位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当 Redis 存储数据时,会选择大小最合适的内存块进行存储。无论是 DictEntry 对象,还是 RedisObject、SDS 对象,都需要内存分配器(如 jemalloc)分配内存进行存储。以 DictEntry 对象为例,有 3 个指针组成,在 64 位机器下占 24 个字节,jemalloc 会为它分配 32 字节大小的内存单元。
11、谈谈redisObject 这种数据结构
Redis 对象有 5 种类型;无论是哪种类型,Redis 都不会直接存储,而是通过 RedisObject 对象进行存储。 RedisObject 对象非常重要,Redis 对象的类型、内部编码、内存回收、共享对象等功能,都需要RedisObject 支持,其结构具体如下5部分:
1)type 字段:表示对象的类型,占 4 个比特。
目前包括 REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
字段类型查看的命令:type keyName
2)encoding 字段:表示对象的内部编码,占 4 个比特,对于 Redis 支持的每种类型,都有至少两种内部编码。通过 encoding 属性,Redis 可以根据不同的使用场景来为对象设置不同的编码,大大提高了 Redis 的灵活性和效率。
例如:
对于字符串,有 int、embstr、raw 三种编码。
对于列表对象,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis 倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入。当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。
对象采用的编码方式的查看命令:object encoding keyName
3)lru:lru 记录的是对象最后一次被命令程序访问的时间
4)refcount(引用数量)与共享对象:
a.refcount:记录的是该对象被引用的次数,类型为整型。refcount 的作用,主要在于对象的引用计数和内存回收。当创建新对象时,refcount 初始化为 1;当有新程序使用该对象时,refcount 加 1;当对象不再被一个新程序使用时,refcount 减 1;当 refcount 变为 0 时,对象占用的内存会被释放。
b.共享对象:Redis 中被多次使用的对象(refcount>1),称为共享对象。Redis 为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。共享对象的具体实现:Redis 的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和 CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为 O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为 O(n^2)。虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。就目前的实现来说,Redis服务器在初始化时,会创建 10000 个字符串对象,值分别是 0~9999 的整数值;当 Redis 需要使用值为 0~9999 的字符串对象时,可以直接使用这些共享对象。10000 这个数字可以通过调整参数 REDIS_SHARED_INTEGERS(4.0 中是 OBJ_SHARED_INTEGERS)的值进行改变。
共享对象的引用次数的查看命令:object refcount keyName
5)ptr 指针:
指针指向具体的数据,如前面的例子中,set hello world,ptr 指向包含字符串 world 的 SDS。Redis 没有直接使用 C 字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了 SDS。SDS 是简单动态字符串(Simple Dynamic String)的缩写。
12、谈谈SDS结构这种数据结构?
包含以下三个部分:
a.buf 表示字节数组,用来存储字符串;
b.len 表示 buf 已使用的长度;
c.free 表示 buf 未使用的长度;
13、redis 的五种对象类型及其内部编码?
Redis 支持的5种对象类型,而每种结构都有至少两种编码,Redis 内部编码的转换,都符合编码转换在 Redis 写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换。这样做的好处是:接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。
1)redis 的对象类型--字符串
a. 字符串是最基础的类型,因为所有的键都是字符串类型,且字符串之外的其他几种复杂类型的元素也是字符串,字符串长度不能超过 512MB。
b. 字符串类型的内部编码有 3 种。具体使用场景如下:
int:8 个字节的长整型。字符串值是整型时,这个值使用 long 整型表示。
embstr:<=39 字节的字符串。embstr 与 raw 都使用 RedisObject 和 sds 保存数据。 embstr 的使用只分配一次内存空间(因此 RedisObject 和 sds 是连续的),而 raw 需要分配两次内存空间(分别为 RedisObject 和 sds分配空间)。因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个 RedisObject 和 sds 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读;
raw:大于 39 个字节的字符串;
c.编码转换:当 int 数据不再是整数,或大小超过了 long 的范围时,自动转化为 raw。而对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 39 个字节。
2)redis 的对象类型--列表(list)
a.列表用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储 2^32-1 个元素。Redis 中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等。
b.内部编码:列表的内部编码可以是压缩列表(ziplist)或双端链表(linkedlist)
双端链表:由一个 list 结构和多个 listNode 结构组成,双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针。链表中保存了列表的长度;dup、free 和 match 为节点值设置类型特定函数。所以链表可以用于保存各种不同类型的值,而链表中每个节点指向的是type为字符串的 RedisObject。
压缩列表:压缩列表是 Redis 为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构,具体结构相对比较复杂。与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高。因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。压缩列表不仅用于实现列表,也用于实现哈希、有序列表;使用非常广泛。
c.编码转换
只有同时满足下面两个条件时,才会使用压缩列表:列表中元素数量小于 512 个、列表中所有字符串对象都不足 64 字节。如果有一个条件不满足,则使用双端列表;且编码只可能由压缩列表转化为双端链表,反方向则不可能。
3)redis 的对象类型--哈希
a.内部编码:内层的哈希使用的内部编码可以是压缩列表(ziplist)和哈希表(hashtable)2 种
与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省空间。同时,虽然对于元素的操作复杂度也由 O(n)变为了 O(1),但由于哈希中元素数量较少,因此操作的时间并没有明显劣势。
b.编码转换
只有同时满足下面两个条件时,才会使用压缩列表:哈希中元素数量小于 512 个;哈希中所有键值对的键和值字符串长度都小于 64 字节。如果有一个条件不满足,则使用哈希表;且编码只可能由压缩列表转化为哈希表,反方向则不可能。
4)redis 的对象类型--集合(set)
a.集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。一个集合中最多可以存储 2^32-1 个元素;除了支持常规的增删改查,Redis 还支持多个集合取交集、并集、差集。
b.内部编码:集合的内部编码可以是整数集合(intset)或哈希表(hashtable)。集合在使用哈希表时,值全部被置为 null。
c.编码转换:只有同时满足下面两个条件时,集合才会使用整数集合:集合中元素数量小于 512 个,集合中所有元素都是整数值。如果有一个条件不满足,则使用哈希表;且编码只可能由整数集合转化为哈希表,反方向则不可能。
5)redis 的对象类型--有序集合
a.有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。
b.内部编码:有序集合的内部编码可以是压缩列表(ziplist)或跳跃表(skiplist)跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此 Redis 中选用跳跃表代替平衡树。跳跃表支持平均 O(logN)、最坏 O(N) 的复杂点进行节点查找,并支持顺序操作。Redis 的跳跃表实现由 zskiplist 和 zskiplistNode 两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点,具体结构相对比较复杂。
c.编码转换:只有同时满足下面两个条件时,才会使用压缩列表:有序集合中元素数量小于 128 个;有序集合中所有成员长度都不足 64 字节。如果有一个条件不满足,则使用跳跃表;且编码只可能由压缩列表转化为跳跃表,反方向则不可能。
参看博文:https://mp.weixin.qq.com/s/75uewvZWpSzxRbVvOrcOcg,还有一些其他的资料,本次整理是基于之前学习的笔记整理的,但是忘记出处了,待回忆起来再补充相关的原文链接!
原文:https://www.cnblogs.com/jiarui-zjb/p/14311108.html