八股文之Redis

REDIS 专题。版本:3.2

数据结构

基础数据结构

一. 字符串 string

底层实现为简单动态字符串(SDS),是可修改的字符串,采用预分配冗余空间的方式来减少内存的频繁分配。

SDS数据结构主要由len、free、buf[]这三个属性组成。

  • len:buf数组已用的长度。

  • free:buf数组未用的长度。

  • buf[]:存储字符串

空间预分配:

对SDS字符串修改后,如果需要扩容,那么不仅为SDS分配此次扩展的空间,还会分配额外的空间,待下次使用。

惰性空间释放:

对SDS字符串缩短后,不会立即回收多余的空间,而是更新free的值,方便下次使用。

可以通过object encoding 查看对象编码。 SDS类型内部编码有3中:

  • int:整型。

  • embstr:小于等于44字节。

  • raw:大于44字节。

embstr和raw都使用redisObject(对象包装类)和sds保存数据,embstr使用只分配一次内存空间,raw需分配两次内存空间(redisObject和sds内存地址不连续)。

redisObject定义:

 typedef struct redisObject {
        unsigned type:4; /* 类型 */
        unsigned encoding:4; /* 编码 */
        unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) 内部时钟,记录对象最后一次被访问的时间 */ 
        int refcount; /* 记录对象引用次数 */
        void *ptr; /* 指向底层数据结构的指针 */
    } robj;

二. 列表 list

可用来做异步消息队列。rpush/lpush 存入列表和散key, 取列表值后,成功处理则删除散key, 保证数据不丢失。如取队列值后处理失败,定时任务重push散key进入队列。

较早版本为 ziplist压缩列表O(n), linkedlist 双向链表O(n);
3.2后底层实现为 quicklist

ziplist是内存空间连续的有序数据结构。存储效率高,内存开销少。但插入删除操作时,需要频繁申请和释放内存。

quicklist:linkedlist 和 ziplist 的结合,quicklist 将linkedlist 按段切分,每一段使用ziplist 来紧凑存储,多个ziplist 之间使用双向指针串接起来。

list-max-ziplist-size -2 : 每个quicklist节点上的ziplist大小不能超过8Kb。超出这个字节会增加一个ziplist。

三. 集合 set

无序、自动去重的集合数据类型。底层实现:哈希表O(1), inset 整数数组O(n)。

四. 哈希 hash

适合用来缓存对象信息。底层实现为:hashtable 哈希表O(1), ziplist 压缩列表O(n)。

五. 有序集合 zset

有序、自动去重的集合数据类型。底层实现为:压缩列表O(n), skiplist 跳表O(logN)。

redis.conf配置文件中有定义两个参数:

  • zset-max-ziplist-entries 128 有序集合元素个数,默认128。

  • zset-max-ziplist-value 64 元素字节数,默认64字节。

zset集合满足上面两个条件,使用ziplist(压缩列表)编码,如果某个元素字节数大于64,会转换成skiplist(跳表)编码。

跳跃表

跳表:将有序链表改造为支持“折半查找”算法,通过建立索引层,可以快速的插入、删除、查询操作。

跳表的数据结构主要有两个,zskiplist(定义跳表头尾指针、长度、层级信息),zskiplistNode(跳表的结点信息,包含前后指针)。

假设目前6个元素,插入如下图所示。第一层L0是双向链表,判断结点是否加入到上一层是根据一个随机层函数来判断。有1/4的概率决定是否将当前结点加入到上一层。

最高层级level = 32。

举例插入一个score=3.5 的结点。当前最高为3层,level=3。层级结构level[]是一个数组,也就是从L2层开始查找下一个元素forward,5 > 3.5,不满足继续找。最终在L0层插入3.5结点,改变指针地址。

伪代码
update[]数组记录每一层插入位置的前一个节点。
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;

跳表删除操作和插入逻辑类似,比如删除x 结点,从最高层往下找,更新update[]数组,记录每层x 的前一个结点指针backward,把x从跳表上逐层删除并更新指针地址。

zset底层结构用平衡二叉树也能实现,但skiplist实现起来简单,平衡二叉树需左旋右旋来保持平衡,实现起来复杂。

布隆过滤器

数据结构:一个大型的位数组和几个不一样的无偏(元素算的比较均匀)hash函数

原理: 计算key时,使用多个hash函数对key进行hash算得整数索引值然后对位数组长度取模得到一个位置,每个hash函数都会算得一个不同位置,再把位数组的这几个位置都置为1完成操作。

查询:先把hash几个位置都算出来,看看位数组几个位置是否都是1,只要有一个位为0,那说明布隆过滤器中这个key不存在,如果都是1,也不一定存在,因为有可能被其它key占用的位置(下图)。所以数组空间比较大,判断准确率高;空间比较小,准确率低。

