当前位置:   article > 正文

redis工作原理;redis为什么这么快;redis单线程如何处理高并发;_redis怎么实现并发请求的

redis怎么实现并发请求的

引言

最近研究redis的相关底层知识,查阅了一些资料、博客,发现对于redis工作原理的说明不够用全面,同时针对“redis为什么这么快”的解答发现了一些问题。故基于这些文章结合自己的理解做了如下整理
【如发现有问题的地方,望指出】

redis工作原理

要聊redis的工作原理,那就要从工作流程出发,逐步分析:
1、首先redis采用了epoll I/O多路复用器:经过网络数据传输,大量的连接请求通过epoll接收并且监控,有数据的连接才放到redis中进行读写处理,所以首先就要聊聊epoll

2、请求数据进入到redis中后,redis进程采用单线程处理,命令完全串行,不用额外维护锁机制,因此单线程处理是redis快的原因之一

3、处理数据时,redis采用的底层数据结构简单,提供的api操作简单,没有实现比较复杂的逻辑,因为redis的设计理念更多的在于查询快上,要针对计算密集型处理,更多的建议在后端代码中实现。所以其次要了解redis各类数据类型采用的数据结构是什么,优点在哪里

4、数据请求处理完成后,为了迎接下一次数据请求,那么还需要实现的是保证内存空间可用,即内存占用满了redis怎么处理

5、同时保存在内存里的数据,如果重启就会消失的,那么redis是怎么实现持久化的呢?redis的持久化机制有哪些亮点?

1、epoll

网络传输时是需要建立连接的,但是建立完连接并不会马上就传输数据,特别是在大量连接请求的情况下,同一时间段有数据传输来的连接是少数。每个连接会建立一个socket,每个socket都有一个它自己的等待队列,当socket阻塞时就会把任务放到等待队列中,当它唤醒时就会调用等待队列中的任务

1、在BIO时期,一个请求如果进程没有反馈数据,则线程会一直阻塞在那里,直到数据传输完
2、在NIO时期,内核做了优化,线程非阻塞了,可以使用一个死循环来遍历所有的连接,看哪个连接有数据,没有就遍历下一个,有数据就唤醒这个连接对应的进程,但是被唤醒的进程是不知道是哪些socket有数据的,因此进程又要遍历一次它关注的那些连接,即和他建立了连接的socket(实际上是遍历的文件描述符,这里为方便理解直接用连接来说明,有兴趣可自查),来找到是哪些连接有数据传输过来,然后把这些连接传给内核,进行后续的读写操作
3、如果有1000个连接的话,就意味着光是知道连接有数据传来,就要遍历1000遍,这样的消耗是比较高的,能不能有什么东西可以直接监控这些连接,通过一次调用这个东西就知道进程关心的这些连接有没有数据传来。这个东西就是IO多路复用器。
4、最初的多路复用器是select,但是它只解决了监控这些连接,不用遍历就知道这些连接有数据传来。但是对于具体是哪些连接有数据,select还是得通过再遍历进程关心的所有连接才知道。所以select的消耗还是比较高的,所以规定了select能监控的连接数不能超过1024个。
5、那么能不能有一个多路复用器即不用再遍历一次,也没有1024的限制呢?也就引入了epoll。下面详细介绍下epoll的工作原理,方便大家理解:
(1)进程会首先调用epoll_create方法来创建一个eventpoll对象,这个对象中包含(不仅有)一个等待队列,一个就绪列表
(2)有请求向进程建立连接时,就会通过epoll_ctl来添加epoll对socket的监控,即将eventpoll的引用添加到socket的等待队列中
(3)socket有数据传来时,网卡会发出中断信号,然后内核会调用中断程序,中断程序就会唤醒socket的等待队列,就会执行eventpoll的监听事件,将socket的引用添加到eventpoll的就绪列表中
(4)同时进程会被唤醒,然后通过就绪列表就能知道哪些socket有数据传来,再把这些socket传入内核,执行后续的读写操作(这里实际传入的是文件描述符,为了方便理解说明为socket,本质一样)

总结:epoll能够接收大量的请求,并且筛选出有效的请求放给redis,且执行效率高消耗资源少

