01、Redis工作原理

redis为什么这么快?redis单线程如何处理高并发?

引言

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

redis工作原理

要聊redis的工作原理,那就要从工作流程出发,逐步分析:
1、 首先redis采用了epollI/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[]; // 存储的实际内容
}

具体怎么优化的?针对上述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

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 
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

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
另外要注意,这里设置了过期时间,是为了防止当前占用锁的线程因为报错或者崩溃导致一直不释锁, 其他线程就获取不到锁的情况的发生,因此设定一个过期时间,时间一到自动释放,所以针对这个时间的设定要看具体的业务耗时
setlock 11111 NX PX 1000 (这里时间最好使用PX,即毫秒)
释放锁(伪代码)
if(get lock == ‘11111’)
dellock

&nbsp;

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();
}

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