redis官方在4.0提供了布隆过滤器插件。

docker部署包含布隆过滤器的redis镜像
命令:
docker search redis
docker pull redislabs/rebloom
docker run --name redislabs -p 6379:6379 -d redislabs/rebloom
docker exec -it redislabs bash

redis指令:

  • bf.reserve:创建自定义布隆过滤器。其中有个错误率参数:error_rate。error_rate越小,需要存储空间就越大。

  • bf.add:添加元素到布隆过滤器。

  • bf.exists:查看value值是否存在。

  • bf.mexists:判断多个值是否存在。

应用场景:

  1. 对爬虫url过滤,大幅降低去重存储消耗,会错过少量页面;

  2. 黑名单校验,垃圾邮件过滤;

  3. 解决缓存穿透,过滤掉一定不存在的查询请求。

持久化策略

一. RDB

redis配置文件默认rdb备份,将数据保存到磁盘,文件名为dump.rdb。
自动触发条件:900秒/1次,或300秒/10次,或60秒/10000次 key被改变则快照保存。

redis.conf:
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb

原理:

  • 自动触发:redis父进程会调用bgsave 来fork 出一个子进程,子进程创建RDB文件,父进程继续处理请求。

  • 手动触发:调用save命令,但会阻塞redis。

fork产生的子进程使用了linux的copy-on-write 机制

copy-on-write(写时复制):
子进程共享主进程的物理内存空间,子进程和主进程虚拟地址指向同一份物理内存空间。
主进程fork子进程之后,内核会把主进程中所有内存页权限设置为只读。
Redis子进程拷贝的时候,主进程有新的写命令,cpu硬件检测页是只读,会触发页异常中断。内核会把触发异常的页(也就是要修改的数据页,linux内存管理以页为单位,默认4kb,修改的数据对应内存空间中的某一页)复制一份。主进程对应的指针指向新的数据页,子进程指向旧的数据页。

缺点:数据会丢失,fork子进程时会阻塞。

二. AOF

AOF持久化默认关闭。如果需要保证数据完整性和实时性的话,可以开启AOF。如rdb和AOF同时开启,默认加载AOF。

AOF主要采用文件追加方式,文件过大会fork一个子进程重写;重写过程不是读取旧AOF文件,而是将内存中的数据用命令的方式重写生成新的AOF文件。

appendonly no
# AOF文件名
appendfilename "appendonly.aof"
# 同步频率 每秒
appendfsync everysec
# 日志重写时,no指进行命令追加操作,修改数据会出现IO阻塞
no-appendfsync-on-rewrite no
# 自动重写条件一:AOF配置默认达到百分比重写 100%,达到上次重写文件大小1倍
auto-aof-rewrite-percentage 100
# 自动重写条件二:重写aof最小文件大小
auto-aof-rewrite-min-size 64mb

自动触发需满足上面重写条件;手动触发执行bgrewriteaof命令。

原理:
AOF持久化功能的实现可以分为3个步骤:命令追加、文件同步、文件重写。

1. 追加
所有写入命令追加到AOF缓冲区。

2. 同步
同步文件策略由参数appendfsync控制:
always: 主线程命令写入aof_buf后调用系统write写入内核空间,接着调用系统fsync把内核中修改数据写入文件,完成后主线程返回,这段期间主线程是阻塞的。可靠性较高。
no: 主线程命令写入aof_buf后调用系统write操作后返回。由操作系统负责同步。
everysec: 默认配置。 主线程命令写入aof_buf后调用系统write操作后返回。另起同步线程调用fsync函数写入磁盘。

3. 重写 bgrewirteaof
redis主进程fork子进程后,子进程基于当前内存中的数据构建日志,往临时aof文件写入日志。fork子进程写文件和rdb快照保存操作类似。
fork完成后,主进程继续响应客户端命令
所有修改命令写入aof文件缓冲区并根据appendfsync策略同步到磁盘(旧的aof文件),也会写入到aof重写缓冲区
子进程写完后通知主进程。主进程把fork子进程之后产生的修改命令,也就是aof重写缓存区的数据写入新的aof文件。
使用新的aof文件替换老的aof文件,完成aof重写。

为什么重写期间需要写两个缓冲区,防止重写期间宕机不至于丢失fork操作之后的所有命令。

缺点:AOF文件过大;恢复时间较慢。

淘汰算法

LRU:最近最少使用,根据数据的历史访问记录来淘汰数据。核心思想是:如果数据最近被访问,那么将来被访问的几率也更高。

参考leetcode 146题解法思路:数据结构为双向链表 + 哈希表。双向链表用于存储元素,哈希表查询元素,时间复杂度O(1)。