针对多路复用器的介绍不再做过多的展开,如果想要了解更多可以参考我另一篇博客:
多路复用器发展历程,工作原理,区别

2、redis数据类型的底层数据结构

这里只是简单的述说了redis中常用数据类型的底层数据结构,常说redis有5中数据类型:String,list,set,hash,zset。但是千万不要以为只有这5中数据结构,其他的可以自己做了解,这里单独提一下位图(bitmap)数据类型,针对有规律且连续的大量数据,用位图来存储可以实现很小的空间占用存储很大的数据量

(1)针对String的动态字符串(Simple Dynamic String)

redis是基于C语言开发的,在C语言中针对字符串的处理是比较耗费资源的,主要问题在于:
a.容易造成缓冲区溢出:字符串底底层是用了一个数组来存储的,如果在拼接的时候没有计算好内存空间,拼接进去的字符串比剩下的空间要大,就导致了溢出
b.要获取字符串长度,就要遍历一遍,复杂度是O(N)
c.内存重新配置:字符串变长/短都会对数组作内存重分配
针对上述问题redis实现了自己的SDS字符串结构——SDS:

struct sdshdr{
	int len;  // 字符串长度/已使用的空间的长度
	int free; // bug中空闲空间长度
	char buf[]; // 存储的实际内容
}
  • 1
  • 2
  • 3
  • 4
  • 5

具体怎么优化的?针对上述3个问题我们一个一个来看
a.针对溢出问题,redis中是有free记录的,拼接的时候看一下free够不够,不够就扩容,就避免了溢出了
b.可以直接通过len获取到字符串长度而不用每次都遍历
c.减少了内存的重新配置。SDS提供了两种优化策略:空间预分配和惰性空间释放
空间预分配:当扩容时除了分配必需的内存空间还会额外分配未使用的空间:如果字符串大小小于1M时,直接扩大一倍,如果大于1M,那就扩大1M的空间
惰性空间释放:缩容时,不回收多余的内存空间,而是用free记录下多余的空间,后续再有操作直接使用free的空间

(2)针对hash的字典结构

所谓字典结构,其实就是键值对结构,redis中的字典是用的哈希表来实现(可以与hashMap联想理解,同时这里介绍字典概念是为了理解后续的压缩链表)。c中是没有哈希表结构,所以这里的哈希表也是redis自己实现的。既然用了hash,那不可避免的就涉及到哈希冲突的问题。redis是用了拉链法来解决,即引入一个链表来盛装冲突的元素,同时当哈希表太大或太小时就要开始扩容/缩容了
首先与hashMap类似,redis的扩容所容(rehash)也是采用了扩大一倍或者缩小一倍的操作。目的是为了让容量保持为2^x,这样就可以使用按位运算来代替取模运算,即让N%length==N&(leng-1)成立
截止到上述,并没有说到redis处理亮点的地方。redis的一个优化细节,就在于为了保持高效,采用了渐进式rehash,也就是说扩容缩容不是一次性完成的,而是多次渐进完成的,因为当数据有百万量级时一次性完成扩容/缩容必定导致redis无法做别的工作,而渐进式rehash的做法是在rehash期间底层维护了两个哈希表,一个主一个备,查找删除更新都会在两个哈希表上进行,先找主,主没有再找备;新增是在备上进行的,同时扩容也是在备上进行,这样就能把扩容的压力放到备上,主就专心提供当前服务
【redis的快在于很多的实现细节,要理解他的快,就要理解这些细节上的亮点】

(3)针对sort set的跳表

跳跃表的结构是也是redis底层结构的亮点之一。对于redis的sort set要保证两点:查询快、支持排序
这里先引入一个思考:你所知道的满足这两条的数据结构有哪些?
-------------------------

1、AVL树
2、红黑树
3、B树,B+树
4、跳表
首先要理解上述的这些数据结构,如果不知道这些结构的可以先了解后再往后看

