redis
https://www.processon.com/mindmap/60827d6ce401fd45d70648b3
1.Redis缓存穿透,缓存击穿,缓存雪崩
1.1 Redis缓存穿透,缓存击穿,缓存雪崩是什么 和对应解决方法
问题 | 解释 | 解决方法 |
---|---|---|
缓存穿透 | key对应的数据在缓存和数据库不存在,黑客一直用这个信息请求,可能压垮数据源 | 1、布隆过滤器 2、针对这个key设置值为null的数据存入缓存中 |
缓存击穿 | 某个key失效,此时出现大量并发请求(热点数据) | 使⽤互斥锁(mutex key) :SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置 |
缓存雪崩 | 大量key在某一集中时间段缓存失效或者缓存服务器重启 | 1、在原有的失效时间基础上增加⼀个随机值,比如1-5分钟随机(加锁排队解决分布式环境高并发问题但是用户体验很差)2、redis故障:搭建集群主从 3.加hystrix限流 4.加多级缓存Ehcache |
-
缓存穿透:布隆过滤器(删除困难,因为有hash冲突)https://blog.csdn.net/qq_41125219/article/details/119982158
布隆过滤器算法:
用来判断一个元素是否在一个集合中:该算法由一个二进制数组和hash算法组成
使用bitmap数据结构,一个二进制数组,存在是1,不存在是0,因为不同元素经过hash算法取余后在数组下标一样,产生hash冲突。
产生误判问题:
通过hash计算在数组上不一定在集合(hash冲突)【增大数组,或者经过两次hash计算】
通过hash计算不在数组上的数据一定不在集合 -
缓存击穿互斥锁代码
// 伪代码
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防⽌del操作失败的时候,下次缓存过期⼀直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓
存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
1.2 如何使用redis设计分布式锁
使用setnx
如何避免死锁:给这个key设置一个过期时间(ttl)
锁过期时间不好评估:解决方法,分布式锁加入看门狗,通过开启守护线程定期扫描这边锁,如果锁快要过期,而资源还没有释放,就给锁续期
1.3 如何使用redis实现消息队列
- 基于List:消费者难以做ack确认
- 基于Zset :实现延迟队列,消费者没有办法阻塞,只能轮询,不允许重复消息
- PUB/SUB 订阅/发布:会导致消息丢失,只可做即时通讯
- 基于Stream(一般使用这种方式实现消息队列)
1.4 Redis并发竞争key的解决方案(缓存击穿)
- 1. 分布式锁
- 基于zookeeper,redis,数据库 实现
分布式锁的特点 :- 互斥性:只有一个客户端持有锁
- 无死锁:持有锁的客户端崩溃或发生意外,其他客户端仍然可以获取锁
- 容错:大部分redis节点存活就可以获取锁和释放锁
- 基于zookeeper,redis,数据库 实现
- 2.使用队列
- 让操作命令放在队列里,串行化执行。
1.5 Redis如何解决key的冲突
1.业务隔离
2.key的设计:业务模块+系统名称+关键信息
3.分布式锁:客户端拿到锁才可以进行修改等操作,以及利用时间戳确定修改执行顺序
1.6 如何提高缓存命中率
1.数据提前加载(优先将数据提前加载到redis)
2.增加缓存存储空间,提高缓存数据,从而提高缓存命中率
3.调整缓存的存储类型
4.提升缓存的更新频次(mysql binglog发生变化,就推送至redis修改,或者使用队列,客户端来查询队列数据更新redis)
2 、redis持久化
2.1 Redis RDB持久化、AOF持久化(缓存满了和数据丢失等问题)
- 1.为什么要持久化(redis持久化不保证数据的完整性)
redis是内存数据,宕机后会消失,持久化的目的不是为了存储,而是为了数据快速恢复 - 2.redis持久化的两种方式
方式 | RDB | AOF |
---|---|---|
介绍 | 在指定时间间隔对数据进行快照存储 | 记录每次对服务器写的操作,以redis协议追加每次写操作到文件末尾 |
不同 | 1.只记录结果 2.默认开启 3.性能高 | 1.会记录过程 2.默认不开启 3.性能低 |
文件格式 | dump.rdb(二进制压缩文件) | 存储的rdis命令 |
优点 | 1.RDB是二进制压缩文件,占用空间小,便于传输 2.主进程fork子进程,主进程阻塞,主进程fork子进程后继续工作。子进程创建RDB文件 3.相对于数据集大时,比AOF的启动效率更高 | 数据安全 |
缺点 | 不保证数据的完整性,最后一次快照后的更改数据丢失(用AOF辅助) | 1.AOF文件比RDB文件大,且恢复速度慢 2.数据集大的时候,比RDB启动效率低 3.运行效率没有RDB高 |
- 如果服务器开启了AOF功能,会优先使用AOF方式还原数据,只有关闭了AOF持久化功能,才会使用RDB方式还原数据
- 高并发场景下:一般使用RDB,AOF混合方式来进行持久化
3、Redis快的原因
1. Redis为什么是单线程、及高并发快的原因
- redis是基于内存的,内存的读写速度非常快(纯内存访问)
- redis是单线程的,减少了上下文的切换 (redis 单线程指的是网络请求模块使用了一个线程,即一个线程处理所有网络请求,其他模块仍用了多个线程。)单线程下不需要考虑锁的性能消耗,缺点:无法发挥多核cpu性能,不过可以单机开多个redis实例来完善。
- redis采用IO多路复用技术,可以处理多个连接
- 数据结构,全程采用hash结构,特殊的数据结构对数据存储进行优化,采用压缩表对数据进行压缩存储,采用跳表,有序的数据结构对数据进行快速的读取
- 渐进式rehash:把一次性大量拷贝的开销,分摊到了多次处理请求的过程中
Redis默认使用了两个全局hash表,一开始插入数据的时候默认使用hash表1,hash表2没有分配空间,直到数据越来越多,redis开始执行 rehash。-
- 给hash表2分配更大的空间,如是hash1大小的两倍
-
- 把hash1的值重新映射到hash2
-
- 释放hash1的空间
第2步涉及到大量数据的拷贝,如果一次性把数据迁移完,会造成Redis线程阻塞,无法服务其他请求,此时,redis就无法快速访问数据了。因此,每次请求就会处理一次数组下标位置的一个元素或者一个链表迁移。查询时会在表1查不到时查询表2
- 释放hash1的空间
-
- 缓存时间戳:redis使用系统时间戳,不通过系统调用System.currentTimeInMills ,因为一次调用就会造成上下文切换,因此redis是通过缓存时间戳,每毫秒更新一次时间缓存,获取时间都是从缓存里取值
注:复用指的是多路复用一个线程。多路指的是多个网络连接。IO多路可以监听多个套接字(socket连接)
1. 1 Redis使用多路复用模型epoll模型
epoll模型:epoll是一种事件通知模型,当发生了IO事件时,应用程序才进行I/O操作。
【运用:tomcat,nginx,nio,redis】
工作流程:
分配一个内存块,创建一个事件收集器selector,所有的事件都要注册到这里,例如redis启动时就要往selector注册一个accept事件,当有客户端要连接redis,首先由selector感知到有accept事件发生,通知redis来连接,建立好连接后,客户端还会发送数据给redis,一旦建立连接后,客户端会将IO事件也注册到selector中,当多个客户端发数据触发IO事件,selectors会返回一个事件的数组集合events,redis拿到数据后进行处理。如果连接之后没有发数据,selector会处于一个阻塞状态等待事件发生,如果有事件发生马上返回一个events数组
操作系统内核提供函数:
epoll-create 创建收集器分配空间
epoll-wait 阻塞
epoll-ctl 把事件注册进来,由epoll-wait 监听
2. Redis是否存在线程安全问题
- redis服务端层面:redis服务端是一个线程安全的key-value的数据库,在redis的客户端执行指令的时候不需要任何同步机制,不会存在任何线程安全的问题,虽然redis6.0里面增加了多线程的模型,但这只是去处理网络的IO事件,对于指令的执行仍然是通过主线程处理的,所以并不会出现多个线程同时执行操作指令的情况。
为什么redis不采用多线程执行指令,因为redis server 本身可能出现的性能瓶颈点无非就是网络IO,CPU,内存,而cpu不是redis的瓶颈点,所以没有必要采用多线程执行指令,另外如果采用多线程,对redis的所有指令操作就需要考虑线程安全加锁,这样性能影响更大 - redis客户端层面:如果有多个redis客户端同时执行多个指令,多线程下共享资源访问竞争的问题使得数据安全性无法得到保障。因此对于客户端层面的线程安全问题,应该尽量使用Redis里的原子指令,对客户端的资源访问加锁,通过lua脚本来实现多个指令的操作。
3. Redis6.0之前为什么不使用多线程
1、使用Redis,CPU不是瓶颈,受限于内存、网络
2、提高redis性能可以使用pipeline(命令批量)每秒处理100万个请求
3、单线程、内部维护比较低(并发读写,增加系统复杂度)
4、如果是多线程(线程切换,加锁解锁,导致死锁问题)
5、渐进式rehash,阻塞很少
一般公司够用
4. Redis6.0之后为什么引入多线程
1、单线程够用。 数据-》内存 响应时间 100纳秒
比较小的数据包,8w~10w QPS(极限值)
2、大公司,需要更大QPS,解决方式就是使用多线程,IO多线程,内部执行命令依旧是单线程
3、为什么不采用分布式架构(分区)
很大的缺点:
- 服务器数量多,维护成本高
- redis命令不适用于数据分区
- 数据分区,无法解决热点读/写问题
- 数据倾斜,分区重新分配,扩容缩容更加复杂
可以把多线程任务分摊到redis的同步IO中,进行读写负载,减少I/O时间,充分利用CPU资源
5. Redis是多线程还是单线程
redis5.0:单线程:worker线程是单线程
多个客户端连接redis后发送指令到redis,由于redis是单worker线程,因此采用串行方式执行这些命令,执行指令的的前提是要将IO流(因为是TCP协议所有是流的方式)读到,然后将指令进行计算(把数据插到内存中),计算完之后把计算的结果往回写(write IO)插入成功或失败把计算结果告诉客户端
redis6.0:多线程:worker线程还是单线程
多个客户端连接redis后发送指令到redis,由于redis是单worker线程,因此采用串行方式执行这些命令,执行指令的的前提是要将IO流(因为是TCP协议所有是流的方式)读到,之前的读IO是由worker线程读的,这里当一个客户端有指令发送过来时会开辟一个IO子线程。这时就是多线程了,IO子线程专门用来readIO,然后worker线程将指令进行计算(把数据插到内存中),计算完之后由IO子线程把计算的结果往回写(write IO)插入成功或失败把计算结果告诉客户端。
此时又有第二个客户端发送指令,便会开启第二个IO子线程,步骤同上
从执行时间维度上看,单线程只需要6个时间维度,多线程只需要4个时间维度,执行时间更短,性能更高
4、Redis数据一致性
1. Redis缓存和MySQL数据一致性的解决方案
先更新缓存,后更新数据库:数据库异常,缓存的数据就和数据库不一致(不考虑)
先更新数据库,后更新缓存:缓存的数据就和数据库不一致(不考虑)
先删缓存,后更新数据库:问题出现在同时出现两个请求,A修改还未提交事务,B查询缓存为null继而查询数据库旧值更新到了缓存中(解决方法:延时双删)
先更新数据库,再删除缓存
- 1. 采用延时双删策略
- 步骤:
1、删除缓存
2、写数据库
3、休眠500ms(Thread.sleep(N))
4、再删除缓存 - 设置缓存超时时间
- 弊端:最差的情况下超时时间内数据不一致
- 步骤:
- 2.异步更新缓存(基于订阅binlog的同步机制)
订阅,消息队列推送----》没看懂
5、Redis集群
1. Redis哨兵、复制、集群的设计原理,以及区别
1. 保证redis的高可用
- 1.哨兵 :提供了监控
- 故障监控和转移。主从节点,采用Raft算法选举
- 2.复制
2. Redis 主从复制、哨兵和集群这三个有什么区别
1.主从复制:读写分离,一个master可以有多个slave
2.哨兵:监听,自动转移,哨兵发现主服务器挂了之后会从slave种选出一个新的主服务器
3.集群: 为了解决单机redis容量有限的问题,将数据按一定规则分配给多台集群
2. Redis集群怎么做(没记住)
redis cluster:根据一致性hash算法(变种hash算法)进行虚拟分槽,让数据进行均匀分布。集群功能限制:
- key批量操作支持有限:只支持相同槽的key的批量操作
- key事务支持有限
- key作为数据分区的最小粒度
- 复制结构只支持一层,主-从,不支持树状
集群搭建:建议三主三从
6、Redis应用场景
1.redis数据结构
1. 1 Redis有哪些数据结构,分别有哪些应用场景
- 字符串:缓存简单字符串,JSON格式的字符串(应用于redis分布式锁)
应用场景:计数器,session共享,分布式ID。 - 哈希表:可以用来存储key-value,更适合用来存储对象。
- 列表:Redis的列表通过命令的组合,可以当作栈,也可以当作消息队列来使用,可以用来缓存微信公众号、微博等消息流数据。
- 集合:和列表类似,可以存储多个元素,但是不能重复,集合可以进行交集,并集,差集操作,从而实现共同关注,朋友圈点赞功能,给不同标签的用户做不同类型的兴趣爱好推荐。
- 有序集合:集合是无序的,有序集合可以设置顺序,实现排行榜功能。
- 高级数据结构bitmap:布隆过滤器
1.2 redis底层数据结构zset :压缩表或者跳表存储
redis底层数据结构zset 有序集合,使用压缩表或者跳表存储。
(有序集合保存的元素小于128个,或者有序列表存到所有元素的长度小于64个字节,使用压缩表,否则使用跳表)
为什么不使用红黑树:
跳表支持范围查找,效率高于红黑树
跳表实现比红黑树简单易懂
-
压缩表 :本质上是一个数组
压缩表介绍
增加了列表长度,尾部偏移量,列表元素个数,列表的结束标志,有利于快速寻找列表首和尾的节点,但是对于寻找其他正常的元素,仍然没有很高效,只能一个个便利
-
跳表 :为什么使用跳表,由于zset底层使用链表实现,因此增删快,查询则需要从表头到表尾比较慢,因此对链表进行了优化。因此为该链表建立索引层,每两个元素的第一个元素往上建立一个冗余的元素。因此查询可以从左上角索引查询,如图所示(类似于折半查找)。
跳表:将有序链表改造为支持近似“折半查找算法”,可以进行快速的插入,删除,查找操作。
2. Redis有哪些高级功能
- redis+lua脚本实现限流(支持分布式项目)
- RDB+AOP结合的持久化功能
- 分布式锁
- 主从复制,高可用的话引入哨兵
2.1 Redis+lua脚本实现限流
预防库存超卖,Redis+lua是如何实现原子性的
decr命令对库存计数器-1
lpush 往list列表左侧增加userid
多线程并发下,goods:1001(商品编号,库存10)>0,但是20个线程同时开始,都对库存进行减少,并将自己用户编号压入列表,导致超卖
redis 2.6 集成lua脚本语言
执行原理:redis单线程命令队列,保证redis+lua的原子性
支持多行模式(lua代码)根据1或者0判断是否抢购成功
3. 为什么要使用Redis
从高性能和高并发两个维度切入
1.高性能:访问mysql数据库(访问磁盘效率低,1k~2k/s),为了提高性能加redis缓存(微妙)
2.高并发:mysql(1k/s),redis (10w/s)集群下就可以更多,还可以进行故障转移
4. 什么是bigKey
key对应的value所占的空间比较大,value最大存储512M
字符串类型:体现在单个value值过大(10kb,会随业务设定波动)
非字符串类型:哈希、列表、集合、有序集合,体现在元素个数过多
bigkey的危害:
- 内存空间不均匀,造成有的节点使用内存很大,有的很小
- 超时阻塞:由于redis处理命令单线程的处理,操作bigkey比较耗时
- 网络拥塞:每次获取bigkey产生的网络流量较大
如果是热点数据的话,就会造成很大的影响
解决:对bigkey进行结构拆分
7、Redis和memcached
1. Redis和memcached比较
Redis | memcached | |
---|---|---|
整体类型 | 1、支持内存(key-value) 2、非关系型数据库 | 1、支持内存 2、key-value (简单存储) |
数据类型 | String 、List、Set、Zset、Hash | 1、文本类型 2、二进制类型 |
操作类型 | 1、单个操作 2、批量操作 3、事务支持(弱势务,可以结合lua脚本做出事务比较完整的方案) 4、每个类型不同的curd | curd、比较少的其他命令 |
附加功能 | 1、发布和订阅 2、主从高可用(哨兵:故障转移,集群)3、序列化支持 4、支持lua脚本 | 多线程服务支持 |
网络IO模型 | 执行命令-单线程(有子任务线程进行AOF,有锁冲突) 、网络操作-多线程(多路复用IO) | 支持多线程、非阻塞的IO模式 (多路复用) |
持久化 | 支持RDB,AOF | 不支持 |
redis功能比较强大,memacahe 多线程比较优势
8、Redis事务
要么全都执行,要么全不执行
将一组命令放到multi和exec两个命令之间,multi命令代表事务开始,exec代表事务结束,discard代表事务回滚
redis的事务功能很弱,在事务回滚机制上,对基本的语法错误进行判断,但是运行时错误不会回滚
9、Redis过期策略以及内存淘汰机制
9.1 Redis过期策略
内存有限,因此数据结构会设置过期时间(ttl)(加入过期字典)时间一到,会自动删除过期键,但是redis是单线程的,大量key过期进行删除会发生卡顿。
因此redis过期策略有两种:1.定期删除 2.惰性删除(相结合)
-
定期删除:定期扫描策略
默认每秒进行10次扫描,不会遍历过期字典中所有的key,而是使用一种简单的贪心算法
1.随机从过期字典中随机20个key
2.删除20个key中过期的key
3.如果过期key的比例超过1/4,就会重复第一步如果一个redis实例中大量key同时过期,就会持续扫描过期字典(循环多次),知道字典中过期的key变得稀疏,才会停止,这就导致线上读写请求出现明显卡顿现象。
从库的过期策略:从库不会进行过期扫描,而是接受主库的命令(在AOF增加del指令同步给从库执行)因为指令同步是异步进行的,所以主库过期的key的del指令没有及时同步给从库的话会出现主从数据不一样。
-
惰性删除
客户端访问key时检测过期时间,如果过期就立即删除,不会返回任何东西。定期删除导致很多key到了时间没有被删掉,所以有了惰性删除。
总结:定期删除是集中处理,惰性删除是零散处理
9.2 内存淘汰机制
- Noevition:不进行处理,当redis内存超过物理内存限制,默认可读但是不能写
- VOLATILE-LRU :最少使用的key被优先淘汰,没有设置过期时间的key不会被淘汰,这样需要持久化的数据不会突然丢失
- VOLATILE-TTL :key剩余寿命TTL的值越小优先被淘汰
- VOLATILE-Random :过期key集合中随机的key
- ALLKEYS-LRU :与VOLATILE区别在于,没有设置过期时间key也会被淘汰
- ALLKEYS-Random
9.3 LRU算法
实现LRU算法除了需要key-value字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列,当空间满的时候,会踢掉链表尾部的元素,当一个元素被访问时,这个元素就被被移动到链表头
redis中采用近似LRU算法,给每个key增加一个长度是24bit的字段,也就是最后异常被访问的时间戳,随机抽样五个key,淘汰掉最旧的,直至内存低于maxmemory,停止随机抽样淘汰
10.redis性能
10.1 redis性能问题和解决方案
1.主从复制流畅,在同一个局域网
2.主从复制尽量不要网状结构
3.尽量避免主库压力很大,增加从库
4.主从,主不要做持久化,从做持久化
5.从库做AOF持久化,策略每秒同步一次
10.2 redis什么情况下会发生阻塞
1.AOF日志同步时,大量的写操作(1个同步写磁盘耗时1-2ms)
2.从库加载RDB文件,文件越大,阻塞越久
3.删除BigKey(zset 100万个元素2s)
4.主从节点切换,清空库 flushdb flushall