《Redis设计与实现》读书笔记
2022-10-12 15:19:28 0 举报
AI智能生成
Redis
作者其他创作
大纲/内容
第1章 引言
2.1 SDS定义
C字符串获取字符串长度的时间复杂度是O(n),SDS的是O(1),因为SDS定义结构有长度len属性,而C字符串需要重新计算。
获取字符串长度的时间复杂度为O(1)
C字符串存在内存溢出覆盖相邻区域内存的问题,SDS修改时会先检查空间大小是否满足需求,如不够,则先进行空间扩展之后再执行。(空间扩展方案参考2.3)
杜绝缓冲区溢出
C字符串拼接操作时需要先扩展底层数组空间(否则“缓冲区溢出”);截断操作时要释放不再使用的空间(否则“内存泄露”)。
若对SDS进行修改后,len<1MB,分配与len同样大小的未使用空间。此时buf数组的实际长度为len+free+1byte(1为表示结束的空字符)
若对SDS进行修改后,len>=1MB,则分配1MB的未使用空间。此时buf数组的实际长度为len+free(1M)+1byte。
通过空间预分配,减少内存分配次数,将连续增长N次字符串由原来的必定N次重分配降低为最多N次。当然,缺点是会有一定内存损耗,在当前成本下是可以接受的。
分配策略
SDS进行字符串增长操作,程序会分配必要空间,同时还分配额外的未使用空间,然后更新free属性值。
空间预分配
SDS进行字符串缩短操作,程序不会立即使用内存分配来回收多出的空间,而是记录在free属性。
redis的淘汰策略
比如什么具体场景?
真正需要时,通过SDS提供的API释放。
惰性空间释放
SDS内存分配
减少修改字符串时带来的内存重分配次数
C字符串不能包含空字符,导致只能存文本,不能存多媒体(图片、视频、音频、压缩文件等)类的二进制数据。
SDS依据len的属性值判断数组是否结束,所以buf内容可以是任何二进制数据。仍在buf结尾加上'\\0',是为了方便调用C语言API对字符串进行处理,即兼容部分C字符串函数<string.h>。
二进制安全
2.2 SDS与C字符串的区别
2.3 SDS API
K-V键和值都可能是SDS
用作缓冲区,AOF持久化的缓冲区,以及客户端状态中的输入缓冲区
在Redis中的应用
补充
第2章 简单动态字符串
3.1 链表和链表节点的实现
3.2 链表和链表节点的API
List
发布与订阅、慢查询、监视器
第3章 链表
字典(dict)
哈希表(dictht)
哈希表节点(dictEntry)
字典
4.1 字典的实现
4.2 哈希算法
4.3 解决键冲突
4.4 rehash
4.5 渐进式hash
Redis数据库、Map
第4章 字典
跳跃表
5.1 跳跃表的实现
zset
在集群节点中用作内部数据
第5章 跳跃表
6.1 整数集合的实现
6.3 升级的好处
6.4 降级
6.2 升级
6.5 整数集合API
set
第6章 整数集合
7.1
List、Map
第7章压缩列表
HyperLogLog 是用来做基数统计的算法
什么是基数?
在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。
优点
只存储计算基数,不会存储元素本身。
缺点
添加指定元素到 HyperLogLog 中。
PFADD key element [element ...]
返回给定 HyperLogLog 的基数估算值。
PFCOUNT key [key ...]
将多个 HyperLogLog 合并为一个 HyperLogLog。
PFMERGE destkey sourcekey [sourcekey ...]
命令
Redis HyperLogLog
Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。
添加地理位置的坐标。
geoadd
获取地理位置的坐标。
geopos
计算两个位置之间的距离。
geodist
根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
georadius
根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
georadiusbymember
返回一个或多个位置对象的 geohash 值。
geohash
Redis GEO
扩展
数据结构
8.1 对象的类型与编码
8.2 字符串对象
8.3 列表对象
8.4 哈希对象
8.5 集合对象
8.6 有序集合对象
8.7 类型检查与命令多态
8.8 内存回收
8.9 对象共享
8.10 对象的空转时长
第8章 对象
第一部分 数据结构与对象
struct redisServer { // ... // 一个数组,保存着服务器中的所有数据库 redisDb *db; // ... //服务器的数据库数量 int dbnum; // ...};
Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库。
dbnum默认值16
生产中尽量避免使用这个命令,因为Redis没有提供查看当前数据库的功能,容易忘了当前是哪个数据库。
通过SELECT命令选择切换数据库。
typedef struct redisClient {// ...//记录客户端当前正在使用的数据库redisDb *db;// ...} redisClient;
Redis客户端通过redisDb指针指向当前使用的数据库。
初始化redis服务器时,根据dbnum决定创建多少个数据库。
服务器中的数据库
typedef struct redisDb { // ... // 数据库键空间,保存着数据库中的所有键值对 dict *dict; // ...} redisDb;
Redis是键值对数据库,所有的键值对都保存在redisDb的字典中,谓之“键空间”。
即key,都是SDS字符串对象。
Sting
Set
Zset
Map
键空间的值
键空间的键
新增、更新
SET key value
删除
DEL key
慎用
清空数据库
FLUSHDB
返回随机键
RANDOMKEY
返回数据库键数量
DBSIZE
其他命令参考后述链接
读一个键时,检查键是否过期,过期则删除。
命中次数
keyspace_hits
不命中次数
keyspace_misses
INFO stats命令查看
读一个键后,根据读时命中与否,更新命中与不命中的次数。
OBJECT idletime命令查看key闲置时间
读一个键后,更新LRU(最后一次使用)时间,用于计算键闲置时间。
修改一个键后,若该键被WATCH命令监视,将这个键标记为脏(dirty)。让事务知道这个键已经被修改过。
修改一个键后,脏(dirty)计数器+1,这个计数器会触发服务器的持久化和复制操作。
若开启了数据库通知功能,修改键之后,服务器将按配置发送相应的数据库通知。
键空间的读写维护操作
EXPIRE<key><ttl>命令用于将键key的生存时间设置为ttl秒。
EXPIRE
PEXPIRE<key><ttl>命令用于将键key的生存时间设置为ttl毫秒。
PEXPIRE
EXPIREAT<key><timestamp>命令用于将键key的过期时间设置为timestamp所指定的秒数时间戳。
EXPIREAT
PEXPIREAT<key><timestamp>命令用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳。
EXPIRE、PEXPIRE和EXPIREAT三个命令都会转换成PEXPIREAT命令来执行。
PEXPIREAT
SETEX命令可以在设置一个字符串键的同时为键设置过期时间。只能用于字符串键。
SETEX
设置键过期时间
typedef struct redisDb { // ... //过期字典,保存着键的过期时间 dict *expires; // ...} redisDb;
redisDb结构的expires字典保存了数据库中所有键的过期时间,我们称这个字典为过期字典。
保存过期时间
移除一个键的过期时间
数据结构上是移除了键关联的过期时间(expires)数据。
PERSIST
移除过期时间
以秒为单位返回键的剩余生存时间
TTL
PTTL命令则以毫秒为单位返回键的剩余生存时间
PTTL
剩余生存时间
检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间。
检查当前UNIX时间戳是否大于键的过期时间:如果是的话,那么键已经过期;否则的话,键未过期。
键的过期判定
键生存或过期时间
键空间
数据库键空间
过期立即删除,实时性好。
CPU性能消耗大。每个定时器是一个任务线程。
在设置键的过期时间的同时,创建一个定时器(timer,占用一个线程,睡眠过期时间),让定时器在键的过期时间来临时,立即执行对键的删除操作。
定时删除
CPU友好,不用浪费性能去删除非热点数据。
内存不友好,过期数据占用内存空间。
放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
惰性删除
CPU友好,定期处理过期数据,不用频繁占用CPU。
实时性差。数据过期,还未被定期删除,但被读取的场景。
每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
定期删除
惰性删除和定期删除结合。
Redis过期删除策略(默认)
类型
由db.c/expireIfNeeded函数实现
由redis.c/activeExpireCycle函数实现
实现
SAVE或BGSAVE命令是,过期键不会持久化。
生成
过期键在载入时会被忽略。
载入
RDB
写入
重写
AOF
主服务器在删除一个过期键之后,通知从服务器删除这个过期键。从服务器不会主动删除过期键。
复制
AOF、RDB和复制对过期键的处理
过期删除策略
Redis通过设置maxmemory-policy参数定义内存使用达到阈值时的淘汰策略。
当内存不足以容纳新写入数据时,新写入操作会报错,读数据正常工作。
noeviction
采用删除存活时间最短的键值对策略。
volatile-ttl
在所有有过期时间的 key 中使用 LRU 算法淘汰数据。
最近最久未用淘汰策略
volatile-lru
在所有有过期时间的 key 中使用 LFU 算法淘汰数据。
最近用次最少淘汰策略
volatile-lfu
在所有有过期时间的 key 中使用随机淘汰策略删除超时的键值对。
volatile-random
在所有key(无论有没有过期时间) 中使用 LRU 算法淘汰数据。
allkeys-lru
在所有key(无论有没有过期时间)中使用 LFU 算法淘汰数据。
allkeys-lfu
在所有key(无论有没有过期时间)中使用随机淘汰策略删除所有的键值对。这个策略不常用。
allkeys-random
8种
内存淘汰机制
第9章 数据库
SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止。
Redis服务器会被阻塞,直到SAVE完成。
执行时服务器状态
SAVE
BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。
SAVE命令会被服务器拒绝
BGSAVE命令会被服务器拒绝
如果BGSAVE命令正在执行,那么客户端发送的BGREWRITEAOF命令会被延迟到BGSAVE命令执行完毕之后执行。
如果BGREWRITEAOF命令正在执行,那么客户端发送的BGSAVE命令会被服务器拒绝。
BGREWRITEAOF
BGSAVE
SAVE和BGSAVE底层调用rdb.c/rdbSave函数完成数据备份任务。
触发RDB创建的方式和场景。
SAVE命令触发
BGSAVE命令触发
900秒内改变1条数据,自动生成RDB文件
SAVE 900 1
300秒内改变10条数据,自动生成RDB文件
SAVE 300 10
60秒内改变1万条数据,自动生成RDB文件
SAVE 60 10000
常见规则命令
SAVE规则命令下,调用的是BGSAVE。
Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。
注意
设定规则触发
主动方式(3种)
主从复制时,主会自动生成RDB文件。
全量复制
Redis中的debug reload提供debug级别的重启,不清空内存的一种重启,这种方式也会触发RDB文件的生成。
debug reload
会触发RDB文件的生成。
shut down
被动场景(3种)
触发条件
创建
RDB文件的载入工作是在服务器启动时自动执行。
Redis并没有专门用于载入RDB文件的命令。
载入RDB文件的实际工作由rdb.c/rdbLoad函数完成。
载入RDB文件时,服务器处于阻塞状态。
若Redis开启了AOF,优先使用AOF。
二进制格式
RDB文件结构
数据载入快,适合大规模数据恢复。
因为不是实时同步,数据实时性不高。
特点
第10章 RDB持久化
表示开启AOF持久化
开启后redis会预留内存供缓存使用,默认大小1M
需要配置文件redis.conf参数appendonly yes
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
命令追加(append)
Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。
在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面。
文件写入
追求安全
每次修改数据立即同步到AOF文件。实时性高,安全,但是效率较低。
always
每秒同步一次,效率高,有1秒的数据偏差。(默认配置)
everysec
追求效率
缓冲区满了才同步。效率高,数据安全度最低。
no
参数配置appendfsync
同步策略
文件同步(sync)
三个步骤
AOF文件实现由主线程完成。
1.创建不联网伪客户端(fake client)。
2.伪客户端逐条读取并执行AOF文件中命令直到结束。
步骤
AOF不断保存被执行的命令,存在体积膨胀的问题。需要重写(rewrite)来优化体积。
重写生成新AOF文件代替旧AOF文件。
重写期间Redis继续新执行的命令如何同步。
问题
重写期间,另预留一份重写缓冲区。新命令除了追加到AOF缓冲区,还会追加到AOF重写缓冲区。
重写完成后,将AOF重写缓冲区命令同步到新AOF文件。
覆盖操作是原子操作,避免覆盖过程中新执行命令导致不一致。即这个过程,主线程阻塞,暂停执行新命令。
执行完AOF重写缓冲区命令后,修改新AOF文件名覆盖旧AOF文件
双缓冲区设计
解决方案
原理
AOF文件超过64M开始第一次重写
生产环境建议这个值设置为5G,减少重写次数。
auto-rewrite-min-size 64M
第一次重写之后,每次体积较前一次体积加倍之后再开始重写。
auto-aof-rewrite-percentage 100
配置
AOF文件重写由子线程执行。
对比RDB,数据实时性更高。
载入需要执行全部命令,命令很多时,耗时长,效率低,且容易出BUG。
生产环境建议RDB和AOF并行。RDB做灾备。
1.开启AOF,且存在AOF文件,则Redis加载AOF文件。但不存在AOF文件,Redis直接启动,不会考虑RDB。
2.未开启AOF,存在RDB则加载RDB,否则直接启动。
Redis持久化文件加载顺序
Redis建议持久化方案
第11章 AOF持久化
Redis 主进程中,主要处理客户端的连接请求与相应。
文件事件File Event
fork 出的子进程中,处理如 AOF 持久化等定时操纵类任务。
指定时间触发一次指定程序。
定时事件
例如AOF每秒同步一次。
周期性执行执行程序。
周期事件
分类
时间事件Time Event
第12章 事件
第13章 客户端
第14章 服务器
第二部分 单机数据库的实现
子主题
第15章 复制
Raft协议实战之Redis Sentinel的选举Leader源码解析
Raft协议实战之Redis Sentinel的选举Leader源码解析
参考资料
第16章 Sentinel
第17章 集群
第三部分 多机数据库的实现
Redis Stream 是 Redis 5.0 版本新增加的数据结构。
Redis 发布订阅 (pub/sub)功能的缺点是消息无法持久化。
span style=\
Redis Stream
第18章 发布与订阅
将多个命令打包,一次性、序列化的执行完毕。事务执行过程中不会被中断。
例
Redis通过MULTI、EXEC、WATCH等命令实现事务功能。
一次性
顺序性
排他性
事务中有命令执行失败,不会回滚。
Redis的架构理念是简洁,事务回滚会导致复杂性,官方不支持,也没必要。
不保证原子性
Redis遇到MULTI命令时开启事务状态。
MULTI
开始事务
每个Redis客户端都有自己的事务状态multiState
队列命令执行顺序FIFO(先入先出)
事务队列
包含一个事务队列,以及一个已入队命令计数器multiCmd
事务状态
开始事务之后,Redis会将后续命令缓存进事务队列。返回QUEUED表示命令加入队列成功。
命令入队
Redis在事务状态遇到EXEC命令后执行事务队列的命令,返回结果。
EXEC
事务执行
三个阶段
不支持
原子性(Atomicity)
支持
一致性(Consistency)
隔离性(Isolation
appendfsync选项的值为always时,事务也具有持久性。
特定情况下支持
持久性(Durability)
事务ACID性质
WATCH \"name\" “Lily”
格式 WATCH key value
WATCH命令是乐观锁,不允许监控的value值被修改。事务中若修改被WATCH的值,则会导致事务执行失败。
WATCH
若事务队列有命令存在编译错误(语法错误),EXEC时,所有命令都不会执行。
若事务队列存在运行时异常,EXEC执行时,运行时异常会跑出错误信息,但其他命令继续执行。
执行事务
放弃事务
DISCARD
取消WATCH对所有key的监控
UNWATCH
第19章 事务
第20章 Lua脚本
第21章 排序
第22章 二进制位数组
第23章 慢查询日志
第24章 监视器
第四部分 独立功能的实现
Redis设计与实现
0 条评论
回复 删除
下一页