Redis复习系列

Redis

Redis底层原理

基本数据类型

请添加图片描述

内部数据结构

https://zhuanlan.zhihu.com/p/70174863

https://baijiahao.baidu.com/s?id=1642948039877235233&wfr=spider&for=pc

string

Simple dynamic string (SDS)

struct sdshdr{
     //记录buf数组中已使用字节的数量
     //等于 SDS 保存字符串的长度
     int len;
     //记录 buf 数组中未使用字节的数量
     int free;
     //字节数组,用于保存字符串
     char buf[];
}
  1. 常数级时间复杂度 获取串长度
  2. 采用了空间预分配策略避免C字符串每一次修改时都需要进行内存重分配(C是修改N次就必然分配N次,SDS是修改N次最多分配N次)
  3. 惰性空间释放用于优化SDS的 字符串缩短 操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性见这些字节的数量记录起来,并等待将来使用。
  4. 字符串扩展 时并不是必然进行内存分配(有free的存在)
  5. 可以保存二进制数据(’\0’不作为结束符)
list

双向链表

请添加图片描述

hash

字典在Redis中应用非常广泛,Redis数据库本身就是使用字典来作为底层实现的,比如我们执行命令:SET msg “hello world”,redis就会在数据库中创建一个键为msg值为"hello world"的字典。

字典在实现上包含两个hash表,常规状态下,只有第一个hash表存储数据,当发生rehash时,会给第二个hash表分配空间,并且把第一个hash表中的数据拷贝到第二个hash表中。如果hash表中的数据量比较大时,会采用渐进式rehash,分多次完成,字典中有个rehashindex字段记录rehash进度(如果当前未进行rehash值为-1),这样的话在字典中查询数据时,可能会查找两个hash表,先查第一个表,如果查不到再查第二个,但是新增的数据都会添加到第二个hash表中,发生rehash之后会由第二hash表负责查询客户端请求数据。

请添加图片描述

请添加图片描述

请添加图片描述

set

set是一个无序的、自动去重的集合数据类型,Set底层用两种数据结构存储,一个是hashtable,一个是inset

inset为可以理解为数组,使用inset数据结构需要满足下述两个条件:

  • 元素个数不少于默认值512
  • 元素可以用整型表示
zset
  1. zset为有序(有限score排序,score相同则元素字典序),自动去重的集合数据类型,其底层实现为 字典(dict) + 跳表(skiplist)
  2. 当数据比较少的时候用ziplist编码结构存储。
ziplist

请添加图片描述

字典(dict) + 跳表(skiplist)

字典:保证数据是唯一,并且查询的时间复杂度为O(1)

跳表:保持有序,并且便于数据的插入

跳表

可看做是二叉树的一个变种,在原有有序链表上面添加了多级索引,然后通过索引来实现快速查找。
在这里插入图片描述

它在性能上和红黑树,AVL树相差无几。

平衡二叉树也可以实现以上效果,那么为什么选择跳表呢?

  1. 范围查找:跳表的范围查找更加简单(只需要找到最小值之后,对最底层进行若干步数的遍历)

  2. 插入删除逻辑简单:平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。

  3. 内存占用:从内存占用上来说,与平衡树相差无几,甚至更灵活一些。

    一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。

    如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

跳表(skiplist)

跳表2

请添加图片描述
请添加图片描述

应用场景

String分布式锁:使用setnx("lock", "Redis Lock!!!")
计数器(帖子阅读量)
共享Session
List消息队列
redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端是用lupsh从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞时的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡 和高可用性

队列
Hash放置对象:
Student:{
ID1:{Name,age,sex,…}
ID2:{Name,age,sex,…}
ID3:{Name,age,sex,…}
…}
Set共同好友
Zset排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等。

需要统计某个商家商品的实时销量排行,可以使用SQL语句,根据销量字段排序,但是这个方法需要进行全表扫描,当数据量非常大的时候,效率很低

