redis学习笔记
2022-03-22 17:29:54 1 举报
AI智能生成
redis经常使用到分布式锁和数据缓存,了解其底层的实现原理尤为的重要
作者其他创作
大纲/内容
应用场景
秒杀
缓存
分布式锁
常用数据类型
string
底层结构
简单动态字符串
list
底层结构
双向链表
压缩列表
hash
底层结构
哈希表
压缩列表
set
底层结构
整数数组
哈希表
zset
底层结构
压缩列表
跳表
底层数据结构
简单动态字符串
双向链表
压缩列表(节约空间)
zlbytes:列表长度
zltail:列表尾的偏移量
zllen:列表中entity的个数
zlend:列表结束
哈希表(快)
跳表(快)
跳表在链表的基础上,增加了多级索引
整数数组(节约空间)
Redis键值的底层结构
键值对描述
Redis使用一个哈希表来保存键值对,一个哈希表就是一个数组,数组的元素称为一个哈希桶,
每个哈希桶保存一个键值对数据,哈希桶中的元素保存的不是值本身,而是kekey和value的指针(索引)
每个哈希桶保存一个键值对数据,哈希桶中的元素保存的不是值本身,而是kekey和value的指针(索引)
变慢?
原因
哈希表的冲突问题和 rehash 可能带来的操作阻塞
解决
链式哈希
描述:同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接
缺点:哈希冲突链上的元素只能通过指针逐一查找再操作。如果哈希表里写入的数据越来越多,
哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低
哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低
解决:采用rehash解决
rehash
描述:扩容操作
缺点:涉及大量的数据拷贝
解决:渐进式 rehash
查询的时候根据索引顺便移到新的hash表中
Redis为什么那么快?
大部分操作在内存上完成
高效的数据结构,例如哈希表和跳表
多路复用机制
实现
1. 基于linux select/epoll
2. 内核可同时监听多个监听套接字(socket)和 多个已连接套接字
3. 一旦内核监听到套接字(socket)上有数据返回,立刻交给redis线程处理数据
描述
Redis的网诺I/O和键值对读写是有一个线程来完成的,但是对于持久化、异步删除和 集群同步等有额外的线程执行
Redis采用多路复用机制使在网诺I/O中并发处理大量的客户端请求,实现高吞吐量
“多路”指的是多个网络连接
“复用”指的是复用同一个Redis处理线程
采用单线程,避免了不必要的线程切换和竞争条件,也不存在多线程导致的切换消耗cpu,不需要考虑各种锁的问题,不需要执行加锁和释放锁的操作
Redis的持久化
AOF日志
实现方式
Redis是先写内存再写日志
好处
可以避免出现记录错误命令的情况
不会阻塞当前的写操作
坏处
AOF 日志在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了
持久化策略
Always
同步写回:每个写命令执行完,立马同步的将日志写回磁盘
Everysec
每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
No
操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
日志重写
由bgrewriteaof 子进程来完成的,不用主线程参与
重写机制:对过大的AOF文件进行重写,以此来压缩AOF文件的大小
查当前键值数据库中的键值对,记录键值对的最终状态,从而实现对某个键值对重复操作后产生的多条操作记录压缩成一条
缺点
恢复数据慢
文件很大
RDB 快照
描述
把某一时刻的状态以文件的形式写到磁盘上
实现方式
save:在主线程中执行,会导致阻塞
bgsave:创建一个子线程,专门来写入RDB快照文件,避免主线程阻塞,也是redis的默认配置
生成时fork子进程的读写操作
读:主线程和bgsave子进程互不影响
写:被修改的数据会被复制一份为副本,bgsave 把副本数据写入rdb 文件,主线程修改原数据
缺点
快照生成时间久,消耗cpu
fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢
Redis的主从复制
全量复制
第一阶段:主从库间建立连接
1、从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数
2、主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数
第二阶段:主库将所有数据同步给从库
主库
1、执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库
2、在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作
从库
接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件
第三阶段:主库会把第二阶段执行过程中新收到的写命令发送给从库
1、当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库
2、从库再重新执行这些操作
主从断网后增量复制
实现方式
repl_backlog_buffer缓冲区
当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区
repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置master_repl_offset,从库则会记录自己已经读到的位置slave_repl_offset,在网络断连阶段,主库可能会收到新的写操作命令,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行
Redis的哨兵机制
负责任务
监控
哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行
下线状态
主从库
从库:没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”
主库:没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程
主观下线:如果哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”
客观下线:当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线。这个判断原则就是:少数服从多数
选主
从很多个从库里,按照一定的规则选择一个从库实例,把它作为新的主库
筛选
down-after-milliseconds * 10(新建 sentinal.conf 文件进行配置,不在 redis.conf 中),如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了
发生断连的次数超过了 10 次,就说明这个从库的网络状况不好,不适合作为新主库
打分
从库优先级
优先级最高的从库得分高(通过 slave-priority 配置项)
从库复制进度
和旧主库同步程度最接近的从库得分高(有从库的 slave_repl_offset 最接近 master_repl_offset,那么它的得分就最高)
从库 ID号
ID 号小的从库得分高(在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库)
通知
从库:把新主库的连接信息发给其他从库,让它们执行 replicaof 命令和新主库建立连接,并进行数据复制
客户端:哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上
集群配置
配置方式
1、基于 pub/sub 机制的哨兵集群组成过程
主库上有一个名为“__sentinel__:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的
2、基于 INFO 命令的从库列表,这可以帮助哨兵和从库建立连接
哨兵给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵
3、基于哨兵自身的 pub/sub 功能,这实现了客户端和哨兵之间的事件通知
要保证所有哨兵实例的配置是一致的,尤其是主观下线的判断值 down-after-milliseconds
leader的条件
1、拿到半数以上的赞成票
2、拿到的票数同时还需要大于等于哨兵配置文件中的 quorum(赞成票) 值
Redis实现分布式锁
Redis原子操作
INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作
把多个操作写到一个 Lua 脚本中,以原子性方式使用 Redis 的 EVAL命令执行单个Lua 脚本
常见分布式锁
1、数据库悲观锁
实现:select...for update
1、在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
2、如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常
3、如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了
4、其间如果有其他事务对该记录做加锁的操作,都要等待当前事务解锁或直接抛出异常
2、数据库乐观锁
实现:Compare and Swap(CAS)技术
当我们提交更新的时候,判断数据库表对应记录的当前version与第一次取出来的version进行比对,如果数据库表当前version与第一次取出来的version相等,则予以更新,否则认为是过期数据
问题
ABA问题. 解决的办法是version字段顺序递增
乐观锁的方式,在高并发时,只有一个线程能执行成功,会造成大量的失败,这给用户的体验显然是很不好的
3、基于Redis的分布式锁
实现:setnx和expire两个命令完成
如何保障setnx和expire两个操作的原子性?
使用set的命令时,同时设置过期时间,不再单独使用 expire命令
语法:set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)
实现
加锁:jedis.set(String key, String value, String nxxx, String expx, int time)
解锁
写一个的Lua脚本代码
将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId
使用lua脚本,将加锁的命令放在lua脚本中原子性的执行
4、基于ZooKeeper的分布式锁
Redis键的删除策略
1、在未达到最大内存使用限制时
定时删除
描述 :为每个键设置一个定时器,一旦过期时间到了,则将键删除
缺点:对内存很友好,但是对 CPU 不友好,因为每个定时器都会占用一定的 CPU 资源
惰性删除
描述:不管键有没有过期都不主动删除,等到每次去获取键时再判断是否过期,如果过期就删除该键,否则返回键对应的值
缺点:对内存不够友好,可能会浪费很多内存
定期扫描
描述:系统每隔一段时间就定期扫描一次,发现过期的键就进行删除
缺点:定期的频率要结合实际情况掌控好,使用这种方案有一个缺陷就是可能会出现已经过期的键也被返回
2、达到最大内存以后,会根据配置项maxmenory-policy来进行相应的淘汰策略
volatile-lru:对设置了过期时间的键进行近似LRU算法
LRU,即:最近最少使用淘汰算法(Least Recently Used)
allkeys-lru:对所有件进行近似LRU算法
volatile-lfu:对设置了过期时间的键进行近似LFU算法
LFU,即:最不经常使用淘汰算法(Least Frequently Used)
allkeys-lfu:对所有件进行近似LFU算法
volatile-random:随机淘汰设置了过期时间的键
allkeys-random:随机淘汰键
volatile-ttl:淘汰最接近过期时间的键
noeviction:啥事也不干
Redis如何保证数据和缓存一致性
1、对于读写缓存来说
策略
同步直写策略:写缓存时,也同步写数据库,缓存和数据库中的数据一致
异步写回策略:写缓存时不同步写数据库,等到数据从缓存中淘汰时,再写回数据库。使用这种策略时,如果数据还没有写回数据库,缓存就发生了故障,那么,此时,数据库就没有最新的数据了
2、对于只读缓存来说
数据新增
直接写入数据库
数据删改
需要把只读缓存中的数据标记为无效
策略
先删除缓存值,再更新数据库
是否有并发
无
缓存删除成功,数据库更新失败
影响:应用从数据库读到旧数据
解决:重试数据库更新操作
有
缓存删除成功后,数据库未更新成功,此时有并发线程
影响:并发从数据库读到旧值更新到缓存,导致缓存一直是旧值
解决:延迟双删
1、先删除缓存
2、再更新数据库
3、休眠一会(比如1秒),再次删除缓存
休眠时间需要大于并发线程读取数据再写入缓存的时间
先更新数据库,再删除缓存值
是否有并发
无
数据更新成功,缓存删除失败
影响:应用从缓存读到旧值
解决:重试缓存删除
有
数据更新成功,缓存删除失败,此时有并发线程
影响:并发请求从缓存读到旧值
解决
同步biglog异步删除缓存(canal技术监听binlog)
https://github.com/alibaba/canal
https://github.com/alibaba/canal
1、读取缓存中是否有相关数据
2、如果缓存中有相关数据value,则返回
3、如果缓存中没有相关数据,则从数据库读取相关数据放入缓存中key->value,再返回
4、如果有更新数据,则先更新数据库,再删除缓存
5、为了保证第四步删除缓存成功,使用binlog异步删除
6、如果是主从数据库,binglog取自于从库
7、如果是一主多从,每个从库都要采集binlog,然后消费端收到最后一台binlog数据才删除缓存,或者为了简单,收到一次更新log,删除一次缓存
删除缓存重试机制
1、写请求更新数据库
2、缓存因为某些原因,删除失败
3、把删除失败的key放到消息队列
4、消费消息队列的消息,获取要删除的key
5、重试删除缓存操作
建议
先更新数据库再删除缓存
先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力
如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置
Redis常见问题
缓存雪崩
缓存击穿
缓存穿透
0 条评论
下一页