1、首先AVL树有旋转操作,其最高子树与最矮子树高度差不能超过1,它通过插入节点时旋转来使后续的查询操作的效率更高,也就是说通过新增节点时增加成本,来减少查询节点的成本。也就是说明AVL树适合查多增删的场景,那如果增删一样多呢?同时因为高度差不能超过1,随着节点数增加,不可避免的,整棵树还是会越来越高,树越高就会导致IO量越大,查询效率就越低,怎么办?

2、引入了红黑树,红黑树可以旋转来调整节点,也可以引入了变色,同时其容纳的高度差在两倍以内,也就是说最高子树不超过最矮树高度的两倍就可以了。这样就让查询性能和插入节点的性能得到了一个近似的平衡

3、但是因为红黑树是二叉的,随着数据量越来越大,不可避免的一层能装的节点始终有限,树还是会越来越高。引入了有序多叉树——B树,因为B树在每个节点中会盛装key和value,那么一个节点能装的数据就很有限,为了提高节点能盛装的数据量,又引入了B+树,非叶子节点只装key,只有叶子节点才会把所有的key和value再盛装下

到这里就简单的描述了下这些树的演变,按上面所说的,B+树是最能装的了,
那么为什么不用B+树而用跳表呢?
通过上面的描述可以知道B+树引入的目的就在于尽可能的让树矮一点,每个节点装的数据多一点,这样去查找数据时的IO量就更少,核心目的在于减少IO,那么要理解IO是什么?简单来说IO就是计算机的核心(CPU,内存)与其他设备之间数据转移的过程,比如数据从磁盘读取到内存,或者从内存写入到磁盘,都是IO操作。而redis的本质是什么,就是基于内存操作的,所以对他而言,是读写数据是没有IO的(除非持久化操作),那就没有必要使用B+树呀。
那为什么不用红黑树呢?
redis追求的核心就是查询快,更加适用于查多改少的场景。
1、红黑树的实现是需要旋转、变色等操作的,是要耗费性能的,而跳表的实现更加简单,就修改、删除数据而言效率更高,只需要维护前后节点,如果节点还没选为了上层节点,那么还需要再维护一些上层节点的前后节点,但都比红黑树更简单
2、跳表的结构决定了,其范围查询的效率比红黑树更高。跳表通过上层链表可以很快的定位到数据范围,所以针对范围查询效率很高
3、查找单个key时,跳表和红黑树的时间复杂度都是O(logN)

综上所诉,选择了跳表

【拓展】有兴趣再考虑一下以下问题,可以帮助你理解这些数据结构的选型:
1、HashMap 在jdk1.8后链表长度超过8会转换为红黑树,为什么这里用红黑树不用B+树?
2、 HashMap为什么用红黑树不用跳表?
3、mysql存储引擎的索引数据结构为什么用B+树而不用红黑树?

(4)针对list的压缩列表

压缩列表ziplist是列表键(list)和字典键(hash)的底层实现之一。是由特殊编码的内存块组成的列表。它的亮点就在于内存是连续分配的,也就是说这些列表中的元素在物理内存中都是挨着的,那么当遍历时其速度就非常快。

【这里只讲诉redis数据结构中的亮点,想要了解更加深入全面的知识,可以详读下述参考文献】

3、redis的删除策略和淘汰策略

redis提供了两种删除策略用来清空过期的key:
(1)主动删除/定期删除:每隔一段时间就扫描一定数据的设置了过期时间的key,并清除其中已过期的keys,大家也注意到了,这里说的是一定数量而不是全部,具体是多少呢?这也是redis亮点之一:基于贪心算法的清除
a、判断随机抽取的N个key是否过期
b、删除所有已经过期的key
c、如果有多于25%的key过期,重复上述两步
为什么不一下子全部删完呢?操作是要消耗性能的,数据量少的情况不会有什么影响,一旦数据量大,修改、删除、新增、查询任何一个操作都会耗费性能,也就是因为如此才产生和各种各样的分布式方案、分布式锁、中间件等等。所以当过期的key比较多时,一下子删很可能导致其他的操作不可用,整个redis就停止工作了。而redis又不知道要删的数据到底有多少,所以采用了25%的标准来判断垃圾到底多不多,多就继续删,少就把当前这点删完就好了
(2)被动删除/惰性删除:访问key时判断是否过期,过期才删除。也就是说如果这个key过期后一直没有访问它,那么他就一直在不会被删除