redis实现商品销量排行榜

  1. 当存在点赞的时候,更新zset对应记录,并更新Mysql
  2. 使用reverseRangeWithScores获取对应排行榜

单线程

Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程;

其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。

请添加图片描述

Redis只是在执行客户端提交的命令时会进行排队然后逐条执行,其他的操作,比如持久化、异步删除、集群同步都是由其它的额外线程执行的。

所以我们常常说的Redis单线程其实是命令执行的时候的单线程

单线程的优势

  1. 不存在多线程情况中的上下文切换而消耗CPU
  2. 编码逻辑更简单
  3. 不需要考虑各种的问题,也不需要考虑死锁问题

Redis为什么很快

  1. 绝大部分是纯粹的内存操作
  2. 采用单线程,不存在上下文切换和多线程中线程竞争的问题
  3. 高效的数据结构
  4. 非阻塞式IO,多路复用IO,采用epoll作为IO多路复用技术的实现

BIO,NIO,IO复用,AIO,同步,异步,阻塞,非阻塞

多路复用之前,有必要讲一下什么是阻塞IO、非阻塞IO、同步IO、异步IO这几个东西;linux的五种IO模型

  1. 阻塞I/O(blocking I/O)
  2. 非阻塞I/O(nonblocking I/O)
  3. I/O复用(select和poll)(I/O multiplexing)
  4. 信号驱动I/O(signal driven I/O (SIGIO))
  5. 异步I/O(asynchronous I/O (the POSIX aio_functions))。

**同步:**等待应答结果时,主动的获取消息结果;

​ 可以什么都不干一直等待结果(阻塞);也可以先干其他事情,但是每隔一段时间进行询问应答结果(非阻塞,轮询)

**异步:**等待应答结果时,等待别人通知;

IO复用,AIO,BIO,NIO,同步,异步,阻塞和非阻塞

多路复用

I/O模型

网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  • 第一阶段:等待数据准备 (Waiting for the data to be ready)。

  • 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。

对于socket流而言:

  • 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区
  • 第二步:把数据从内核缓冲区复制到应用进程缓冲区。
阻塞式I/O

最广泛的模型是阻塞I/O模型,默认情况下,所有套接口都是阻塞的。 进程调用recvfrom系统调用,*整个过程是阻塞的*,直到数据复制到进程缓冲区时才返回(当然,系统调用被中断也会返回)

请添加图片描述

非阻塞式I/O

请添加图片描述

当我们把一个套接口设置为非阻塞时,就是在告诉内核,当请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。当数据没有准备好时,内核立即返回EWOULDBLOCK错误,第四次调用系统调用时,数据已经存在,这时将数据复制到进程缓冲区中。这其中有一个操作时轮询(polling)。

I/O多路复用

阻塞I/O只能阻塞一个I/O操作,而I/O复用模型能够阻塞多个I/O操作,所以才叫做多路复用

请添加图片描述

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,*使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求*。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

Select, Poll, Epoll

select,poll机制:

当触发IO事件之后,会无差别轮询所有的流,找出可出发IO事件的流,并进行处理!

selectpoll
最大文件描述符数量有限制无限制
底层结构数组链表

select 有最大连接数目的限制(数组实现)

poll没有最大连接数目的约束(链表实现)

Epoll机制:

当触发IO事件,Epoll方式会直接处理触发IO事件的流的集合,而不需要遍历所有的流。

selectpollepoll
最大文件描述符数量有限制
底层结构数组链表红黑树
处理方式遍历所有fd,找出活跃FD遍历所有fd,找出活跃fd只处理活跃可用的fd
内存拷贝内核需要将消息传递到用户空间,都需要内核拷贝动作都需要内核拷贝动作利用mmap()文件映射内存加速与内核空间的消息传递(内核和用户空间共享一块内存)

polling

while true
{  
 for i in stream[]  // 忙轮询的方式,stream代表不同流组成的数组
 {  
     if i has data  
     read until unavailable  
 }  
}  