LFU:最不经常使用,按访问频次来淘汰最少被访问的数据。

参考leetcode 460题解法思路:双向链表实现,维护两个哈希表。hash1 key为频次count, value为双向链表。hash2 key为键值,value为单个结点。

get时在hash2 中找到结点的访问次数,从hash1 中通过次数找到双向链表,移除此结点,加入到key为 count+1 的双向链表中。

put时容量不够,删除最小频率缓存。然后构建访问频次为1 的key加入hash1 中。

Redis LRU:
相关淘汰策略配置:

# 设置内存使用上限
maxmemory <bytes>
# 清理缓存策略
maxmemory-policy noeviction
# 随机采样的数量,选择一个最近最少使用的key删除
maxmemory-samples 5

maxmemory-policy 策略机制:

  • noeviction 不淘汰,超过内存后返回错误。

  • allkeys-lru 所有key采用lru算法淘汰。

  • volatile-lru 设置过期时间的key采用lru算法淘汰。

  • allkeys-random 随机删除key。

  • volatile-random 随机删除设置了过期时间的key。

  • volatile-ttl 按过期时间删除key。

Redis为了节约内存,采用近似LRU算法实现。原理:通过随机获取采样的数据,从中淘汰key。

Redis LFU:
redis4.0 新增 lfu淘汰机制。相关配置:

# 设置过期时间的键按lfu算法淘汰
volatile-lfu
# 所有key按lfu算法淘汰
allkeys-lfu

lfu算法把原来key对象的内部时钟24bit 分为高16位和低8位。

  • 高16位记录上一次递减时间。根据衰减周期计算递减时间。

  • 低8位记录访问次数。最大为255,超过这个访问次数不再递加。并随递减时间更新访问次数。

集群模式

Redis 集群是一个提供在多个Redis节点间共享数据的程序集。

Redis 集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。

Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。

Redis 集群的优势:

  • 自动分割数据到不同的节点上。

  • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

Redis 集群拓扑图:

所有集群节点通过tcp连接和一个二进制协议建立通信。 每一个节点都通过cluster bus 与集群上的其余每个节点连接起来。 节点们使用一个gossip协议来传播集群的信息。

由于集群节点不能代理请求,所以客户端在接收到重定向错误 -moved 和 -ask的时候,将命令重定向到其它节点。

主从节点间采用异步复制。

currentEpoch
当前纪元,记录集群状态变更的递增版本号。作用:适用于故障转移流程。

集群选举机制:

  1. 从节点会将自己记录的currentEpoch +1, 并广播一个FAILOVER_AUTH_REQUEST 数据包给集群里的每个主节点请求选票;

  2. 主节点如果给从节点投票,则回复FAILOVER_AUTH_ACK 消息,在一个给定的时间段内,一个主节点只能投一次票;

  3. 从节点忽略epoch比当前epoch小的回应acks;

  4. 从节点收到了大多数回应,赢得了选举;

  5. 用ping或pong数据包向其它节点宣布自己是主节点,并提供哈希槽信息,其它节点升级自己的配置信息。

从节点不是在主节点fail状态后立马发起,而是在一个延迟时间后。

分布式锁

实现分布式锁需满足条件:

  • 互斥性。在任意时刻,只有一个客户端能持有锁。

  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

  • 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。

redis 分布式锁
原子命令: set key value [EX seconds] [PX milliseconds] [NX|XX]。
设置一个不存在的key, 并加入过期时间。

单实例redis 分布式锁命令:set xiaoli 123 ex 3 nx。 如果key xiaoli不存在则写入key,并设置过期时间为3秒。

总结分布式锁问题对应解决方案:

  • 死锁问题:设置过期时间。

  • 锁过期:守护线程,锁自动续期。

  • 释放别人的锁:锁写入唯一业务id。

  • 可冲入锁:计数器。

由于单实例redis 加锁只作用在一个节点上。即使通过sentinel保证高可用,但master节点由于某些原因发生主从切换,则会出现锁丢失。

所以redis 作者基于分布式环境提供的实现方式:redlock
redlock基本原理: 有n个master节点,相互独立,写锁满足n/2+1 以上成功就代表加锁完成。

开源项目 redisson 是在redis 基础上实现的一套开源解决方案。 在分布式锁基础上提供了:

  • RedissonLock 分布式可重入锁。 加锁只作用在一个redis节点上。支持可重入,失败重试,可设置锁最大等待时间。

  • RedissonRedLock 分布式红锁。 实现了Redlock介绍的加锁算法,将多个RLock对象关联为一个红锁,在大部分节点上加锁成功就算成功。

[1] [2]

参考资料

[1]

redis中文网站: http://www.redis.cn/

[2]

知乎问答:怎么实现分布式锁: https://www.zhihu.com/question/300767410

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值