基于上述两个删除方案,key过期了都不是马上删除的,无论是定期删除还是访问的时候再删除,都存在了一个时间空隙,如果在这个空隙时间段,内存满了怎么办
所以引入了淘汰策略,redis提供了8种淘汰策略:
我这里总结为23+1+1
所谓2
3就是allkey,volatile和lru,random,lfu的组合总共6种,前2个表示范围,后3个表示策略
1+1就是两个单独的:
noeviction(无受害者) 内存满了返回错误,不会清除任何key
volatile-til: 从配置了过期时间的key中清除马上就要过期的键
allkey表示的就是所有键,包含过期和未过期的
volatile表示的就是过期键
lru: 最近最少使用的优先淘汰,也就是淘汰最长时间未使用的key.不了解LRU算法的建议可以单独看看
【手写一个LRU算法】
random: 随机淘汰
lfu: 最近最不常用的优先淘汰,也就是淘汰一定时间范围内使用次数最少的。因此可能刚刚用过,但是总共只用了这一次,也会被淘汰。注意和LRU的区别

那么这6种淘汰策略,就不言而喻了,组合起来理解即可

有了删除策略,为什么还要有淘汰策略?

这个问题其实上述已经解答了:两个删除方案,key过期了都不是马上删除的,无论是定期删除还是访问的时候再删除,都存在了一个时间空隙,如果在这个空隙时间段,内存满了怎么办?所以引入了淘汰策略
所以两者是互补的,也正是因为如此,很多博文也说有3种删除策略,第3种就是maxmemory策略,也就是通过配置maxmemory对应的淘汰策略,来设置内存满了时怎么删除key

# redis.conf中的配置详情
# 配置最大内存
maxmemory <bytes> 
# 配置淘汰策略
maxmemory-policy noeviction
  • 1
  • 2
  • 3
  • 4
  • 5

4、redis的持久化

因为redis本身是基于内存的,那要是服务器突然崩了,数据不是全没了吗,redis是怎么解决这个问题的呢,就是用了持久化机制
redis提供了两种持久化机制:RDB和AOF

RDB

redis的RDB简单来讲就是定时去对当前所有的key-value做一个快照,以二进制数据的形式将快照存储到rdb文件中,然后永久存储在磁盘上。因为是快照形式的,所以这个文件相对较小。那么具体的持久化流程是怎么样的呢,这里面的亮点在哪儿?

RDB提供了两种触发持久化的方式:
1、手动触发:通过指令
有两种指令:
(1)save: 这个指令执行的持久化会阻塞当前redis服务器,直到持久化完成,也就是说他的持久化实现是在当前redis线程下进行的,而redis又是单线程的,除非把它完成否则别的工作就无法进行。所以这个指令在线上系统的时候要禁用!!!发现同事用了这个指令直接送到衙门!更多是在测试或者自己研究redis的时使用。
(2)bgsave:这个指令和save有很大的区别,它会以fork的方式创建一个新的子进程来完成持久化,阻塞只会发生在通过fork创建创建子进程的这个时间段,而fork是很高效的,这也是redis持久化亮点所在,后面会详细说明。所以实际阻塞的时间很短。
2、自动触发:通过配置文件
真正使用的时候不可能手动去调用指令吧,哪怕自己写一个脚本触发也很麻烦,所以提供了自动触发的方式。配置文件中使用的是save关键字,但是实际上关联的是bgsave指令,由此也看出,官方是不推荐使用save指令的
在redis.conf中配置如下:

# save <secends> <changes>
# 在seconds秒内操作数达到changes了,就执行保存,可以配置多条,默认是开启的
# 思考一下为什么要配置多条?
save 900 1
save 300 10  
save 60 10000 
# 其他相关配置
# 是否开启压缩
rdbcompression yes  
# rdb文件名
dbfilename dump.rdb
# rdb文件路径  
dir /var/lib/redis/6379 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
rdb在持久化时为什么采用fork来创建子进程?