要把所有流从头到尾查询一遍,就可以处理多个流了,但这样做很不好,因为如果所有的流都没有I/O事件,白白浪费CPU时间片。

— 待确认

Select

while true 
{  
    select(streams[]) //这一步死在这里,知道有某个流有I/O事件时,才往下执行  
    for i in streams[]  
    {  
        if i has data  
        read until unavailable  
    }  
}  

select那里仅仅能知道有I/O事件发生,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以***select具有O(n)的无差别轮询复杂度***,同时处理的流越多,无差别轮询时间就越长。

epoll

while true  
{  
    active_stream[] = epoll(epollfd)  // epoll会把具体哪些流发生了I/O事件通知我们
    for i in active_stream[]  
    {  
        read or write till  
    }  
} 

****epoll可以理解为event poll****,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。

selectepoll这2个系统调用都可以在内核准备好数据(网络数据到达内核)时告知用户进程

信号驱动I/O

等待数据报到达(第一阶段)期间,进程可以继续执行,不被阻塞。免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler处理。

请添加图片描述

异步I/O

告诉内核启动某个操作,并让内核在整个操作(包括第二阶段,即将数据从内核拷贝到进程缓冲区中)完成后通知我们。

请添加图片描述

总结

在这里插入图片描述

Redis缓存问题

img

缓存击穿

定义:热点数据失效,大量请求打到MySQL层面。

**解决方案1:**互斥锁 + 阻塞

在并发的多个请求中,只有第一个请求线程能拿到锁执行数据库查询操作,其他的线程拿不到锁就阻塞,等到第一个线程将数据写入缓存后,其他请求直接走缓存。

锁:redis的setnx方法

**解决方案2:**分级缓存 + 锁

采用两级缓存(L1、L2),L1缓存失效时间短,L2缓存失效时间长。

请求优先从L1缓存获取数据,如果L1缓存未命中则加锁,

只有一个线程获取到锁,获取到锁的线程从数据库中读取数据,并将数据更新到L1和L2缓存中,在数据更新到L1和L2缓存之前,其他请求从L2缓存中获取数据。

避免缓存同时失效。所以当数据更新时候,智能淘汰L1缓存,不能将 L1 和 L2 同时淘汰。
L2中可能存在脏数据,需要业务能够容忍这种短时间的不一致。并且存在额外的空间浪费

缓存雪崩

**定义:**大量缓存同时失效

解决方案1:随机值设置

设置缓存失效时间时候,加上随机值,让失效时间更加均匀

解决方案2:排队访问

采用队列或者加锁的方式,排队访问数据库

解决方案3:分级缓存

设置二级缓存机制,这样就能够保证即时第一层缓存失效了,还有第二级缓存的保障;不过就增加了系统复杂性。

缓存穿透

数据本身就不存在(Redis、MySQL),一般来说存储层查不到的数据则不会写入缓存,这将导致这个不存在的数据会一直访问MySQL。

解决方案1:不存在的数据缓存 NULL

将数据库中的空值也缓存到缓存层中,这样查询该空值就不会再访问数据库,而是直接在缓存层访问就行。

问题:占用太多缓存!

解决:设置较短的过期时间!

解决方案2:布隆过滤器

当一个查询请求过来时,先经过布隆过滤器进行查,如果判断请求查询值存在,则继续查;如果判断请求查询不存在,直接丢弃。

将所有可能存在的数据通过 Khash函数,然后将K个hash值在 位数组 中对应位置设置为 1(默认为0)。

布隆过滤器 :K 组哈希函数、位数组

当判断 数据X 是否存在时,首先使用 Khash函数 计算哈希,然后分别查看对应的位置是否都为1;若全为1,则表示可能存在该数据,若不全为1,则表示一定不存在该数据。

可能存在:可能刚好另外几个数据的hash值就刚好覆盖了 数据X 的对应位置。

一定不存在:只要有一个Hash对应位置为0,表示没有数据可以映射到对应位置。

