1.类型与数据结构
1.1 数据结构
-
简单动态字符串(scs)
-
链表(list)
-
字典(map)
是一种用于保存键值对的的抽象数据结构
-
跳跃表(skiplist)
跳跃表是一种有序的数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
-
整数集合(intset)
有序不重复数组 -
压缩表(ziplist)
由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩表包含多个节点,每个节点可以保存一个字节数组或整数值。
1.2 数据类型
数据类型底层的数据结构:
- 字符串 string:int(整型),embstr,raw(小于32字节)
- 列表 list:ziplist(压缩表),linkedlist(链表)
- 哈希 hash:ziplist(压缩表),hashtable(哈希表)
- 集合 set:intset(整数集合),hashtable(哈希表)
- 无序集合 zset:ziplist(压缩表),skiplist(跳表)
2.redis过期策略、内存淘汰策略
2.1 过期策略
- 惰性删除
对CPU时间友好:程序只会在读出键的时候进行过期检查。
对内存不友好:如果这个键没有被访问到,就不会被删除,内存不会被释放。 - 定时删除
对内存友好:可以保证过期键会尽可能快地被删除,释放内存。
对CPU时间不友好:过期键比较多的时候,删除行为会占用较多CPU时间,在请求高的情况下,会影响服务器的响应时间和吞吐量。 - 定期删除
前两种方案的中和:每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的频率和时长来减少删除操作对CPU时间的影响。需要根据情况合理设置删除操作的时长和频率。
2.2 淘汰策略
lru、lrf、random、ttl、不删除直接报错
3.持久化
3.1 RDB
SAVE和BGSAVE命令用于生产RDB文件。
SAVE会阻塞Redis进程,直到RDB文件创建完毕之前,服务器不能处理任何命令请求。
BGSAVE会派生一个子线程负责创建RDB文件,服务器进程继续处理请求。
3.2 AOF
- 命令追加:服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到服务器状态的aop_buf缓冲区的末尾。
- 文件写入与同步:redis.comf文件参数flushAppendOnlyFile配置同步策略。
always:每次将aop_buf缓冲区同步到AOF文件。
everysec(默认):每秒同步。
no:何时同步由操作系统决定。 - AOF重写:由于追加式写入会导致AOF文件越来越大,所以需要AOF重写来减小文件体积。用一条命令记录键值对来代替多条对这个键值对的命令。
AOF重写是通过子线程实现的,过程中数据的数据不一致性问题:设置了一个AOF缓冲区,执行AOF重写时会将写命令同时发送到AOF缓冲区和AOF重写缓冲区,重写完成后,缓冲区的命令会追加到重写的AOF文件末尾。
redis启动时自动检测文件并还原数据库,载入文件期间处于阻塞状态。
如果服务器开启了AOF持久化功能,会优先使用AOF文件来还原数据库状态。只有在AOF关闭的时才是用RDB还原数据库状态。
RDB保存键值对,AOF保存命令。
4.事件
Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:
- 文件事件
- 时间事件
4.1 文件事件
Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器。
- 文件事件处理器使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
4.1.1 构成
四个组成部分:套接字、I/O多路复用程序,文件事件分派器,事件处理器。
尽管多个文件事件可能并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的时间被处理完后,I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
4.1.2 I/O多路复用程序
Redis的I/O多路复用程序的所有功能都是包装常见的select、epoll、evport、和kqueue这些I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件,比如ae_select.c、ae_epoll.c、ae_kqueue.c等。
程序会在编译时自动选择系统中性能最高的I/O多路复用函数来作为Redis的I/O多路复用程序的底层实现。
4.1.3 文件事件处理器
连接应答处理器、命令请求处理器、命令回复处理器、复制处理器。
4.2 时间事件
类型:定时时间,周期性时间。
服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。
5.集群
5.1 主从模式
5.1.1 复制的实现
执行SLAVEOF命令或者设置slaveof选项:
- 同步:将从服务器的数据库状态更新至主服务器当前所处的状态。主服务器将写命令发送给从服务器执行。
- 命令传播:主服务器状态被修改时,让主从服务器的数据库重新回到一致状态。从服务器发送PSYNC。PSYN具体完整重同步和部分重同步模式。
5.1.2 完整重同步和部分重同步
完整重同步用于初次复制。
主服务器收到SYNC命令后执行BGSAVE命令,生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有命令。从服务器接收RDB文件,执行完后再接收缓冲区的所有写命令。
部分重同步用于断线重连。
- 主从服务器分别维护一个复制偏移量,主服务器传播N个字节偏移量加N,从服务器接收N个字节偏移量加N。
- 复制积压缓冲区是由主服务器维护的一个固定长度的先进先出队列,默认大小为1MB。进行命令传播时会同时写入队列,保留最近执行的写命令和复制偏移量,用于断线重连。
- 每个服务器都有自己的服务器ID,断线重连时根据ID执行部分重同步操作。
5.1.3 复制过程
- 设置主服务器的地址和端口(slaveof选项)
- 建立套接字连接
- 发送PING命令
- 身份验证(masterauth选项)
- 发送端口信息
- 同步
- 命令传播
- 心跳检测
5.2 哨兵模式
5.2.1 概念
Sentinel是Redis的高可用解决方案:Sentinel实例组成的Sentinel系统监听主从服务器,当主服务器下线时,自动将某个从服务器升级为主服务器。在旧的主服务器重新上线后,将新的主服务器降级为从服务器。
- 检测主观下线:每秒一次发送PING命令,超过一定时长(down-after-milliseconds选项)没有回复,则认为主观下线。
- 检查客观下线:向其他监视这台主服务的Sentinel进行询问,接收足够数量的下线判断后,判断为客观下线,并执行故障转移。
- 选举领头Sentinel:监视这个下线主服务器的个个Sentinel进行协商,选举出一个领头Sentinel进行故障转移
- 故障转移:将某个从服务器转换为主服务器,将其他从服务器的复制目标改为新的主服务器,将已下线的主服务器设置为新的主服务器的从服务器,当旧的主服务器从新上线时,会变成新的主服务器的从服务器。
5.2.2 选举Sentinel过程
- 每个发现主服务器客观下线Sentinel都会要求其他Sentinel给自己投票。
- 先到先得原则:收到被要求投票Sentinel只会给第一个Sentinel投票,不会再接收其他Sentinel的投票要求。
- 被半数以上的Sentinel投票后将成为领头Sentinel。
- 在给定的时间内没有选出领头Sentinel,过一段时间将再次进行选举。
5.2.3 新主服务器的的选举
- 删除已下线的所有从服务器
- 按服务器的优先级选出优先级最高的从服务器。
- 如果优先级相同,选举偏移量最大的从服务器(偏移量越大代表数据保存的最新)
- 如果偏移量相同,按照ID排序,选择ID最小的从服务器。
5.3 分片集群
Redis集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。
节点
一个集群由多个节点组成,节点通过握手(CLUSTER MEET命令)将其他节点添加到自己的集群。
槽指派
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot)。
通过向节点发送**CLUSTER ADDSLOTS命令,可以将一个或多个槽指派给节点负责。
每个槽都有节点在处理时,集群处于上线状态,,数据库有任何一个槽没得到处理,集群处于下线状态。
当客户端发送与数据库键有关的命令时,接收命令的节点会计算(Hash)键属于哪个槽,如果不是自己处理的槽,则返回MOVED命令移动至下一个节点,直到找到对应的节点。
通过跳跃表保存槽和键值对间的关系。
6.事物
6.1 实现
Redis通过MULTI、EXEC、WATCH等命令来实现事物功能。事物提供了一种将命令打包,然后一次性、顺序地执行多个命令的机制,并且在事物中的所有命令执行完后再去处理其他请求。
- MULTI标志事物开始
- 中间的命令放入一个事物队列
- EXEC提交后一次性执行所有命令
- 不具备回滚能力
6.2 WATCH
WATCH是一个乐观锁,可以在EXEC执行前监听数据库键是否被修改,如果被修改,则拒绝执行事物。
6.Lua脚本
6.1 Lua环境
为了在Redis服务器中执行Lua脚本,Redis在服务器内嵌了一个Lua环境。并载入多个函数库,让Lua脚本来使用。
6.2 伪客户端
Redis服务器专门为Lua环境创建了一个伪客户端,并由这个客户端负责处理Lua脚本中包含的所有Redis命令。
lua_scripts字典
6.3 脚本执行
Lua脚本通过EVAL命令执行。
EVAL “return ‘hello world’”
执行过程:
- 根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数。函数名称由f_前缀加上脚本的SHA1校验和组成,函数体是脚本本身。
- 将客户端给定的脚本保存到lua_scripts字典,等待将来进一步的使用。这个字典的键为某个Lua脚本的SHA1校验和,值则是对应的Lua脚本。
- 执行刚刚在Lua环境中定义的函数,以此来执行客户端给定的Lua脚本
EVALSHA命令根据EVAL脚本的SHA1校验和来执行脚本,但这个命令要求对应的Lua脚本至少被EVAL执行过一次。
6.4 脚本管理命令
SCRIPT FLUSH 用于清除所有的Lua脚本信息。
SCRIPT EXISTS 根据输入的SHA1校验和判断脚本是否存在。
SCRIPT LOAD 创建Lua脚本并保存到lua_scripts字典中。
SCRIPT KILL 用来在脚本执行超时停止脚本。
7.客户端
Jedis,Redisson,Lettuce
Redis三种客户端对比
8. 面试
redis用途
缓存、分布式锁、排序、发布订阅、消息队列
redis为什么单线程还那么快
- 基于内存操作
- 单线程避免了锁的复杂性,以及上下文切换的花销
- 使用IO多路复用:Reactor模式、NIO、selector、epoll
缓存与数据库的一致性如何保证
延时双删,然后采用缓存穿透的方式同步redis
使用binlog
缓存穿透、缓存击穿、缓存雪崩的含义及解决方法
名称 | 含义 | 解决方法 |
---|---|---|
缓存雪崩 | 大量缓存同时过期,全部请求都直接访问数据库,从而导致数据库的压力骤增 | 均匀设置过期时间、互斥锁、双 key 策略、后台更新缓存 |
缓存穿透 | 某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮 | 互斥锁方案; 不给热点数据设置过期时间,由后台异步更新缓存 |
缓存击穿 | 当用户访问的数据,既不在缓存中,也不在数据库中,没办法构建缓存数据,那么当有大量这样的请求到来时,数据库的压力骤增 | 非法请求的限制; 缓存空值或者默认值 布隆过滤器 |
如何设计redis分布式锁
key的唯一性、原子性操作,失效时间等等
如何避免死锁
用setnx()原子性操作设置key和过期时间
key失效方案
时间续费机制
主从模式/哨兵模式下的redis分布式锁设计
RedLock(红锁)、联锁 、 数据库锁补充
redis分布式锁和zookeeper分布式锁的区别
实现原理、性能、获取锁、死锁等方面
Redis和Zookeeper实现分布式锁
分布式缓存如何扩容
一致性hash
参考书籍:《Redis设计与实现》