Redis
2021-08-25 19:12:20 0 举报
AI智能生成
登录查看完整内容
为你推荐
查看更多
Redis
作者其他创作
大纲/内容
分支主题
SDS 与 C字符串的使用
buf数组中使用的字节数量 = SDS保存字符串的长度(不包括最后的\\0)
int len
buf数组中未使用的字节数量
int free
char buf[]
sds.h/sdshdr(struct)
结构组成
O(n)遍历
C
SDS
常数复杂度获取字符串的长度
杜绝缓冲区溢出
为这个SDS分配额外和SDS长度相同的额外未使用空间(free=len)
修改后SDS的长度(len)<1MB
为这个SDS分配1MB未使用的空间(free)
修改后SDS的长度(len)>=1MB
空间预分配(增大)
惰性空间释放(减少)
减少修改字符串带来的内存重分配次数
二进制安全
兼容部分C字符串函数
SDS与C字符串的区别
简单动态字符串(SDS)
双端 prev next指针可以O(1)获取前置和后置结点
无环 head.prev = NULL tail.next = NULL
O(1)获取头尾结点
O(1) 通过len属性获取链表结点个数
双向无环链表
指向前驱结点的指针
struct ListNode *prev
指向后继结点的指针
struct ListNode *next
void *value
ListNode
链表属性
链表结点函数
List
链表
每个dictEntry结构保存一个键值对
键
void *key
值
void *val
struct dictEntry *next
哈希表结点(dictEntry)
table
size
已有的结点数量
span style=\
和哈希值决定了一个键被放到table数组的那一个索引上
sizemask(=size-1)
哈希表 (dictht)
type
保存了要传给哪些类型特定函数的可选参数
privatedata
dictType *typevoid *privatedata
- ht[0]是正常使用的哈希表- ht[1]只会对ht[0]进行rehash的时候使用
ht
rehashindex=-1 表示目前没有进行rehash
rehashindex
字典(dict)
- load_factor = ht[0].used / ht[0].size- 负载因子 = 已保存的结点数量 / 哈希表大小
负载因子
BGSAVE || BGREWRITEAOF 命令
执行扩展操作的条件
负载因子<0.1
收缩操作的必要条件
哈希表的扩展和收缩
将保存在ht[0]中的键值对rehash到ht[1]上
rehash的步骤
为什么分多次渐进式的进行rehash
渐进式rehash的步骤
添加
渐进式rehash期间的哈希表操作
渐进式rehash
rehash
字典
前进指针用于方位位于表尾方向的结点
struct zskiplistNode *forward
int span
struct zskiplistLevel level[]
后退指针(backward)
结点按照各自的分值从小到大排列(double类型)
分值(score)
成员对象(obj)
redis.h/zskiplistNode
跳跃表的头尾结点
head tail
level
跳跃表的长度即跳跃表的结点数(表头结点不算在内)
length
redis.h/zskiplist
跳跃表
int16_t int32_t int64_t
encoding
整数集合的每个元素都是contents数组的一个数组项(item) 各个项在数组中从小到大排列并且不重复
content数组的真正类型取决于encoding属性的值
contents[]
intset.h/intset
升级步骤
提升灵活性
节约内存
升级的好处
升级
不支持降级
升级 与 降级
整数集合
压缩列表是一种为节约内存而开发的顺序型数据结构font color=\"#333333\" face=\
记录压缩列表占用的内存字节数
zlbytes
zltail
压缩列表包含的结点数量
zllen
压缩列表包含的各个结点
entryX
特殊值 0xFF(255) 标记压缩列表的末端
zlend
压缩列表
每个压缩列表结点可以保存一个字节数组或者一个整数值
previous_entry_length
content
压缩列表结点
数据结构
字符串
incr
数值
setbit bitcount bitpos bitop
#对应天登陆setbit key 1 1 setbit key 7 1setbit key 364 1#统计最后两天登陆的天数bitcount key -2 -1
- key:用户 value:代表365天的bit- 365位bit表示这个人第几天登录- 通过 bitcount 统计某个窗口的1就是登录的天数
#user1登录两天 user7登录1天setbit 20190101 1 1setbit 20190102 1 1setbit 20190102 7 1#两天的数据做按位&复制到destkeybitop & destkey 20190101 20190102#统计所有用户中两天都登陆的 1就说明登录了bitcount destkey 0 -1
统计活跃用户
bitmap
String
hash
场景: 抽奖
随机事件
set
排名前几的用户
sortedset
数据类型的应用
type属性记录了对象的类型
type <数据库键>:
类型 type
每种类型的对象都使用了至少两种不同的编码
编码 encoding
void *ptr
对象类型与编码
int
embstr编码通过调用一次内存分配一块连续的空间 依次包含redisObject和sdshdr两个结构
embstr
使用SDS来保存这个字符串值 长度>32字节
raw调用两次内存分配分别创建redisObject 和 sdshdr结构
raw
编码
int embstr在条件满足的情况下会转换成raw
使这个对象从保存整数值变成保存字符串值
int -> raw
对这个对象修改的时候转换成raw(append)
embstr -> raw
编码的转换
字符串对象(int embstr raw)
子主题
ziplist
linkedlist
- 列表对象保存的所有字符串元素的长度都小于64字节- 列表对象保存的元素数量小于512
使用ziplist编码
不能满足这两个条件的都需要使用linkedlist编码
列表对象(ziplist linkedlist)
ziplist编码的哈希对象底层使用压缩列表
hashtable
- 哈希对象保存的所有键值对的键和值的字符串长度都<64字节- 哈希对象保存的所有键值对的数量<512
不满足的都需要使用hashtable
编码转换
哈希对象(ziplist hashtable)
intset
- 集合对象所有元素都是整数值- 元素个数<=512
intset编码的条件
不满足的需要用hashtable编码
集合对象(intset hashtable)
有序集合的每一个元素的成员都是一个字符串对象每个元素的分值都是一个double类型的浮点数
使用压缩列表作为底层实现
zsl跳跃表
- 为有序集合创建一个从成员到分值的映射- 字典的每个键值对都保存了一个集合元素 - 字典的键保存了一个元素的成员 - 字典的值保存了一个元素的分值- 可以用O(1)复杂度找到给定成员的分值- ZSCORE就时根据这一特性实现的
dict字典
为什么同是使用跳跃表和字典来实现?
skiplist
- 有序集合元素数量<128- 保存元素的成员长度都<64字节
使用ziplist的条件
不能满足的都要使用skiplist编码
有序集合对象(ziplist skiplist)
对象
IO多线程加快了从网卡到内核复制数据的过程也加快了数据返回的过程
redis线程模型的工作流程
IO模型采用的多路复用的EPOLL
worker线程单线程避免了多线程的上下文切换以及加锁解锁的问题
redis单线程快的原因
多线程DB
单线程redis
秒杀场景用多线程DB和单线程Redis对比
redis线程模型
原因
分布式锁
解决方案
缓存穿透
一个热点key过期
设置热点key永远不过期
缓存击穿
热点key大面积过期
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
缓存预热
避免重复请求
过滤无效请求
缓存雪崩
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
缓存降级
原理
占用内存小增加和查询数据的时间复杂度为 O(k) k为hash函数的个数
优点
缺点
布隆过滤器
- setnx 不存在设置 对应的是新建- setxx 存在设置 对应的是更新EX seconds: 设定过期时间,单位为秒PX milliseconds: 设定过期时间,单位为毫秒NX: 仅当key不存在时设置值XX: 仅当key存在时设置值
setnx/xx
setnx + setex
加锁和释放锁不是一个线程的问题
不可重入
主从复制实现HA
将不存在设置(nx)以及设置超时时间(px)变成原子操作
redlock
实现方法
第三方redis实例实现
缓存不存在去抢锁
一个key的多个请求只有一个到达数据库
总结
分布式场景相关
- EXPIRE <key> <ttl> 生存时间设置为ttl秒- PEXPIRE <key> <ttl> 生存时间设置为ttl毫秒- EXPIREAT <key> <timestamp> 过期时间设置为 timestamp秒数时间戳- PEXPIREAT <key> <timestamp> 过期时间设置为 timestamp毫秒数时间戳
设置过期时间
保存键的过期时间
定时删除
惰性删除
定期删除
缓存过期策略
复制AOF文件的时候会忽略过期键
复制AOF文件时如何处理过期
Redis使用的过期策略
Redis过期键
1. noeviction:默认禁止驱逐数据。内存不够使用时,对申请内存的命令报错。 2. volatile-lru:从设置了过期时间的数据中淘汰最近没使用的数据。 3. volatile-ttl:从设置了过期时间的数据中淘汰即将要过期的数据。 4. volatile-random:从设置了过期时间的数据中随机淘汰数据。 5. allkeys-lru:淘汰最近没使用的数据。 6. allkeys-random:随机淘汰数据。
淘汰策略
Redis缓存淘汰
缓存淘汰与过期删除
问题
我们始终只能保证一定时间内的最终一致性
数据库和缓存不一致
save(阻塞)
BGREWRITEAOF和BGSAVE不能同时执行
bgsave(非阻塞)
主动触发
save m n
主从同步
被动触发
将某一时刻的缓存快照以二进制的方式写入磁盘
优缺点
RDB(Redis DataBase)
命令追加
每个操作都将aof_buf内容写入AOF文件 并且同步到磁盘
always
everysec(默认)
no
appendfsync
AOF文件的写入与同步
AOF持久化的实现
创建新的AOF文件
1. 忽略空数据库2. 写入SELECT命令+数据库号码3. 遍历数据库所有的键(或略过期键)4. GET 获取键的值5. SET命令重写键
遍历数据库
重写过程
Redis将AOF重写程序放到子进程进行
在服务器创建子进程以后使用
1. 执行客户端发送的命令2. 将执行的写命令追加到AOF缓冲区3. 将执行的写命令追加到AOF重写缓冲区
保证了
AOF重写缓冲区
AOF后台重写
4.x版本的整合策略
AOF重写
还原步骤
AOF文件的载入与数据还原
AOF(Append Only File)
- AOF文件比RDB更新频率高,优先使用AOF还原数据。- AOF比RDB更安全也更大- RDB性能比AOF好- 如果两个都配了优先加载AOF
AOF vs RDB
Redis持久化机制
Redis 事务通过MULTI、EXEC、DISCARD、WATCH等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。
在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
也可以基于Lua脚本实现事务,Redis可以保证脚本内的命令一次性、按顺序地执行,
Redis事务的概念
事务开始 MULTI命令入队事务执行 EXEC事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队
Redis事务的三个阶段
WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
WATCH
MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
MULTI
EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
EXEC
通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
DISCARD
Redis事务相关命令
事务执行EXEC之前
事务执行EXEC之后
为什么Redis不支持回滚(roll back)
事务中的错误
Redis事务
Sentinel是Redis高可用的解决方法
负责监控 redis master 和 slave 进程是否正常工作。
集群监控:
如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
消息通知:
如果 master node 挂掉了,会自动转移到 slave node 上。
故障转移:
如果故障转移发生了,通知 client 客户端新的 master 地址。
配置中心:
功能
redis-sentinel <sentinel.conf的路径>redis-server <sentinel.conf的路径> --sentinel
初始化服务器
使用Sentinel专用代码
初始化一个sentinel.c/sentinelState结构 保存服务器中所有和Sentinel功能相关的状态
初始化Sentinel状态
字典键是被监视主服务器的名字
字典的值是被监视主服务器对应的sentinel.c/sentinelRedisInstance结构
masters字典记录所有被Sentinel监视的主服务器的相关信息
初始化Sentinel状态的masters属性
命令连接
订阅主服务器的 _sentinel_:hello频道
订阅连接
创建连向主服务器的网络连接
启动并初始化Sentinel
根据这些信息更新主服务器的实例结构
主服务器本身信息
主服务器属下从服务器信息
获取主服务器的信息
创建连接后 会以十秒一次的频率通过命令连接向从服务器发送INFO命令
获取从服务器信息
默认 Sentinel以两秒一次的频率 通过命令连接向所有被监视的主从服务器放以下格式的命令
向主服务器和从服务器发送信息
Sentinel为主服务器创建的实例结构中Sentinels字典保存了除Sentinel本身以外所有其他监视这个主服务器的Sentinel资料
- 字典键是一个Sentinel的名字 ip:port的形式- 值是键对应的Sentinel的实例结构
更新Sentinels字典
创建连向其他Sentinel的命令连接
接收来自主从服务器的频道信息
与主从服务器的信息交互
+PONG; -LOADING; -MASTERDOWN
有效回复
除了有效回复外的其他回复
无效回复
检测主观下线状态
发送 SENTINEL is-master-down-by-addr命令
根据主服务器的IP PORT 检查主服务器是否下线
down_state
leader_runidfont color=\"#333333\" face=\
leader_epoch
返回三个参数的回复
接受 SENTINEL is-master-down-by-addr命令
接受 SENTINEL is-master-down-by-addr回复
检查客观下线状态
发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设为局部领头Sentinel(携带epoch runid就是为了让接收方sentinel选举发送方为局部领头)
选举领头 Sentinel
检测服务器下线
在线状态并且最近5秒回复过领头sentinel并且与下线主服务器连接的从服务器
向选中的从服务器发送 SLAVEOF no one
选出新的主服务器
领头Sentinel对已下线的主服务器进行故障转移
让下线服务器的从服务器复制新的主服务器 SLAVEOF <newMaster_ip> <newMaster_port>
修改从服务器的复制目标
下线主服务器设置为新的主服务器的从服务器
旧的主服务器变成从服务器
故障转移
哨兵模式(sentinel)
将从服务器状态更新至和主服务器一致
同步(SYNC)
命令传播
完全复制
执行复制的双方 主从服务器分别维护一个复制偏移量
复制偏移量
复制积压缓冲区
每个Redis服务器在服务器启动的时候都会生成40个随机的十六进制字符组成的运行ID
服务器运行ID
实现原理
部分复制
+FULLRESYNC <runid> <offset> 表示执行完整重同步
从服务器等待主服务器发送缺少的那部分数据
+CONTINUE 表示执行部分重同步
-ERR
接到PSYNC主服务器会有三种回复
PSYNC命令的实现
主从复制模式
- 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽区间的数据,默认分配了16384 个槽位- 每份数据分片会存储在多个互为主从的多节点上- 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)- 同一分片多个节点间的数据不保持一致性- 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点- 扩容时时需要需要把旧节点的数据迁移一部分到新节点- 在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。
基本原理
保存了节点当前的状态并为集群中其他所有节点创建相应的clusterNode结构
clusterNode
clusterState
集群数据结构
节点
clusterNode结构的slots 属性 和 numslot属性记录了结点负责处理哪些槽
记录结点的槽指派信息
每个结点会将自己的slots数组通过消息发送给集群中的其他节点
每个接收到slots数组的结点都会将数组保存到发送这个slots数组信息的对应结点的clusterState.nodes.clusterNode结构里
以集群中的每个结点都会知道数据库中的16384个槽分别被指派给了集群中的哪个结点
传播结点的槽指派信息
clusterState结构的slots数组记录了集群所有16384个槽的指派信息
如果只有clusterNode.slots
访问clusterState.slots[i] O(1)
通过clusterState
clusterState.slots解决了clusterNode.slots无法法高效解决的问题
clusterNode.slots仍然是必要的
clusterNode.slots数组记录了clusterNode结构所代表的结点的槽指派信息clusterState.slots数组记录了集群所有槽的指派信息
记录集群所有槽的指派信息
CLUSTER ADDSLOTS命令的实现
槽指派
计算键属于哪个槽
clusterState.slots[i] = clusterNode.myself 槽i由当前结点负责可以执行命令
判断槽是否由当前结点负责处理
MOVED <slot> <ip>:<port>
MOVED 错误
结点数据库的实现
集群中执行命令
clusterState结构的 importing_slots_from数组记录当前结点正在从其他节点导入的槽
CLUSTER SETSLOT <slot> IMPORTING <source_id>
目标结点准备导入槽的键值对
migrating_slots_to[i] 指向一个clusterNode结构说明当前结点正在将槽[i]迁移至clusterNode代表的结点
CLUSTER SETSLOT <slot> MIGRATING <target_id>
源结点准备迁移槽的键值对
判断源结点是否保存了属于槽的键
将槽指派给目标结点
重新分片
div style=\"orphans: 4;\
客户端请求的数据库键属于被正在被迁移的槽
ASKING命令需要做的就是打开发送该命令的客户端的REDIS_ASKING 标识
ASKING命令
(正在迁移)ASK错误代表两个结点在迁移槽的时候使用的临时措施
ASK错误和MOVED错误的区别
ASK错误
CLUSTER REPLICATE <node_id>
clusterState.nodes字典中找到node_id对应的ClusterNode结构
SLAVEOF <master_ip> <master_port> 对主节点进行复制
结点成为从节点 复制 主节点 这个信息变成消息发送给集群其他节点
集群中的所有结点都会在代表主节点clusterNode结构的slaves属性和numslaves属性记录正在复制这个主节点的从节点名单
设置从节点
故障检测
选举新的主节点
复制与故障转移
分片(sharding)
Redis集群方案
计算向数据移动
数据向计算移动
移动数据成本大于移动计算
数据存储类型
Redis:
多线程非阻塞IO
Memcached
网络IO模型
RDB AOF
不支持
持久化支持
Redis vs Memcached
Redis
0 条评论
回复 删除
下一页