优势:

  1. 使用bit存储,节省空间
  2. 查询复杂度为 O(K),K为Hash函数的数目

缺陷:

  1. 假阳性问题,判断存在的不一定存在
  2. 只能插入和查询元素,不能删除元素

Redis事务

持久化

redis的rdb和aof的详细介绍

redis原理之rdb持久化

Redis的数据放置在内存中,一旦服务器断电,进程异常退出等情况发生,Redis中的数据久会丢失!

为了防止以上情况的发生,所以提出了持久化方法!

启动:

  1. Redis服务器启动之后,会读取持久化文件
  2. 优先使用 AOF (Why:AOF更新的频率更高,数据更加完整)
  3. AOF未开启, 使用RDB

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uBmJTrOT-1638362285418)(Redis.assets/webp.webp)]

RDB(Redis DataBase)

功能:保存Redis数据库的快照,用于进行数据恢复,与主从复制

形式:经过压缩的二进制文件(保存在 磁盘 中)

方法:save和bgsave

  • 前者会阻塞当前的业务,而去进行saveRDB
  • 后者通过fork构建子进程,异步的进行savaRDB
触发方式

**1. 自动触发:**可以进行设置save 方法,让其定期进行持久化。

save 600 1 表示 600 秒以内进行了至少 1 次修改,则进行持久化

save 60 10000 表示 60 秒以内进行了至少 10000 次修改,则进行持久化

2. 手动触发:

主动调用 save,bgsave方法

save

阻塞服务进程,直到RDB持久化完成,期间不能处理任何请求!

bgsave

功能:fork一个子进程,并由子进程将内存中的数据生成快照并且存储到硬盘介质上。

bgsave流程:

  1. fork子进程(拷贝一个新的自己)
  2. 子进程将内存中数据写到临时文件
  3. 持久化完成将临时文件替换之前的持久化文件
  4. 销毁子进程,释放资源
// 伪代码
def SAVE():
    # 创建RDB文件
    rdbSave()

def BGSAVE():
    # 创建子进程
    pid = fork()
    if pid == 0: ## 这个if语句块中,是子进程执行
        # 子进程负责创建RDB文件
        rdbSave()
        # 完成之后向父进程发送信号
        signal_parent()

    elif pid > 0:
        # 父进程继续处理命令请求,并通过轮询等待子进程的信号
        handle_request_and_wait_signal()
    else:
        # 处理出错情况
        handle_fork_error()

在此之前需要了解 fork()

功能: fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程

一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间以及内存数据。相当于把原来的进程的所有资源都复制到了新的新进程中。相当于克隆了一个自己。也就是说此时内存使用量是存储之前的2倍

fork返回后,子进程和父进程都从调用fork函数的下一条语句开始运行。

注意:

内存开销:fork的方式需要双倍于Redis的内存,内存开销大。若内存不足,则使用save的方式(阻塞服务进程),直到持久化结束释放内存。

IO频繁

save与bgsave的区别

save:阻塞服务进程。

bgsave:双倍内存消耗。

AOF(Append Only File)

作用:将redis执行过程中的所有写指令以 追加的方式 写到一个文件!

形式:类似于指令的方式存储,可读性强

在这里插入图片描述

步骤:

  1. 命令追加:所有写入的命令会追加到 AOF缓冲区
  2. 文件同步:根据对应的策略,将 AOF缓冲区 的数据同步到 AOF文件
  3. 文件重写:压缩 AOF文件
1. 命令追加

所有的写入命令会追加到AOF 缓冲区的中。

大家可能会有疑问了,直接将写命令写到aof文件不就行了嘛,为什么还要写入到一个aof缓冲区呢?缓冲区的作用是什么呢?

  1. 减少IO次数,提高效率。redis使用单线程响应命令,如果每次写入AOF文件都直接加到硬盘,每次IO都会阻塞主进程,那么性能完全取决于当前的硬盘负载了。
  2. 为文件同步策略提供基础。写入AOF缓冲区还有另一个好处,redis可以提供多种缓冲区同步硬盘策略也就是文件同步的同步策略,在性能和安全性方面作出权衡