fork使用了copy-on-write(写时复制)机制:
创建子进程的时候并不会拷贝父进程的内存空间,而是拷贝了一个虚拟空间,这个虚拟空间指向的是父进程的物理空间。可以理解为是拷贝了一个内存空间的引用,因此拷贝速度很快,但是又可以通过这个引用访问到内存中间中的数据。
只有当父进程发生写操作修改内存数据时,子进程才会真正去复制物理内存数据,并且也只是复制被修改的内存页中的数据,不是全部的内存数据
所有fork占用的空间更小、更快
最后通过fork创建出来的子进程来进行数据的持久化操作

rdb的优点缺点

优点:
采用快照的方式存入的,类似于java的序列化,所以通过它恢复数据时很快
缺点:
1、不支持拉链:通俗讲就是只能备份一个rdb文件,不能每天都单独生成一个,这样就没办法进行版本重制
2、不实时:因为是定期备份的,所有会存在数据缺失,因此也就引入了AOF实时备份方式

AOF

直接实时将写操作记录到aof文件中,备份的就是写操作,因此aof文件中记录的实际上是写操作,而不是二进制数据,因此占用空间大,用其恢复数据时实际就是将这些指令重新执行一遍,所以速度很慢

能不能把RDB和AOF的优点集合一下开发出一个新的持久化方案呢?
答案是可以!但并不是新的,redis4.0版本后AOF做了优化,这也是redis持久化亮点之一,那就是AOF中包含了RDB全量数据,再加上AOF在RDB备份时间后的实时操作(增量)记录

这样就及利用了RDB的快,又保证了实时性

开启AOF

# redis.conf
# 文件名,可以看到这里有*,也就是说aof支持
appendfilename *appendonly.aof
# appendfsync always
appendfsync everysec
# appendfsync no
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

redis是内存数据库,写操作持久化会触发IO,所以有三个级别可以调:
no:缓冲区什么时候满了什么时候向磁盘写入数据
always:每次操作都会将缓冲区的数据向磁盘写入
everysec:每秒,默认项,每秒将缓冲区的数据向磁盘写入

redis为什么这么快

1、基于内存

redis的数据都是存储在内存中的,所有读写速度很快

2、单线程,不用维护锁机制

redis采用单线程机制,指令串行,不用维护额外的锁机制,资源竞争等

3、数据结构简单,操作简单

自己内部实现了各种数据结构,根据情况进行了优化
(1)动态字符串结构
(2)hash的字典结构的容量大小取2^N,使取模运算可以转换为按位运算,更快。同时扩容缩容采用采用渐进式rehash
(3)zset采用跳表结构,范围查询更快
(4)list采用压缩链表结构,内存空间连续

4、采用了epoll多路复用器

能够接收大量socket连接,并且监控,能将有效socket传给内核执行后续读写操作

redis的VM机制

之前有看到不少博客都将redis的VM机制纳入redis快的原因之一,VM的操作是内存满了将冷数据保存到磁盘,之前了解到redis的淘汰策略是内存满了将冷数据删除,于是不禁产生这两个不是矛盾吗的疑问,进一步查询资料后发现VM机制在redis2.4的时候就已经弃用了,在redis2.6的时候删除了。使用VM反而存在性能问题,可能导致redis卡死,所以被弃用了。因此个人觉得VM机制不应该再作为redis快的原因

VM机制为什么被弃用?

1、重启太慢
2、保存数据太慢
3、代码过于复杂

redis单线程如何处理高并发

redis要承接住高并发请求并且不出错就要保证以下几点:
1、部署集群:通过负载均衡服务器转发请求。
(1)缓存的数据量不大的情况下,每台redis都缓存全量数据,将读请求分发到不同的redis上,减少单机压力
(2)要缓存的数据量比较大的情况下,每个redis负责缓存一部分数据,对于不同数据的访问请求分发到不同的redis上。redis中实现这点的技术是用hash槽来完成的。有兴趣可自查
【这里要区分哈希槽和一致性哈希的区别】

数据分治时会有一个问题:聚合操作很难实现,比如一个操作需要几个key,但这几个Key不在一个redis上
redis的思想是计算向数据靠拢,优先考虑查询的快,然后考虑这些计算的实现

