Redis是基于内存亦可持久化的日志型、Key-Value数据库。
Redis的使用场景
- 抗峰值-读场景
- 分布式锁
- 热点数据缓存
- 后端接口数据缓存
- 异步队列
- 通知
数据结构:
- String
- Redis的字符串是动态字符串,可以修改的字符串,类似于ArrayList,采用分配冗余空间来减少频繁的内存分配,并且支持扩容;
- Redis的数值型,可以进行自增操作,是由范围的,是signed long的最大值最小值;
- 字符串底层原理
- Redis 的字符串叫着「SDS」,也就是Simple Dynamic String。它的结构是一个带长度信息的字节数组
- Redis 规定字符串的长度不得超过 512M 字节
- List
- 类似Java的链表-LinkedList,插入和删除都是O(1),查询是O(N);
- 提供入队列、弹出等功能
- 可以用在异步队列
- List底层:ZipList和QuickList
- Hash
- 类似Java的HashMap,数组+链表,不过,rehash比较耗时,所以,采用的是渐进式rehash,即同时保留新老两个结构;
- 对于存储一个对象而言,Hash结构可以单独对字段进行操作,字符串结构则需要整体取出;Hash结构比字符串结构更消耗Redis存储资源,字符串结构比Hash结构耗费网络资源;
- Set
- 类似于Java的HashSet,是无序唯一的,是特殊的字典,字典的value是Null;
- ZSet
- 类似于 Java 的 SortedSet 和 HashMap 的结合,一方面具有Set的唯一性,另一方面每个key对应value存储的是score(权重),用于排序;
- 跳跃列表采取一个随机策略来决定新元素可以兼职到第几层:
- 首先 L0 层肯定是 100% 了
- L1 层只有 50% 的概率
- L2 层只有 25% 的概率
- L3 层只有 12.5% 的概率
- 一直随机到最顶层 L31 层
底层结构
- SDS:Simple Dynamic String。它的结构是一个带长度信息的字节数组,Redis用于实现键值、String
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位,不理睬它
byte[] content; // 数组内容
}
typedef struct listNode {
struct listNode *prev;//前置节点
struct listNode *next;//后置节点
void *value;//节点的值
} listNode;
typedef struct list {
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//节点值复制函数void *(*dup)(void *ptr);
//节点值释放制函数void (*free)(void *ptr);
//节点值对比函数int (*match)(void *ptr, void *key);
//链表所包含的节点数量
unsigned long len;
} list;
- 字典:用于保存键值对的数据结构,Redis用于实现Hash、Set
- 链地址法解决键冲突
- 字典中的ht属性是一个包含两个项的数组,数组中的每个顶都是一个dictht哈希表,一般情况下只使用ht[0]哈希表,ht[1]哈希表只会在进行rehash时使用
- 渐进式rehash
# 字典
typedef struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash索引
//当rehash不在进行时,值为-1
int rehashidx;
}dict;
# 哈希表
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht;
# 哈希表节点
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry;
- 跳表:跳跃表是一种有序的数据结构,跳跃表查找平均复杂度O(logN)、最坏时是O(N),可以和平衡树媲美
# 跳跃表节点
typedef struct zskiplistNode {
//成员对象
robj *obj;
//分值
double score;
//后退指针
struct zskiplistNode *backward;
//层—每个层元素都包含一个指向其他节点的指针,可以通过指针快速访问其他节点
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned int span;//跨度
} level[];
} zskiplistNode;
# 跳跃表:zskiplist 持有 跳跃表节点,可以更方便的对整个跳跃表进行处理,表头、表尾
typedef struct zskiplist {
//表头节点和表尾节点
struct zskiplistNode *header, *tail;
//表中节点的的数量
unsigned long length;
//表中层数最大的节点层数int level;
} zskiplist;
- 整数集合:是Redis用于保存整数值的集合抽象数据结构
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
分布式锁:
- 原理:
- setnx——缓存中不存在则进行设置value,否则设置失败;
- lua脚本保证多个指令的原子性
- 问题1:超时问题——增加超时时间,避免持有锁的线程长时间不释放,导致其他线程一直拿不到锁;setnx px
- 问题2:锁持有问题——对于锁的排斥的判断,需要增加锁的值的唯一性,一般采用随机值或者线程ID等来证明是某个线程的持有;
- 问题3:锁可重入性——应该记录同一个线程持有锁的计数,可以使用hreadLocal实现,Redis使用Hash结构记录某个线程请求锁次数(Redission);
- 问题4:锁续约问题——如果某个线程执行时间超过锁设置的超时时间的话,会导致逻辑没有执行完成,锁已经释放问题,需要有持续加锁的逻辑;
- 问题5:锁信息同步问题——在分布式环境下,当存有锁信息的Redis节点宕机了,而锁信息还没有同步到从节点,将导致锁失效;
- 解决方案:RedLock算法,在超时时间内,向集群大部分机器加锁,如果失败,则依次删除锁
- 优缺点:
- 锁的询问需要线程不断地询问,Zookeeper分布式锁基于监听实现,不需要线程不断地询问;
- 线程死掉,其他线程只能在到了超时时间才能获得锁,无法在线程失败时直接去获得锁;
Redis的单线程
- 多路复用:单线程实现,通过epoll实现I/O的多路复用;
- 指令队列:为每个socket连接关联一个指令队列,客户端的指令通过指令队列排队进行顺序处理,先到先处理;
- 响应队列:为每个socket连接关联一个响应队列,Redis 服务器通过响应队列来将指令的返回结果回复给客户端。
Redis的持久化
- AOF
- AOF 日志是连续的增量备份,AOF 日志记录的是内存数据修改的指令记录文本;
- AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录
- RDB:
- 快照备份,快照是一次全量备份,快照是内存数据的二进制序列化形式,在存储上非常紧凑;
- Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求;
- 子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中;
- 会使用操作系统的 COW 机制来进行数据段页面的分离
- 数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改
- 子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据
- Redis 4.0 混合持久化
- 将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志
Redis管道
- I/O耗时主要消耗在缓冲区,比如:
- write耗时在写缓冲区时,写满了,然后等待缓冲区重新变空,这个等待过程就是一次整个缓冲区的网络传输过程;
- read耗时在读缓存区时,缓冲区还为有数据,等待缓冲区有数据,这个等待过程也是一次整个缓冲区的网络传输过程;
- 而管道是基于客户端实现的,批量进行操作,使读写请求更加连续,而节省了单个读写请求的等待时长
Redis事务
- 事务通常包括begin、commit 和 rollback,Redis是multi、exec、discard;
- 服务端在接收到 exec 之前,会将指令缓存中事务队列,执行完成后一次性返回结果;
- 并不能真正的保证原子性,因为某个指令失败,后续的指令还会继续执行;
- 通常会配合管道使用,来节省网络开销;
Redis集群
- 主从同步:当网络发生分区时,Redis保证可用性
- Sentinel
- 通过使用一组哨兵,来对外提供服务,可以看成是一组Zookeeper节点
- 客户端连接到哨兵上,而不是直接连接集群上,这样当集群的主节点挂掉,哨兵会重新选取新的主,并在客户端向哨兵询问时,返回新主给客户端;
- 哨兵无法保证数据完全不丢失,不过提供两个参数尽可能的少丢失数据
- min-slaves-to-write:表示主节点必须至少有一个从节点在进行正常复制,否则就停止对外写服务,丧失可用性
- min-slaves-max-lag:它的单位是秒,表示如果 10s 没有收到从节点的反馈,就意味着从节点同步不正常,要么网络断开了,要么一直没有给反馈
- Codis
- 采用数据分片机制,将所有的key划分为1024个slot,对传进来的key进行运算,对于计算之后的整数值进行对1024取模得到对应的槽位,每个槽位都会映射到后面的实例上,
- Codis会维护槽位和实例的映射关系,当客户端进行请求时,可以通过Codis转发到Redis节点上
- 由于数据分片,所以牺牲了很多redis的功能,比如事务等
- 映射关系可以使用Zookeeper进行存取,解决了数据同步问题
- 支持扩容
- Cluster
- 是Redis官方的集群方案,去中心化的
- 没有代理层,访问每个节点都可以知道要访问的数据在哪个节点上
- RedisCluster集群内部实现了一套分布式协议,来保证元数据同步、主从切换
Redis监控
- Info
- Server 服务器运行的环境参数
- Clients 客户端相关信息
- Memory 服务器运行内存统计数据
- Persistence 持久化信息
- Stats 通用统计数据
- Replication 主从复制相关信息
- CPU CPU 使用情况
- Cluster 集群信息
- KeySpace 键值对统计数量信息
实战问题
- 单线程的Redis性能为何这么高?
- 基于内存
- 使用单线程,避免上下文切换
- 数据结构在内存使用上进行了极致的优化
- I/O多路复用
- 如何保证操作的原子性?
1、《Redis深度历险》
2、https://www.cnblogs.com/enochzzg/p/11294773.html
3、https://www.cnblogs.com/wanglijun/p/8797069.html
4、《Redis 设计与实现》