2. 文件同步

AOF缓冲区 同步到 AOF文件

redis.conf文件中对应的策略一共有三个选择:

  1. 命令:每执行一个写命令进行一次保存
  2. 时间:每秒钟保存一次
  3. 不保存
appendfsync always:每次执行完一个写命令之后,直接进行文件同步,而不需要写入到aof缓冲区内
appendfsync everysec:每秒从aof的缓冲区内执行一次文件同步
appendfsync no:不保存。
3. 文件重写

3. 文件重写

**目的:**压缩AOF文件

随着aof文件越来越大,需要定期清理aof文件进行重写,达到压缩的目的。

**执行时机:**此过程只有在AOF文件达到重写的标准时才会执行,并不是每次aof持久化时都执行。

默认的重写标准是:AOF文件到达 64 M 时候,进行重写

通过该功能来创建一个新的 AOF 文件来代替旧文件。并且两个文件所保存的数据库状态一样(就是redis的数据是一样的),但新文件不会包含任何冗余命令,所以新文件要比旧文件小得多。

Question:为什么新文件不会包含任何冗余命令呢?

一般可以这样理解:旧文件保存了对某个 key 有 4 个 set 命令,经过重写之后,新文件只会记录最后一次对该 key 的 set 命令!!!

但是,实际上,重写之后并不是key最后一条命令,而是类似rdb存储的二进制文件。重写功能是通过读取服务器当前的数据库状态来实现的(类似rdb的快照)

这个重写功能和rdb持久化存储很像:

也是fork一个子进程,在子进程内创建一个临时文件,然后读取当前数据库状态后以命令行的形式写入到新的AOF文件中,没有冗余命令。

写入新AOF文件的所有IO操作都是在子进程内操作,不会阻塞主进程。注意RDB写入临时文件的内容和AOF写入临时文件的内容都是一些二进制文件。

当新的AOF文件全部写入成功后,替换旧的AOF文件,销毁子进程。重新完成

所以这个重写过程非常类似rdb的存储过程

**Question:重写过程中可能导致 Redis 与 AOF文件 不一致!!!**那么这里又会存在一个问题,子进程在重写过程中,服务器还在继续处理命令请求,新命令可能会对数据库进行修改,这会导致当前数据库状态和重写后的 AOF 文件,所保存的数据库状态不一致

Redis 设置了一个 AOF 重写缓冲区。在子进程执行 AOF 重写期间,主进程需要执行以下三个步骤:

  1. 执行客户端的请求命令
  2. 将执行后的写命令追加到 AOF 缓冲区
  3. 将执行后的写命令追加到 AOF 重写缓冲区

当子进程结束重写后,会向主进程发送一个信号,主进程接收到之后会调用信号处理函数执行以下步骤:

  1. 将 AOF 重写缓冲区内容写入新的 AOF 文件中。此时新文件所保存的数据库状态就和当前数据库状态一致了。
  2. 对新文件进行改名,原子地覆盖现有 AOF 文件,完成新旧文件的替换。
  3. 当函数执行完成后,主进程就继续处理客户端命令。

因此,在整个 AOF 重写过程中,只有在执行信号处理函数时才会阻塞主进程,其他时候都不会阻塞。

有时aof文件的大小还没有触发重写机制,但是文件已经很大了,我们想手动触发重写,可以使用下面的命令:

bgrewriteaof

主从复制

哨兵

集群

Redis没有使用一致性Hash,而是引入了哈希槽的概念。

Hash槽

Redis-集群中有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。

slot = CRC16(key)/16384

Cluster中的每个节点负责一部分 hash槽(hash slot),比如集群中存在三个节点,则可能存在的一种分配如下:

  • 节点A 包含0到5500号哈希槽;
  • 节点B 包含5501到11000号哈希槽;
  • 节点C 包含11001 到 16384号哈希槽。

CRC16校验:检查数据是否OK

