Redis
什么是redis
redis是一款基于内存的k-v数据结构的非关系型数据库,读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
redis的数据类型
string:字符串 缓存对象,分布式ID,token,session,数字自增,分布式锁,JWT
list:列表 消息队列
hash:哈希 缓存对象 购物车
set:集合 缓存对象 点赞可能认识的人,共同好友。社交功能 收藏夹
zset:有序集合 排行榜
geo:地理 地理位置附近的人,滴滴
hyperloglog:基数统计 网站pu统计
stream:流 消息队列
bitmap:位图 登录状态,连续签到,二值统计
Redis还支持事务,发布-订阅,切片集群,主从复制,哨兵,持久化,内存淘汰机制,lua脚本,过期删除策略
为什么使用redis作为mysql的缓存
高性能
高并发
常见数据类型的底层实现
数据类型 | 3.0 | 7.0 |
---|---|---|
string | SDS | SDS |
list | 双向链表,压缩列表 | quicklist |
hash | 压缩列表,哈希表 | listpack,哈希表 |
set | 哈希表,整数集合 | 哈希表,整数集合 |
zset | 压缩列表,跳表 | listpack,跳表 |
元素数量小于512,元素长度小于64字节
string:简单动态字符串
list:quicklist(压缩列表,双向链表)
hash:listpack(压缩列表,哈希表)
set:元素少于512个且为整数,整数集合;否则哈希表
zset:元素小于128,元素值小于64字节时压缩列表;否则跳表
7.0后压缩列表又listpack代替
Redis线程模型
redis是单线程吗
先指出什么是redis的单线程:redis的单线程指的是接受客户端请求,解析请求,处理请求,进行数据读写操作,发送请求结果是由主线程处理的
然后分析redis程序不是单线程:启动redis程序,不仅由主线程还有后台线程,关闭处理文件,aof刷盘,lazyfree线程(异步释放内存)
redis采用单线程为什么还那么快
redis内存操作
I/O多路复用机制
单线程模型,避免多线程频繁的创建和切换
redis之前为什么是单线程模型
一方面:redis性能瓶颈不在于cpu,而在于网络IO,多线程也仅仅是处理网络IO时使用多线程,指令处理过程还是单线程。
另一方面:多线程频繁的创建销毁和切换也是不小的cpu开销,增加了系统复杂性,还需要考虑加锁场景。aaaaaa
Redis持久化
==AOF日志:==执行完一条写命令,就将命令追加到server.aof_biuf缓冲区,然后通过系统write()调用将aof_buf缓冲区的数据写入到aof文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区page_cache,等待内核将数据写入磁盘。在,redis重启时,会读取aof文件中的命令,进行数据恢复。
这个过程是先执行命令再去存储命令:一方面避免了对命令的语法检查,另一方面不会阻塞写命令运行;但是可能会发生数据丢失和阻塞后续命令
AOF写回策略有三种:
alway:立即写回磁盘
severysec:每秒写入一次
no:有内核控制写入时机
写回策略 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
always | 立即写回 | 可靠性高,几乎不会丢失数据 | 磁盘压力大,性能开销大 |
everysec | 每秒写回 | 性能适中 | 宕机时最多损失1s内数据 |
no | 内核决定 | 性能好 | 宕机时可能损失最多数据 |
AOF日志过大时会触发AOF重写:读取当前数据库中所有的键值对,记录到新的aof文件,从后将文件重命名替换原来的aof文件,先生成后替换模式避免了源文件发生污染。
重写AOF过程是由后台子进程bgrewriteaof完成的:1.避免了主进程阻塞,主进程可能继续处理请求 2.使用主进程而不是主线程,多线程之间会共享内存,修改共享内存数据时需要加锁来保证数据安全。而父子进程是以只读方式共享内存数据的,当一方发生写操作时就会触发写时复制,形成两个独立数据副本就不用加锁来保证数据安全
如果AOF日志重写过程中主线程处理了写请求,那么写请求命令会被同时写进AOF缓冲区和AOF重写缓冲区,当子进程完成AOF重写后,主进程会将AOF重写缓冲区的所有内容追加到新的AOF文件中,改名覆盖原来文件。
RDB快照:记录某一个瞬间的内存数据。这种方法重启时恢复数据比AOF重启快得多。AOF重启是将命令重新执行一遍,速度非常慢
RDB快照又save和bgsave两个命令控制。
save:主线程进行RDB快照生成,会阻塞主线程
bgsave:创建一个子进程来处理生成RDB文件
Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多
RDB执行快照的时候,数据还是可以被修改的,写时复制技术。执行bgsave命令时会fork一个子进程,会将主线程的页表复制一份给子进程,这两份页表指向同一片内存区域,当一方尝试进行写操作时就会触发写时复制,创建两个独立的数据副本,RDB快照继续对副本进行快照,主线程又可以进行写命令操作。
混合持久化:RDB快照重启时恢复快,但是宕机会损失较多数据;AOF重启式恢复较慢,但是宕机时损失数据较少。这样既保证了重启数据恢复速度又降低了数据丢失风险。
混合持久化工作在AOF日志重写的过程中,当执行aof日志重写时,主进程fork的子进程就会先将与主进程共享的数据以RDB快照的形式存入aof文件,然后主进程记录在aof重写缓冲区的数据会以aof日志的形式追加到aof文件,这样就形成了一个混合持久化文件,然后替换原来的aof文件。这样重启时会先加载保存大量数据的rdb文件,然后加载较少的aof文件,使得重启数据恢复数据较快。因为原来的一部分命令以快照的形式存储了起来。这个重启是指数据全部加载完毕,因为rdb里面的key可能被修改,入药aof文件进行修正,这样才算重启全部完成。
持久化方式 | 数据 | 重启 |
---|---|---|
AOF | 宕机数据丢失少 | 重启数据恢复较慢 |
RDB | 宕机丢失数据较多 | 重启数据恢复快 |
混合持久化 | 宕机数据丢失较少 | 重启数据较快 |
Redis如何实现高可用
主从复制
主从复制就是将一台redis服务器同步数据到多台从服务器,就是一主多从的模式,并且主从服务器之间采用读写分离的模式。
主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,从服务器一般只读,并接受主服务器同步的写操作命令。
所有的写操作都是由主服务器完成的,主从服务器的数据是一致的,无法保证强一致性。在从节点进行写操作会报错,因为从节点默认是只读模式
哨兵模式
哨兵模式是为了解决Redis主从模式主服务器宕机时需要手动恢复的问题,哨兵模式可以监控主从服务器,并提供主从节点故障转移功能。
切片集群
当redis缓存数据达到一台服务器无法缓存的时候,就需要使用redis切片集群方案。将数据散发到不同的服务器上。
Redis Cluster采用hash槽来处理数据与节点之间的映射关系。一个切片集群共有16384个hash槽。
具体过程发了为两步:
- 根据键值对的 key,按照 CRC16 算法 计算一个 16 bit 的值。
- 再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。
具体映射方案:
- 平均分配: 在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
- 手动分配: 可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。
集群脑裂导致数据丢失问题
主从架构中,主节点与所有从节点因为网络波动失去联系,但与客户端依旧保持连接,客户端依旧向这个主节点写数据,但是这些数据无法同步给从节点,哨兵发现主节点失联重新选举主节点,这样一个集群就出现了两个主节点。当原来主节点重连时,会降级为从节点,并且清空数据与主节点进行数据同步。那么失联那段时间写入的数据就会丢失。
解决方案:当主节点下线或者通信时间超时的总数量小于阈值的时候,紧张主节点写入数据,客户端返回错误。
min-slaves-to-write 主节点至少需要与几个节点连接,小于这个数禁止写数据
min-slaves-max-lag 主从数据复制和同步延迟不能超过多少秒,超时就会禁止写入数据
即使原主库是假故障,期间也无法响应哨兵心跳和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了。
等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。
Redis过期删除和内存淘汰策略
如果key设置了过期时间就会把key带上过期时间存入到过期字典中。
过期删除:惰性删除,定时删除,定期删除 ,redis使用定期删除和惰性删除结合的策略。
定时删除:顶个计数器,到期自动删除
惰性删除:等查询到key时去判断是否过期
定期删除:过一段时间随机抽取20个键,如果超过五个就会再抽取20个,执行时间有一个阈值默认是25ms
过期删除策略 | 优点 | 缺点 |
---|---|---|
定时删除 | 能及时清理过期键值 | 会占用大量的cpu资源 |
惰性删除 | 占用很少的系统资源,对cpu时间最友好 | key不被访问一直存在,造成内存浪费 |
定期删除 | 限制执行时长和频率,削减对cpu的占用,减少了过期🗡的内存消耗 | 难以去确定执行的时长和频率 |
Redis持久化时会对过期🗡做什么处理
RDB文件生成阶段:RDB文件生成期间会对key进行检查,过期的🗡不会被保存到新的RDB文件中。
RDB文件加载阶段:主服务器:会对🗡进行检查,过期键不会被加载到内存中;从服务器:接收到主服务器的RDB文件不会对键的状态进行检查,从节点在执行只读操作时,如果是有expire设定的key,则会根据自己机器上的时钟来判断是否已过期,如果未过期,则返回给客户端。但从节点本身不执行删除操作,而是会等待后面的del同步操作。
AOF重写阶段:会对键值进行过期检查,过期的不会被保留到新生成的aof文件 (aof文件过大)
AOF写入阶段:如果过期键没被删除,aof文件会保留此过期键,当过期间被删除后,会向aof文件显式追加一条删除命令 (生成aof)
Redis主从复制时,对过期键如何处理
只有主服务器进行过期扫描,从库过期键依靠主服务器控制,主服务器会向AOF文件追加del命令,同步到从库。
Redis内存满了会发生什么
就会触发内存淘汰机制
不做数据淘汰:满了就无法进行写操作命令,但是可以执行删除查询等操作 。maxmemory可以设置最大运行内存
执行数据淘汰:
1.对过期时间中数据进行淘汰:volatile-random,volatile-ttl,volatile-lru,volatile-lfu
2.对所有键进行数据淘汰: allkeys-random,allkeys-lru,allkeys-lfu
LRU 算法和 LFU 算法有什么区别?
传统的LRU算法基于链表实现,链表中的元素按照操作顺序从前往后排列,最新操作的元素会一道链表头部,删除末尾元素。
redis没有采用传统的方法,因为这样需要维护一个链表,会带来额外的空间开销,并且存储数据非常多的时候链表会很大,大量访问时就会产生很多移动操作,极大的耗费性能。
redis在对象头中添加了一个24位的lru字段用于记录最后一次访问的时间,随机淘汰时就是比较这个时间来决定是否淘汰,这样就不用维护链表节省了空间,提高了效率和性能。但是当一次性读取大量的数据,而这些数据只会被读取一次,却能保存相当长的时间,造成缓存污染。
LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题。
redis在对象头中添加一个24位的lfu字段,高16位用来记录上次访问的时间戳ldt,低八位用来记录拼凑logc,这个是按数学概率来进行增加削减的。
Redis缓存设计
如何避免缓存雪崩,缓存击穿,缓存穿透
缓存雪崩:过期时间更加均匀,比如给过期时间加上随机数值;不设置过期时间,由后台服务决定更新时机
缓存击穿:设置互斥锁,如双重检查锁;不设置过期时间,有后台决定更新时机;进行限流,一次只允许几个请求
缓存穿透:对于空请求返回一个默认值;使用布隆过滤器;限制非法请求。
Redis缓存是策略,动态缓存热点数据
数据存储受限,系统并不是将所有的数据都添加到缓存中,而只是将其中一部分热点数据缓存起来。
动态热点缓存的策略是:根据数据最新访问时间来进行排名,并过滤到不常用的数据,只留下经常访问的数据。
在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。
常见的缓存更新策略
Write Back:写回策略
Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
redis没有异步更新数据库的功能
常用在计算机体系结构中的设计,比如 CPU 的缓存、操作系统中文件系统的缓存
Cache Aside:旁路缓存策略
写:更新数据库数据,然后更新缓存数据
读:如果缓存命中,直接返回数据;缓存不在就去数据库查找,然后更新缓存
不能先删除缓存再更新数据库:会出现缓存和数据库数据不一致性的问题
适合读多写少的场景。
Write / Read Through:写穿读穿策略
read through:先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。
write through:当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:
- 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。
- 如果缓存中数据不存在,直接更新数据库,然后返回;
本地缓存可以这种策略;分布式缓存无法实现,因为没有此类功能
Redis如何实现延迟队列
使用场景:
- 订单超时未支付自动关闭
- 外卖十分钟没有接单会自动取消
Redis可以使用有序集合ZSet来实现延迟消息队列,ZSet的score属性可以赋值给延迟执行的时间,轮询比对利用arangebyscore来查询所有待处理任务,循环执行即可
Redis的大Key问题如何处理
大key:
String类型的值大于10kb
Hash,List,Set,ZSet类型元素超过5000个
大key带来的问题:
- 操作大key比较耗时,客户端阻塞,可能响应超时
- 获取大key产生的流量较大,可能引发网络阻塞
- del删除大key引发主线程阻塞
- 集群模型在slot分片均匀的情况下会产生数据和查询倾斜的问题,部分大key的几点内存占用多,QPS也比较大
如何找到大key
-
redis-cli -a “密码” --bigkeys 从节点或者小流量时期,该检测会阻塞主线程 ; 方法只会返回bigest key无法返回前n个数据;集合类型 只会统计menber数量,不会计算占用大小。
-
scan 命令 scan是基于游标的迭代器,scan命令对数据库扫描,然后type获取类型,根据不同类型使用不同命令来获取长度
string: STRLEN来获取字符串长度
集合类型:MEMORY USAGE来查询 -
RdbTools工具
RdbTools 第三方开源工具,可以用来解析 Redis 快照(RDB)文件,找到其中的大 key
rdb dump.rdb -c memory --bytes 10240 -f redis.csv
如何删除大key
不能直接删除大key:在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序。所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。
分批删除大Key:删除大 Hash,使用 hscan
命令,每次获取 100 个字段,再用 hdel
命令,每次删除 1 个字段。
删除大 List,通过 ltrim
命令,每次删除少量元素。
删除大 Set,使用 sscan
命令,每次扫描集合中 100 个元素,再用 srem
命令每次删除一个键。
删除大 ZSet,使用 zremrangebyrank
命令,每次删除 top 100个元素。
异步删除:用 unlink 命令代替 del 来删除, Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。
redis管道有什么作用
管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞。
管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能。
Redis支持事务回滚吗?
Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,已经执行过的命令结果也不会删除,起不到回滚的效果。
如何使用redis实现分布式锁
分布式锁用于控制分布式场景下某一个资源只能被某一个应用所使用。
Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件
- 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
- 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
- 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;
SET lock_key unique_value NX PX 10000
解锁的过程就是将 lock_key 键删除(del lock_key),要保证执行操作的客户端就是加锁的客户端。
解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
解锁是有两个操作,需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
if redis.call("get",KEY[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
基于rediss的分布式锁优点:
- 性能高效
- redis本身提供了setnx方法,实现简单
- redis可以集群部署,能够避免单点故障
缺点:
- 超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。看门狗机制去自动续约。
- Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。主节点获取到锁后还没同步就宕机了,新的主节点还能获取到锁。就失效了
分布式锁算法 Redlock(红锁):是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。这种算法对于集群设备之间的时序和系统时钟苛刻要求。
- 有界网络延迟(可以保证数据包始终在某个保证的最大值内到达 延迟),
- 有界进程暂停(换句话说,硬实时约束,通常只有 在汽车安全气囊系统等中找到),以及
- 有界时钟错误
系统 有 5 个 Redis 节点(A、B、C、D 和 E)和 2 个客户端(1 和 2)。如果时钟在一个上会发生什么 的 Redis 节点向前跳跃?
-
客户端 1 在节点 A、B、C 上获得锁定,由于网络问题,无法访问 D 和 E。
-
节点 C 上的时钟向前跳跃,导致锁过期。
-
客户端 2 在节点 C、D、E 上获得锁定,由于网络问题,无法访问 A 和 B。
-
客户端 1 和 2 现在都认为他们掌握着锁。
-
客户端 1 请求锁定节点 A、B、C、D、E。
-
当对客户端 1 的响应在传输中时,客户端 1 进入停止世界的 GC。
-
所有 Redis 节点上的锁都会过期。
-
客户端 2 在节点 A、B、C、D、E 上获得锁定。
-
客户端 1 完成 GC,并收到来自 Redis 节点的响应,指示其成功 获取了锁(当进程 暂停)。
-
客户端 1 和 2 现在都认为他们掌握着锁。
如何进行分布式锁定 — Martin Kleppmann 的博客
详解Redis底层数据结构
Dict
SDS
SDS,简单动态字符串
结构组成:
- len:字符串长度
- alloc:分配的空间大小,修改字符串时去判断已分配空间是否满足,否则就去扩容,避免了缓冲区溢出
- flags:结构类型,SDS支持多种结构类型,根据数据大小灵活分配合适类型,比如一个人的年龄用byte存储足够,那就不需要使用long或者int类型
- buf数组:存放具体数据
SDS与传统c语言的char类型对比有何优势?
- 传统的char必须以 \0 结尾,SDS通过len来标记出buf数据长度,即使中间有\0也不会中途被判结束,因此避免了二进制安全问题,并且能够存储图片、视频等二进制数据
- 通过len字段记录数据长度,更够避免在数据修改时造成缓冲区溢出,支持自动扩容
- 使用flags灵活选取合适的SDS数据结构类型,取消了结构体对齐填充,节省了空间
双向链表
就是自定义节点实现的双向链表,包含前驱节点,后继节点,value,同时封装List,包含了链表长度,头节点,尾节点。
优点:
- 定义了pre和next,获取某个节点的前置节点和后置节点只需要O(1)的复杂度,
- len属性表示节点数量,可以直接获取链表长度缺点:
缺点:
- 链表利用的是碎片化地址,无法很好的利用到CPU缓存,只有数组能。
- 需要保存额外字段,增加了内存开销
压缩列表
- zlbytes: 整个压缩列表占用的字节总数
- zltail:列表尾的偏移量
- zllen:节点数量
- zlend:压缩列表结束点
- prevlen: 前一个节点长度
- encoding:编码方式
- data:节点数据
查询首尾节点元素只需要o(1)复杂度,查询其他节点只能遍历O(n)复杂度,因此压缩列表不适合存储较多数据
如果前一个节点长度小于254,prevlen只需要1个字节,如果大于254就需要5个字节
当插入或者更新节点时,如果空间不够就会内存重分配,如果插入节点元素较大,可能导致后续元素的prvlen都发生变化,进而造成连锁更新,原因就在于增加的4个字节
Listpack
listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量,然后 listpack 末尾也有个结尾标识。图中的 listpack entry 就是 listpack 的节点了。
每个 listpack 节点结构如下:
主要包含三个方面内容:
- encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
- data,实际存放的数据;
- len,encoding+data的总长度;
可以看到,listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
跳表
zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。
哈希表
Redis采用拉链法解决Hash冲突,将Hash冲突的元素通过链表串联存储
Redis哈希表结构包含了两个哈希表,当hash表数据增多时便会触发rehash过程,
此时将ht[1]初始化为ht[0]二倍大小,让后将ht[1]中数据rehash后存入ht[2],最后互换地址
由于rehash过程涉及大量数据的拷贝,可能会造成阻塞,影响功能,因此采用渐进式rehash过程,客户端请求驱动rehash过程,也就是有请求就处理迁移一点数据。
QuickList
双向链表和压缩列表的组合,QuickList就是一个链表,而链表中的每个元素又是一个压缩链表
压缩列表的不足,虽然压缩列表是通过紧凑型的内存布局节省了内存开销,但是因为它的结构设计,如果保存的元素数量增加,或者元素变大了,压缩列表会有「连锁更新」的风险,一旦发生,会造成性能下降。
quicklist 解决办法,通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
quicklist 会控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,但是这并没有完全解决连锁更新的问题。
整数集合
当Set全部包含整数并且数量不多时就会使用整数集合
typedef struct intset {
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
整数集合的升级操作
当新元素类型比所有元素的类型都要长时,整数集合就会升级,拓展空间,能节约内存资源
整数集合不支持降级操作,主要是频繁的升降级会消耗CPU资源,
本文自小林Coding总结而来,感谢小林图解Redis