数据一旦被分开就很难整合使用,反过来数据放在一起就可以实现复杂的计算整合,hash tag就是用来实现将不同的key放在一起的。
比如你要存的key有相同的前缀,则用这个相同的前缀来作为hash,这样不同的key都会有相同的hash值,就会存在一个redis中
eg: set {tag}k1 value1
set {tag}k2 value2

2、防止单点故障:部署主从,主从同步,哨兵选举新master

3、读写分离:slave只负责读,但是主从同步数据是有时延的,所以如果业务本身要求强一致性和实效性则不能采取这个方案

redis实现分布式锁

首先说明目前实现分布式锁最好的方案是用zookeeper,而不是redis,如下描述,只是为了说明使用redis如何实现分布式锁,可以通过其加深对setnx已经锁实现的理解
对于锁的实现无非在于3个核心:加锁、获取锁、解锁。所以实现分布式锁在就是如何在并发场景下实现这3个方法

setnx实现分布式锁

定义一个key作为锁,将其设置value就认为是去获取锁,使用完后通过del指令删除key表示释放锁。value取值要全局唯一,可以用uuid,这是为了防止以下情况:线程A加锁,因为某些原因线程A一直没有释放锁,锁过期自动删除后,线程B来加锁,如果这时线程A又来删除这个锁,这就有问题了,删除时比较两个value值是否相同,相同才允许删除

尝试获取锁,设置成功就表示加锁成功,失败就表示已经有操作占用了这个锁了,稍后再重试吧。用setnx可以很好的控制分布式占用,因为当它存在时是设置不进去的,会返回null
另外要注意,这里设置了过期时间,是为了防止当前占用锁的线程因为报错或者崩溃导致一直不释锁, 其他线程就获取不到锁的情况的发生,因此设定一个过期时间,时间一到自动释放,所以针对这个时间的设定要看具体的业务耗时
set lock 11111 NX PX 1000 (这里时间最好使用PX,即毫秒)
释放锁(伪代码)
if (get lock == ‘11111’)
del lock
在这里插入图片描述

Redisson类实现分布式锁

直接提供了getLock(key),tryLock,unLock等api