16384取模:计算Hash值

在这里插入图片描述

集群扩容

  1. 准备新节点

  2. 加入集群

  3. 迁移槽和数据

    1. 首先需要为新节点制定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点。迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀。
    2. 数据传输

如果扩充一个节点D,只需要将A、B、C节点中的部分槽放置在D上;

集群收缩

在这里插入图片描述

  1. 节点下线,迁移槽
  2. 集群内的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点。

如果想移除节点A,只需要将A的slot转移到B和C节点上。

分布式锁

setnx在集群模式下会有什么问题?

多个客户端拿到同一把锁!

  1. 客户端A主节点 拿到锁X
  2. 在主节点将锁X同步到 从节点 之前怠机
  3. 从节点被选为 新的主节点
  4. 这时候客户端B 从新的主节点 获取到锁X

解决方案:RedLock

XMIND

淘汰、过期策略

在这里插入图片描述

单线程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8vEsGWgK-1638362285420)(Redis.assets/image-20210816184104483.png)]

持久化

在这里插入图片描述

事务

在这里插入图片描述

缓存问题

在这里插入图片描述
在这里插入图片描述

分布式锁

在这里插入图片描述

主从复制

在这里插入图片描述

集群

在这里插入图片描述

面试题

redis mysql数据一致性解决方案?

Redis与Mysql双写一致性方案解析

单个请求的情况一般不会出现一致性问题,要考虑多个请求的情况!!!

在这里插入图片描述

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。

  1. 设置过期时间
  2. 先更新数据库,再更新缓存**(不考虑)**
  3. 先删除缓存,再更新数据库**(延时双删)**
  4. 先更新数据库,再删除缓存

先更新数据库,再更新缓存: (不考虑)

  • (1)线程A更新了数据库
  • (2)线程B更新了数据库
  • (3)线程B更新了缓存
  • (4)线程A更新了缓存

缺陷:

  1. 理想的顺序应该是1423,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据;
  2. 写操作较多的情况下,数据还没读,缓存就会频繁的更新

难 以 处 理 !!!!

先删缓存,再更新数据库:

  • (1) A 进行写操作,删除了缓存
  • (2) B 来读数据,发现缓存不存在, B请求数据库,查到 旧值 并写入缓存
  • (3) A 将新值写入数据库

缺陷:

这时候缓存和数据库不一致,并且不采用给缓存设置过期时间策略,该数据永远都是脏数据。

解决方案 1 : 延时双删策略
  1. 先删除缓存
  2. 写数据库
  3. 休眠 y 秒,再次删除缓存
public void write(String key,Object data){
        redis.delKey(key);
        db.updateData(data);
        Thread.sleep(1000);
        redis.delKey(key);
    }

y 秒:这个时间如何确定?

根据业务中读数据的耗时确定,比如:读请求耗时 x,我们可设置睡眠时间 y = x+几百ms;确保读请求结束,写操作可以删除读请求产生的脏数据

方案一,第二次删除缓存,如果删除失败怎么办

提供一个用于保障的重试机制;

流程如下所示:

  • (1)更新数据库数据
  • (2)数据库会将操作信息写入binlog日志当中
  • (3)订阅程序提取出所需要的数据以及key
  • (4)另起一段非业务代码,获得该信息
  • (5)尝试删除缓存操作,发现删除失败
  • (6)将这些信息发送至消息队列
  • (7)重新从消息队列中获得该数据,重试操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-En0eiOdL-1638362285425)(Redis.assets/image-20210811122936550.png)]

上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。

解决方案 2:异步更新缓存(基于订阅binlog的同步机制)

上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。

Redis怎么实现高可用性?

Redis主从复制机制,简单地实现了Redis高可用。然后,如果Master服务器宕机,会导致整个Redis瘫痪,这种方式的高可用性较低。正常会采用多台Redis服务器构成一个集群,即使某台,或者某几台Redis宕机,Redis集群仍能正常运行,从而提高其高可用性。

end

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值