redis由浅入深全知识点
2024-02-21 09:56:13 56 举报
AI智能生成
Redis是一个开源的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。它支持多种数据结构,如字符串、哈希、列表、集合和有序集合。Redis具有高性能、高可用性和持久化特性,可以通过主从复制和哨兵模式实现高可用性,通过RDB和AOF实现持久化。此外,Redis还支持事务、管道和Lua脚本等功能。要深入学习Redis,需要掌握其基本概念、数据结构和操作命令,以及如何配置和使用Redis集群、分布式锁等高级功能。
作者其他创作
大纲/内容
I/O
BIO
阻塞IO模型 Block IO
NIO
非阻塞I/O模型(Non-Block IO)
AIO
异步I/O模型(Asynchronous IO)
I/O复用模型(IO Multiplex)
多路复用
Signal Driven IO
信号驱动IO
分支主题
持久化
AOF
默认不开启
AOF 采用日志的形式来记录每个写操作,并追加到文件中。开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。 Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复 工作
redis.conf
# 开关appendonly no
appendfilename "appendonly.aof"
带来的问题:
数据都是实时持久化到磁盘吗?
不是的,是先进入了系统的缓存区,硬盘缓存区
appendfsync
everysec
表示每秒执行一次 fsync,可能会导致丢失这 1s 数据。通常选择 everysec , 兼顾安全性和效率。
always
表示每次写入都执行 fsync,以保证数据同步到磁盘,效率很低;
no
表示不执行 fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全;
文件越来越大,怎么办?
如 set abc 666,执行 1000 次,结果都是 abc=666。
以使用命令 bgrewriteaof 来重写
当 AOF 文件的大小超过所设定的阈值 时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集
auto-aof-rewrite-percentage 100
默认值为 100。aof 自动重写配置,当目前 aof 文件大小超过上一次重写的 aof 文件大小的 百分之多少进行重写,即当 aof 文件增长到一定大小的时候,Redis 能够调用 bgrewriteaof 对日志文件进行重写。当前 AOF 文件大小是上次日志重写得到 AOF 文件大小的二倍(设 置为 100)时,自动启动新的日志重写过程。
auto-aof-rewrite-min-size 64mb
默认 64M。设置允许重写的最小 aof 文件大小,避免了达到约定百分比但尺寸仍然很小的 情况还要重写。
重写过程中,AOF 文件被更改了怎么办?
no-appendfsync-on-rewrite
设置为 yes 表示 rewrite 期间对新写操作不 fsync, 暂时存在内存中,等 rewrite 完成后再写入,默认为 no,建议修改为 yes。Linux 的默认 fsync 策略是 30 秒。可能丢失 30 秒数据。
aof-load-truncated
aof 文件可能在尾部是不完整的,当 redis 启动的时候,aof 文件的数据被载入内存。重启 可能发生在 redis 所在的主机操作系统宕机后,尤其在 ext4 文件系统没有加上 data=ordered 选项,出现这种现象。redis 宕机或者异常终止不会造成尾部不完整现象,可以选择让 redis 退出,或者导入尽可能多的数据。
如果选择的是 yes,当截断的 aof 文件被导入的时候, 会自动发布一个 log 给客户端然后 load。如果是 no,用户必须手动 redis-check-aof 修复 AOF 文件才可以。默认值为 yes。
AOF和RDB的选择
如果可以忍受一小段时间内数据的丢失,毫无疑问使用 RDB 是最好的,定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要 比 AOF 恢复的速度要快
否则就使用 AOF 重写。但是一般情况下建议不要单独使用某一种持久化机制,而 是应该两种一起用,在这种情况下,当 redis 重启的时候会优先载入 AOF 文件来恢复原始 的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
分支主题
优点:
AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步 一次,Redis 最多也就丢失 1 秒的数据而已。
对于具有相同数据的的 Redis,AOF 文件通常会比 RDF 文件体积更大(RDB 存的是数据快照
RDB快照
默认的持久化策略,生成dump.rdb
RDB 触发
配置规则触发 redis.conf
save 900 1 # 900 秒内至少有一个 key 被修改(包括添加)不使用时 直接注释
配置保存文件路径,是否开启压缩
手动触发
save
save 在生成快照的时候会阻塞当前 Redis 服务器, Redis 不能处理其他命令。如果 内存中的数据比较多,会造成 Redis 长时间的阻塞,生产环境不建议使用
bgsave
执行 bgsave 时,Redis 会在后台异步进行快照操作,快照同时还可以响应客户端请 求
Redis 进程执行 fork 操作创建子进程(copy-on-write),RDB 持久化 过程由子进程负责,完成后自动结束
shutdown 触发,保证服务器正常关闭
flushall,RDB 文件是空的,没什么意义
练习操作命令
优势:
1、RDB 是一个非常紧凑(compact)的文件,它保存了 redis 在某个时间点上的数据 集。这种文件非常适合用于进行备份和灾难恢复
2、生成 RDB 文件的时候,redis 主进程会 fork()一个子进程来处理所有保存工作,主 进程不需要进行任何磁盘 IO 操作
3、RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
劣势:
RDB 方式数据没办法做到实时持久化/秒级持久化。因为 bgsave 每次运行都要 执行 fork 操作创建子进程,频繁执行成本过高
在一定间隔时间做一次备份,所以如果 redis 意外 down 掉的话,就会丢失最后 一次快照之后的所有修改(数据有丢失)
内存回收
淘汰策略
Redis 的内存淘汰策略,是指当内存使用达到最大内存极限时,需要使用淘汰算法来 决定清理掉哪些数据,以保证新数据的存入
redis.conf 参数配置:# maxmemory <bytes>
64 位系统不限制内存,32 位系统最多使 用 3GB 内存
动态修改:config set maxmemory 2GB
LRU
Least Recently Used:最近最少使用
volatile-lru
在带有过期时间的键,中删除最近最少使用的
分支主题
allkeys-lru
在所有带有过期时间的Key中,删除最近最少使用的
Redis优化过:
如果淘汰策略是 LRU,则根据配置的采样值 maxmemory_samples(默认是 5 个), 随机从数据库中选择 m 个 key, 淘汰其中热度最低的 key 对应的缓存数据
LFU
Least Frequently Used,最不常用(最近使用频次最少的),4.0 版本新增
volatile-lfu
在带有过期时间的键中删除最不常用的。
allkeys-lfu
在所有的键中选择最不常用的,不管数据有没有设置超时属性。
Rondom
随机删除
volatile-random
在带有过期时间的键中随机删除
allkeys-random
随机删除所有键,直到腾出足够内存为止。
noeviction
默认策略,不会删除任何数据,达到内存最大后,返回错OOM,只响应读操作
volatile-ttl
根据键值对象的 ttl 属性,删除最近将要过期数据。如果没有,回退到 noeviction 策略
动态修改淘汰策略
config set maxmemory-policy volatile-lru
默认推荐使用:建议使用 volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的 key。
思考如何基于一个数据结构实现LRU算法:
LinkedHashMap是有序的,且默认为插入顺序,先进入在前,后进去的在后面,
继承LinkedHashMap并重写removeEldestEntry方法
过期策略
定时过期
每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除
效率很高,但是key很多的时候 也创建了大量的定时器,对内存很友好,占用大量的 CPU 资源去处理过期的 数据
惰性过期(被动淘汰)
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除
该策略可以最 大化地节省 CPU 资源,却对内存非常不友好,极端情况可能出现大量的过期 key 没有再 次被访问,从而不会被清除,占用大量内存
定期过期
每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清 除其中已过期的 key。该策略是前两者的一个折中方案
介绍关系
主存:内存
计算机主存(内存)可看作一个由 M 个连续的字节大小的单元组成的数组,每个字 节有一个唯一的地址,这个地址叫做物理地址(PA)。早期的计算机中,如果 CPU 需要 内存,使用物理寻址,直接访问主存储器。
带来的问题:
在多用户多任务操作系统中,所有的进程共享主存,如果每个进程都独占一块物 理地址空间,主存很快就会被用完。我们希望在不同的时刻,不同的进程可以共用同一 块物理地址空间
如果所有进程都是直接访问物理内存,那么一个进程就可以修改其他进程的内存 数据,导致物理地址空间被破坏,程序运行就会出现异常
引入虚拟内存
CPU 不再使用物理地址访问,而是访问一个虚拟地址,由这个中间层把地址转换成物理地址, 最终获得数据
总结:
引入虚拟内存,可以提供更大的地址空间,并且地址空间是连续的,使得程 序编写、链接更加简单。并且可以对物理内存进行隔离,不同的进程操作互不影响。还 可以通过把同一块物理内存映射到不同的虚拟地址空间实现内存共享
辅存:磁盘
用户空间和内核空间
为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部 分,一部分是内核空间(Kernel-space)/ˈkɜːnl /,一部分是用户空间(User-space)。
在 Linux 系统中, 内核进程和用户进程所占的虚拟内存比例是 1:3。
上下文切换
多任务操作系统是怎么实现运行远大于 CPU 数量的任务个数的?
这些任务实 际上并不是真的在同时运行,而是因为系统通过时间片分片算法,在很短的时间内,将 CPU 轮流分配给它们,造成多任务同时运行的错觉
什么叫上下文?
在每个任务运行前,CPU 都需要知道任务从哪里加载、又从哪里开始运行,也就是 说,需要系统事先帮它设置好 CPU 寄存器和程序计数器(Program Counter),这个叫做 CPU 的上下文。
上下文,会存储在系统内核中
上下文切换会很消耗资源
进程的阻塞
正在运行的进程由于提出系统服务请求(如 I/O 操作),但因为某种原因未得到操 作系统的立即响应,该进程只能把自己变成阻塞状态,等待相应的事件出现后才被唤醒。 进程在阻塞状态不占用 CPU 资源
文件描述符 FD
Linux 系统将所有设备都当作文件来处理,而 Linux 用文件描述符来标识每个文件 对象。
0:标准输入(键盘)
1:标准输出(显示器)
2:标准错误输出(显示器)。
介绍一个读操作:
核心流程图
当应用程序执行 read 系统调用读取文件描述符(FD)的时候,如果这块数据已经 存在于用户进程的页内存中,就直接从内存中读取数据。如果数据不存在,则先将数据 从磁盘加载数据到内核缓冲区中,再从内核缓冲区拷贝到用户进程的页内存中。(两次 拷贝,两次 user 和 kernel 的上下文切换)
如果此时发生阻塞,那么可能有两个情况:
从磁盘读取到内核空间 没准备好,发生阻塞
从内核空间复制到用户空间 阻塞
由于发生阻塞 引出思考:如何解决阻塞呢:
1、 在服务端,看好是在服务端,创建多个线程,来执行任务,但是会存在创建 销毁线程带来的资源开销
2、请求发起轮询。等内核空间准备好 复制到了用户空间,再发起下一步操作
引入多路复用
多路指的是多个 TCP 连接
复用指的是复用一个或多个线程
比之前快在哪里?
I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符, 而这些文件描述符(套接字描述符)其中的任意一个进入读就绪(readable)状态,select() 函数就可以返回
就是说一个线程来控制多个请求,只要内核空间的数据准备好就可以返回了
减少了创建多个线程造成的资源浪费,销毁,以及上下文切换的时间
selector
个人理解
优点:通过一个线程来在内核空间和用户空间之间进行管理,所有IO请求操作FD进来都由 selector管理,内核空间里只要有一个FD准备好了,就可以返回
缺点:首先,用户空间告诉内核空间,要操作那些FD的时候,是将selector的队列里 所有的FD都复制到内核空间。同样,返回的时候也是如此,再从内核空间复制到用户空间。
fd_set的大小做了限制,你不能很大,一直copy来 copy去的。最大1024 ;其实可以改但是要重新编译
好一点的描述
每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,一般不会改变(限制为1024)
poll
poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024
epoll
基于事件驱动的I/O方式
使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
核心方法
int epoll_create(int size)
函数创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数注册要监听的事件类型
epfd 表示epoll句柄
op: fd操作类型
EPOLL_CTL_ADD 注册新的fd到epfd中
EPOLL_CTL_MOD 修改已注册的fd的监听事件
EPOLL_CTL_DEL 从epfd中删除一个fd
fd 是要监听的描述符
event 表示要监听的事件
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epfd 是epoll句柄
events 表示从内核得到的就绪事件集合
maxevents 告诉内核events的大小
timeout 表示等待的超时事件
使用逻辑:
当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体 : struct rb_root rbr; //红黑树根节点 struct list_head rdllist;//双向链表rdllist
调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件
当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回
所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中
水平触发(LT)
默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件
边缘触发(ET)
当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。
ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。
优点:
当我有很多个IO去操作FD时 ,我创建一个文件描述符FD ,统一去描述这很多个IO操作的FD; 然后就只需要cpoy一次从用户空间到内核空间
epoll_wait非常高效
多路复用直观对比
用户空间内核空间
安装,集群部署等参考网上教程
基础
数据类型
String
字符串
set name kklt
get name
Hash
哈希
HSET website google "www.g.cn"
HGET website google
hash的结构是:key field value
包含键值对的无序散列表。value 只能是字符串,不能嵌套其他类型
set
集合
SADD bbs "discuz.net"
SMEMBERS bbs
zset
有序集合
ZADD page_rank 10 google.com
sorted set,有序的 set,每个元素有个 score。 score 相同时,按照 key 的 ASCII 码排序
List
列表
LPUSH mylist a b c
LRANGE mylist 0 -1
查看mylist中的所有数据
Hyperloglog、Geo、Streams
不常用,知道即可
常用命令
keys *
type key
dbsize等等
具体所有命令查看网址:
http://redisdoc.com/list/index.html
应用场景
String类型
热点数据缓存
对象缓存,全页缓存。
分布式锁
STRING 类型 setnx 方法,只有不存在时才能添加成功,返回 true。
分布式 Session
全局 ID
INT 类型,INCRBY,利用原子性
计数器
INT 类型,INCR 方法
文章的阅读量,微博点赞数,允许一定的延迟,先写入 Redis 再定时同步到 数据库
限流
INT 类型,INCR 方法
以访问者的 IP 和其他信息作为 key,访问一次增加一次计数,超过次数则返回 false。
位统计
String 类型的 BITCOUNT(1.6.6 的 bitmap 数据结构介绍)。
问题:String类型如何存一个对象
利用分层:mset student:1:sno GP16666 student:1:sname 沐风 student:1:company 腾讯
代表的意思是: student:1:sno 为key GP16666 为value
缺点:key 太长,占用的空间太多
hash:
存储字符串,Hash 与 String 的主要区别?
把所有相关的值聚集到一个 key 中,节省内存空间
只使用一个 key,减少 key 冲突
当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU 的消耗
不适合的场景:
1、Field 不能单独设置过期时间
2、没有 bit 操作
3、需要考虑数据量分布的问题(value 值非常大的时候,无法分布到多个节点)
String 可以做的事情,Hash 都可以做。
购物车
List
存储有序的字符串(从左到右),元素可以重复。可以充当队列和栈的角色
场景:
消息队列
用户消息时间线 timeline
Set
随机获取元素 spop myset
点赞、签到、打卡
商品标签
商品筛选
差集
sdiff set1 set2
交集
sinter set1 set2
并集
sunion set1 set2
zset
排行榜
高级特性与原理
高级特性
发布订阅pub/sub
使用List来做消息队里的缺点: (队尾进队头出)
如果生产者生产消息的速度远大于消费者消费消息的速度,List 会占用大量的内 存
消息的实时性降低。
基于 list 实现的消息队列,不支持一对多的消息分发。
订阅频道
命令:subscribe channel-1 channel-2 channel-3
按规则(Pattern)订阅频道
支持?和*占位符。?代表一个字符,*代表 0 个或者多个字符。
psubscribe *sport
发布命令:
publish channel-1 2673
并不支持一次向多个频道发送消息
取消订阅:
unsubscribe channel-1
在Java代码中实现订阅 取消订阅 等等;
遍历已定于的渠道,发送unsubscribe命令
事务
redis事务的特点:
按进入队列的顺序执行
不会受到其他客户端的请求的影响
命令:
multi(开启事务)
multi 执行后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被 执行, 而是被放到一个队列中, 当 exec 命令被调用时, 所有队列中的命令才会被执 行。
exec(执行事务)
discard (取消事务)
discard 可以清空事务队列,放弃执行。
watch(监视)
为 Redis 事务提供 CAS 乐观锁行为(Check and Set / Compare and Swap),也就是多个线程更新变量的时候,会跟原值做比较,只有它没有被其他线程修 改的情况下,才更新成新的值。
watch是监视key的
或者unwatch取消监视
如果开启事务之后,至少有一个被监视 key 键在 exec 执行之前被修改了, 那么整个事务都会被取消
事务可能出现的问题:
在exec执行之前
比如已经开启了事务后,队列中放了一些正常的命令,然后执行了错误的命令。
事务会被拒绝执行,也就是队列中所有的命令都不会得到执行
在exec执行之后
只有错误的命令没有被执行,但是其他命令没有受到影响
思考:在exec之后发生了错误,但是还是有一些命令执行了。这不符合事务的原理啊
redis:为啥不回滚已执行的命令呢。
LUA脚本
eval lua-script key-num [key1 key2 key3 ....] [value1
eval 代表执行 Lua 语言的命令
lua-script 代表 Lua 语言脚本内容。
key-num 表示参数中有多少个 key,需要注意的是 Redis 中 key 是从 1 开始的,如果没有 key 的参数,那么写 0。
[key1 key2 key3…]是 key 作为参数传递给 Lua 语言,也可以不填,但是需要和 key-num 的个数对应起来。
[value1 value2 value3 ….]这些参数传递给 Lua 语言,它们是可填可不填的。
在Lua脚本中调用redis命令(其实这才是常用的)
用 redis.call(command, key [param1, param2…])进行操作
demo: eval "return redis.call('set',KEYS[1],ARGV[1])" 1 gupao 2673 ========= set gupao 2673
把redis命令写到file中, 然后lua脚本调用
vim abc.lua
abc.luar 中填写redis命令:redis.call('set','abc','lua666') return redis.call('get','abc')
调用脚本: redis-cli --eval abc.lua 0
缓存Lua脚本
原因:有的脚本可能很大,如果每次执行,都要上传到服务端,那么网络开销比较大
script load “具体命令”
得到返回值 "470877a599ac74fbfda41caa908de682c5fc7d4b"
使用命令执行:evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b"
脚本超时了怎么办:
因为脚本可能执行时间过长
在redis服务端的配置文件redis.conf中设置了 lua-time-limit 5000 5秒
超过5秒后Redis 将开始接受其他命令但不会执行 并返回 busy
redis 可以使用:script kill 杀死执行中的脚本
如果当前执行的 Lua 脚本对 Redis 的数据进行了修改(SET、DEL 等),那么通过 script kill 命令是不能终止脚本运行的
原因是要保持脚本的原子性,要么都执行。要么都不执行
只能通过 shutdown nosave 命令来强行终止 redis
shutdown nosave 和 shutdown 的区别在于 shutdown nosave 不会进行持久化 操作,意味着发生在上一次快照后的数据库修改都会丢失。
思考:redis是单线程的 脚本卡死了,怎么去接受shutdown 以及其他命令呢?
lua 脚本引擎功能很强大,它提供了各式各样的钩子函数,它允许在内部虚拟机执行指令时运行钩子代码。比如每执行 N 条(例如:1000条)指令执行一次某个钩子函数,Redis 正是使用了这个钩子函数。
Redis 在钩子函数里会忙里偷闲去处理客户端的请求,并且只有在发现 lua 脚本执行超时之后才会去处理请求,这个超时时间默认是 5 秒
分支主题
原理剖析
Redis为啥这么快
测试命令:
redis-benchmark -t set,lpush -n 100000 -q
SET: 111482.72 requests per second
LPUSH: 118906.06 requests per second
本地测试:每秒11万次的set 和get
redis-benchmark -n 100000 -q script load "redis.call('set','foo','bar')"
script load redis.call('set','foo','bar'): 111731.84 requests per second
每秒执行11万的Lua脚本
这么快的原因:
纯内存结构 key -value结构
单线程
没有创建线程、销毁线程带来的消耗
避免了上线文切换导致的 CPU 消耗
避免了线程之间带来的竞争问题,例如加锁释放锁死锁等等
多路复用
延伸一下什么叫多路复用selector、poll、epoll
异步非阻塞IO
实战
客户端
Jedis
pipeline
Pipeline 通过一个 队列把所有的命令缓存起来,然后把多个命令在一次连接中发送给服务器
使用pipeline批量写入数据,对于结果的实时性和成功性要求不高
jedis线程不安全
Jedis的其他操作 参考API
luttece
线程安全
Lettuce 是 Spring Boot 2.x 默认的客户端,替换了 Jedis。集成之后我们不需要单 独使用它,直接调用 Spring 的 RedisTemplate 操作,连接和创建和关闭也不需要我们 操心
支持同步
支持异步
支持响应式模式
Redission
基于 Netty 实现,采用非阻塞 IO,性能高
支持异步请求
支持连接池
不支持事务,官方建议以 LUA Scripting 代替事务
主从、哨兵、集群都支持。Spring 也可以配置和注入 RedissonClient。
应用
分布式锁
jedis可以实现
Redission也可以实现
分布式锁的思想:
1、互斥性:只有一个客户端能够持有锁
2、不会产生死锁:即使持有锁的客户端崩溃,也能保证后续其他客户端可以获 取锁
3、只有持有这把锁的客户端才能解锁
实际使用需要查看代码
数据一致性
缓存使用场景
先删缓存,还是先删数据库
延时双删的策略
先删除缓存,更新数据,再删除缓存
高并发
热点数据发现
在客户端
Jedis 的 Connection 类的 sendCommand()里面,用 一个 HashMap 进行 key 的计数
代理层
服务端
监控Monitor
影响性能
缓存穿透
数据在redis和数据库中都不存在。造成这样的原因,1可能上一次错误查询,2.是恶意请求,故意制造的查询
(1)缓存空数据 (2)缓存特殊字符串,比如&&
但是如果是恶意请求的时候,上面这个方案就不适用了,因为你会缓存N多个垃圾请求的数据
引出一个经典面试题:
如何在海量元素中(例如 10 亿无序、不定长、不重复)快速判断一个元素是否存在?
大名鼎鼎的布隆过滤器
从容器的角度来说
如果布隆过滤器判断元素在集合中存在,不一定存在
如果布隆过滤器判断不存在,一定不存在!!!这个特性可以帮助我们去解决缓存穿透,用来判断数据库里一定没有这个数据,那么就可以直接返回错误了。
从元素的角度来说:
如果元素实际存在,布隆过滤器一定判断存在
如果元素实际不存在,布隆过滤器可能判断存在
布隆过滤器的原理
此处不说明,百度
缓存雪崩
缓存雪崩就是 Redis 的大量热点数据同时过期(失效),因为设置了相同的过期时 间,刚好这个时候 Redis 请求的并发量又很大,就会导致所有的请求落到数据库
加互斥锁或者使用队列,针对同一个 key 只允许一个线程到数据库查询
缓存定时预先更新,避免同时失效
通过加随机数,使 key 在不同的时间过期
缓存永不过期
缓存击穿
击穿是一个雪花的雪崩,雪崩是所有雪花的穿透
分布式
集群
防止故障
扩容
负载
主从复制
主从复制的配置,修改redis.conf
slaveof 主节点IP 6379
在启动服务时通过参数指定 master 节点
./redis-server --slaveof 主节点IP 6379
客户端直接执行 slaveof xx xx
查看集群状态
info replication
从节点不能写入数据(只读),只能从 master 节点同步数据
主节点写入后,slave 会自动从 master 同步数据
主从复制原理
连接阶段
slave node 启动时(执行 slaveof 命令),会在自己本地保存 master node 的 信息,包括 master node 的 host 和 ip
slave node 内部有个定时任务 replicationCron(源码 replication.c),每隔 1 秒钟检查是否有新的 master node 要连接和复制,如果发现,就跟 master node 建立 socket 网络连接,如果连接成功,从节点为该 socket 建立一个专门处理复制工作的文件 事件处理器,负责后续的复制工作,如接收 RDB 文件、接收命令传播等 当从节点变成了主节点的一个客户端之后,会给主节点发送 ping 请求。
数据同步阶段
master node 第一次执行全量复制,通过 bgsave 命令在本地生成一份 RDB 快 照,将 RDB 快照文件发给 slave node(如果超时会重连,可以调大 repl-timeout 的值)。 slave node 首先清除自己的旧数据,然后用 RDB 文件加载数据
问题:生成 RDB 期间,master 接收到的命令怎么处理?
开始生成 RDB 文件时,master 会把所有新的写命令缓存在内存中。在 slave node
保存了 RDB 之后,再将新的写命令复制给 slave node。
命令传播阶段
master node 持续将写命令,异步复制给 slave node 延迟是不可避免的,只能通过优化网络。
repl-disable-tcp-nodelay no
一般默认都是no
当设置为 yes 时,TCP 会对包进行合并从而减少带宽,但是发送的频率会降低,从 节点数据延迟增加,一致性变差;具体发送频率与 Linux 内核的配置有关,默认配置为 40ms。
当设置为 no 时,TCP 会立马将主节点的数据发送给从节点,带宽增加但延迟变 小
问题:如果从节点有一段时间断开了与主节点的连接是不是要重新全量复制一遍? 如果可以增量复制,怎么知道上次复制到哪里
通过 master_repl_offset 记录的偏移量
info replication
master_repl_offset:7777777
不足之处:
RDB 文件过大的情况下,同步非常耗时
在一主一从或者一主多从的情况下,如果主服务器挂了,对外提供的服务就不可 用了,单点问题没有得到解决。如果每次都是手动把之前的从服务器切换成主服务器, 这个比较费时费力,还会造成一定时间的服务不可用
高可用的保证
Redis sentinel(哨兵)
原理:
通过运行监控服务器来保证服务的可用性
Sentinel 做集群的部署。Sentinel 既监控 所有的 Redis 服务,Sentinel 之间也相互监控,Sentinel之间没有主从之分
服务下线
主观下线
Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令。如果在 down-after-milliseconds 内都没有收到有效回复,Sentinel 会将该服务器标记为下线
sentinel down-after-milliseconds <master-name> <milliseconds>
客观下线
询问其他节点是否主节点下线,如果得到的多数!!回复是下线那么就是下线了 master 下线
故障转移
选举master
Raft算法
此处不做描述,已经根深蒂固了,可百度
总结:
Sentinle 的 Raft 算法和 Raft 论文略有不同。
master 客观下线触发选举,而不是过了 election timeout (任期)时间开始选举。
Leader 并不会把自己成为 Leader 的消息发给其他 Sentinel。其他 Sentinel 等 待 Leader 从 slave 选出 master 后,检测到新的 master 正常工作后,就会去掉客观下 线的标识,从而不需要进入故障转移流程
Redis sentinel(哨兵)的集群配置
参考百度
哨兵机制的不足
主从切换的过程中会丢失数据,因为只有一个 master。
只能单点写,没有解决水平扩容的问题
如果数据量非常大,这个时候我们需要多个 master-slave 的 group,把数据分布到 不同的 group 中
分组带来的问题:
数据怎么分片?分片之后,怎么实现路由?
分布式解决方案
客户端
第一种是在客户端实现相关的逻 辑,例如用取模或者一致性哈希对 key 进行分片,查询和修改都先判断 key 的路由
Redis Sharding
Sharded 分片的原理?怎么连接到某一个 Redis 服务?
1:根据redis节点名称虚拟出160*weight个虚拟节点。
2:在对key进行存取时,先通过keytag(key或key的一部分)计算出hash值,找到离他最近的比它大的虚拟节点,然后把请求发送到这个节点上。
优点
优势是配置简单,不依赖于其他中间 件,分区的逻辑可以自定义,比较灵活
缺点
基于客户端的方案,不能实现动态的服务 增减,每个客户端需要自行维护分片策略,存在重复代码
代理
第二种是把做分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端连接到 这个代理服务,代理服务做请求的转发
以前开源Codis、Twemproxy,知道即可
RedisCluster
基于服务端实现,官方出品,3.0版本以后
数据怎么相对均匀地分片
哈希后取模
一致性hash,不说了你也很懂了
但是Redis使用的:虚拟槽分区
Redis 的每个 master 节点维护一个 16384 位
分支主题
对 key 用 CRC16 算法计算再%16384,得到一个 slot 的值,数据落到负责这个 slot 的 Redis 节点上
查看key属于哪个slot槽
cluster keyslot abc
怎么让相关的数据落到同一个节点上?很重要
key 里面加入{hash tag}即可
set a{abc}a 1
set a{abc}b 1
set a{abc}c 1
原因是redis去取{}之间的字符串进行槽的计算
客户端连接到哪一台服务器?访问的数据不在当前节点上,怎么办?
客户端重定向
举例说明:
set abc 1
(error) MOVED 13724 127.0.0.1:7293
用./redis-cli -c -p port 的命令(c 代表 cluster)
Jedis 等客户端会在本地维护一份 slot——node 的映射关系,大部分时候不需要重 定向,所以叫做 smart jedis(需要客户端支持)。
数据迁移
因为 key 和 slot 的关系是永远不会变的,当新增了节点的时候,需要把原有的 slot 分配给新的节点负责,并且把相关的数据迁移过来
redis-cli --cluster add-node 127.0.0.1:7291 127.0.0.1:7297
新增加节点
新增的节点没有哈希槽,不能分布数据,在原来的任意一个节点上执行:
redis-cli --cluster reshard 127.0.0.1:7291
输入需要分配的哈希槽的数量(比如 500),和哈希槽的来源节点(可以输入 all 或 者 id)。
按操作步骤执行命令,并确定,有交互的回答
高可用和主从切换原理
只有主节点可以写,一个主节点挂了,从节点怎么变成主节点?
slave 发现自己的 master 变为 FAIL
将自己记录的集群 currentEpoch 加 1,并广播 FAILOVER_AUTH_REQUEST 信息
其他节点收到该信息,只有 master 响应,判断请求者的合法性,并发送 FAILOVER_AUTH_ACK,对每一个 epoch 只发送一次 ack
尝试 failover 的 slave 收集 FAILOVER_AUTH_ACK
超过半数后变成新 Master
广播 Pong 通知其他集群节点。
Redis Cluster 可以看成是由多个 Redis 实例组成的数据集合。客户端不需要关注数 据的子集到底存储在哪个节点,只需要关注这个集合整体
搭建,参考百度
优点
无中心架构
数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布
可扩展性,可线性扩展到 1000 个节点(官方推荐不超过 1000 个),节点可动 态添加或删除
高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副 本,能够实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制 完成 Slave 到 Master 的角色提升
降低运维成本,提高系统的扩展性和可用性。
不足
Client 实现复杂,驱动要求实现 Smart Client,缓存 slots mapping 信息并及时 更新,提高了开发难度,客户端的不成熟影响业务的稳定性
节点会因为某些原因发生阻塞(阻塞时间大于 clutser-node-timeout),被判断 下线,这种 failover 是没有必要的
数据通过异步复制,不保证数据的强一致性。
多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容 易出现相互影响的情况
0 条评论
下一页