// 设置ip,redis密码
Config config = new Config();
config.useSingleServer().setAddress("redis://172.16.188.2:6379").setPassword("123456").setDatabase(0);
// 构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
//  定一个锁名称,这样不同的资源的不同锁就可以区分开来
RLock lock = redissonClient.getLock("lock");
boolean isLock;
try {
    //尝试获取分布式锁
    isLock = lock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
    if (isLock) {
        //TODO if get lock success, do something;
        Thread.sleep(15000);
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    disLock.unlock();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

RedLock实现分布式锁

mysql,redis如何保证数据一致性/双写一致性

对于双写一致性问题,主要矛盾就在于:
(1)更新完数据库是更新缓存还是删除缓存
(2)就删除缓存而言,是先删缓存还是先更新数据库

解决方案:
缓存设置过期时间【方案1】:
缓存设置过期时间是保持最终一致性的方案,但并不能保证强一致性。缓存过期后,再次获取缓存时会走数据库,获取到后再更新缓存
接下来讨论下述三种情况
(1)先更新数据库,再更新缓存
这种做法实际上是不可取的,因为不能保证线程安全。比如现在线程A更新了数据库,还没更新缓存时,线程B更新了同一数据,并且更新了缓存,之后线程A又把缓存更新为A的了,从时效上来看,A先来B后来,最后要保留的应该是B的更新,这就出现了问题。

其次从业务角度出发,如果业务场景是写操作更多的,就会导致数据还没读到缓存就被更新了,频繁的更新导致性能浪费。

综上而言,删除缓存更加合适。每次获取缓存的时候如果没有,就走数据库,然后再将获取到的数据库的值更新到缓存中

(2)先删除缓存,再更新数据库

会导致脏读:如果线程A进行写操作,删除缓存,还没更新数据库时,线程B来查询了这条数据,因为缓存被删除,就去查数据库,得到了旧值并且更新到缓存中,之后线程A又把新值写入数据库,之后来查这个数据的线程拿到的都是旧数据,也就产生了脏读,要如何解决这个问题呢?

因为线程B查询到旧数据又把旧数据更新到缓存中了,而线程A一开始就把缓存删除了,之后如果线程A还能把线程B设置的旧数据缓存给删掉,脏读问题就解决了

所以引入了延时双删策略【方案2】,即更新数据时:先删除缓存、更新数据库、延时(让线程B有足够的时候把旧数据放到缓存里,确保后续二次删除时删除的是在读操作产生的脏数据,也就是说这个延时的时间应该是读操作的耗时+几百ms)、再删除缓存

当然延时本身会导致时间消耗,降低吞吐量,因此可以新开一个线程来异步延时删除,也就是延时异步双删策略【方案3】

但是就这样仍然会存在问题,就是如果第二次删除失败怎么办?

解决办法就是建立重试机制:

一是可以把删除失败的key放到消息队列中,单写一个接收消息的方法来不断尝试删除key直到成功【方案4】

二是通过订阅数据库的binlog,获得需要操作的数据,在应用程序中单写方法来获得订阅程序传来的消息来删除缓存,订阅binlog可以使用mysql自带的canal来实现【方案5】

(3)先更新数据库,再删除缓存【方案6】

国外一些公司像facebook提出的就是先更新数据库再删除的策略,具体是
从缓存拿数据,没有得到,就走数据库,拿到后再更新到缓存一份
从缓存拿数据,有就直接返回
把数据更新到数据库,然后删除缓存

针对这种做法,出现线程不安全的情形其实就只有一个:
线程A查询数据,发现缓存中没有,就从数据库拿,还没来得及将拿到的值更新一份到缓存时
线程B来更新数据,先将值更新到数据库,然后删除缓存,等线程B删除缓存后,线程A才将旧值更新到缓存上,这样后续线程就产生了脏读

但是针对这一问题的概率需要做讨论:要让线程B在线程A更新缓存前把缓存删除,就得要求线程B写入数据库的操作比线程A查询数据库的速度更快,这样就能先发起线程B删除缓存,但是实际上读操作是比写操作要快得多的,所以这类问题发生的概率很小很小,但是如果一定要考究的话,那么就参考上述的延时异步双删策略最后延时再删除一遍缓存,将读操作产生的旧数据缓存删掉

redis如何解决缓存穿透、击穿、雪崩

首先要理解这3个问题的概念,比较容易记混的是穿透和击穿,个人认为这是个名字而已,更重要的是概念。当然分享一下个人记忆的一个小技巧:透,透明,表示本来就没有的东西,所以缓存穿透指数据库本来就没有这些key,但是又有大量请求打来导致直接访问到数据库上了。

缓存穿透:本来就没有的key

因为数据库中本来就没有这些key,但又有大量的这些key请求访问过来,导致穿过了redis打到数据库上了

解决: 布隆过滤器(过滤器还有其他几种,之间的差异可以自行了解)

缓存击穿:少量key被大量请求

某些key过期或者被清除掉后,针对这些key的大量请求打来过来,直接穿透了redis到数据库上了

解决:第一次访问后发现没有缓存,就访问数据库并设置缓存,这样下一次访问的时候就能访问缓存了

缓存雪崩:大量key被大量请求

大量的key同时过期或被清理,间接造成大量的访问到达数据库

解决:key设置过期时间随机,尽量错开过期时间

如果某些业务要求0点清除:
1、可以设置延迟随机时间执行
2、缓存击穿方案做备案

参考文献

【redis的VM机制/冷热数据分离】https://blog.csdn.net/bieleyang/article/details/77252623
【redis的VM机制】https://www.codenong.com/cs106843764/
【redis或将弃用VM】http://www.voidcn.com/article/p-xrljslgf-xa.html
【单线程的redis为什么快】https://zhuanlan.zhihu.com/p/57089960
【redis为什么快】https://mp.weixin.qq.com/s/PobjHLx5b7j1CVhxinKOxw
【redis底层数据结构】https://www.cnblogs.com/ysocean/p/9080942.html
【redis,mysql双写一致性】https://www.cnblogs.com/liuqingzheng/p/11080680.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/902374
推荐阅读
相关标签
  

闽ICP备14008679号