文章目录
参考文献:
1、参考菜鸟教程:redis
2、参考博客:Redis 的雪崩和穿透?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?
O、存储结构
参考文献:redis为什么用跳表存储,跳表的原理
0.1 跳表结构
- redis怎么存储有序数据:跳表结构,主要是双向链表 + 多层级索引的方式实现,以空间换时间的形式,本身的主要思想是二分法,提高查找速度;
- 为什么不用b+树?b+树的索引维护较为麻烦,不利于增删操作;
- 跳表目的:解决平衡二叉树复杂问题,若用二叉查找树,有序数据会退化为链表,加平衡因子,变成平衡二叉树(可分为b树、b+树、红黑树等);
- 时间复杂度:当数据量比较大的时候,跳表查询、插入、删除的复杂度均为为 O(logn);
- 空间复杂度: O(n) = n/2 + n/4 +n/8 + n/16 + …+8+4+2 = n - 2。
一、数据类型
- Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
1.1 String
- String 是 redis 最基本的类型,结构为一个key对应一个value。
- String 类型是二进制安全的。意思是redis的String 可以包含任何数据,比如jpg图片或者序列化的对象。
- String 类型是 Redis 最基本的数据类型,String 类型的值最大能存储 512MB。
1.2 Hash
hash 是一个 String 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
1.3 List
List 是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。列表最多可存储 232 - 1 元素 (4294967295, 每个列表可存储40多亿)。
1.4 Set
Set是String类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
sadd 命令,添加一个 string 元素到 key 对应的 set 集合中,
- 成功则返回1,
- 元素已存在则返回 0,
- key不存在则返回错误。
1.5 zset(sorted set:有序集合)
zset 和 set:
- 相同点:都是string类型元素的集合,且不允许重复的成员。
- 不同点:zset每个元素都会关联一个double类型的分数,redis通过分数将zset成员按从小到大排序。
zset的成员是唯一的,但分数(score)却可以重复。
zadd 命令:添加元素到集合,元素在集合中存在则更新对应score。
1.6 HyperLogLog
- Redis 在 2.8.9 版本添加了 HyperLogLog 结构,用来做基数统计。
- HyperLogLog 的优点:在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
- HyperLogLog 的缺点:只会根据输入元素来计算基数,而不会储存输入元素本身。 所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
二、Redis的适用场景
缓存:减少压力,增加性能
排行榜:通过Sortset数据格式实现
计数器:原子性的自增操作(点赞访问量等)
集合关系:交并补集合的关系,共同兴趣点等
消息队列:自身的发布订阅模式
session共享:通过保存服务器文件,集群服务中,哪台服务器登录都可以获取信息
三、Redis 数据备份与数据持久化
3.1 主动备份
-
SAVE 命令用于创建当前数据库的备份,命令基本语法如下:
redis 127.0.0.1:6379> SAVE
-
Bgsave:创建 redis 备份文件也可以使用命令 BGSAVE,该命令在后台执行。
-
恢复数据,如果需要恢复数据,只需将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务。获取 redis 目录可以使用如下命令:
redis 127.0.0.1:6379> CONFIG GET dir 1) "dir" 2) "/usr/local/redis/bin"
3.2 被动备份——RDB、AOF
问题:Redis 将自己的数据储存在内存里,如果不将储存在内存中的数据保存到磁盘里,一旦服务器进程退出,服务器的数据也会丢失。为了解该问题,Redis 提供了两种持久化方式—— RDB(Redis DataBase) 和 AOF(Append Only File) ,这可以将 Redis 在内存中的数据库状态保存到磁盘里。
-
RDB快照,在每隔一段时间就将内存中数据集快照写入磁盘 写入一个临时文件,持久化结束后,用这个临时文件替换上持久化的文件。缺点:数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。
-
AOF (Append Only File) ,以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件不可以改写文件。
AOF默认是关闭的,通过将 redis.conf 中将 appendonly no修改为 appendonly yes 来开启AOF 持久化功能。如果服务器开始了 AOF 持久化功能,服务器会优先使用 AOF 文件来还原数据库状态。AOF 持久化功能处于关闭状态时,服务器会使用 RDB 文件还原数据库状态。如图:
四、Redis主从同步
- 读写分配:主机负责写,从机负责读
- 从机主动请求同步:Slave启动成功连接到master后,从机slave会发送一个sync命令。
- Master接到命令,启动后台的存盘进程,同时收集所有接收到的修改数据集命令,在后台进程执行完毕之后,master将传送整个数据文件到slave,以完成一次完全同步。
- 断开之后重新连接,只要是重新连接master,将被自动执行一次全量复制,RDB数据就会给从机。
- 全量复制:而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中。(刚开始从机连接主机,主机一次给)
- 增量复制:Master继续将新的所有收集到的修改命令依次传给slave,完成同步 (主机修改了数据会给予从机修改的数据同步,叫做增量复制)
五、Redis集群
参见:Redis集群
Redis中,实现高可用的技术主要包括:持久化、主从复制、哨兵和集群。
5.1 主从复制
- 主从复制是高可用Redis的基础,哨兵和集群都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复。
- 缺陷:故障恢复无法自动化:写操作无法负载均衡;存储能力受到单机限制。
- 流程:
(1)若启动一个Slave机器进程,则它会向Master机器发送一个"sync_ command"命令,请求同步连接。
(2)无论是第一次连接还是重新连接,Master机器都会启动一个后台进程,将数据快照(RDB)保存到数据文件中(执行rdb操作),同时Master还会记录修改数据的所有命令并缓存在数据文件中。
(3)后台进程完成缓存操作之后,Master机 器就会向Slave机器发送数据文件,Slave端 机器将数据文件保存到硬盘上,然后将其加载到内存中,接着Master机器就会将修改数据的所有操作一并发送给Slave端机器。若Slave出现故障导致宕机,则恢复正常后会自动重新连接。
(4)Master机器收到slave端机器的连接后,将其完整的数据文件发送给Slave端机几器,如果Mater同时收到多个slave发来的同步请求则Master会在后台启动一个进程以保存数据文件,然后将其发送给所有的Slave端机器,确保所有的Slave端机器都正常。
5.2 哨兵
在主从复制的基础上,哨兵实现了自动化的故障恢复,所以,哨兵主要是解决了主从复制出现故障时需要人为干预的问题。
缺陷:写操作无法负载均衡:存储能力受到单机的限制。
5.3 集群(cluster实现了分布式)
- 通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。
- Redis集群采用的算法是哈希槽分区算法。Redis集群中有16384个哈希槽(槽的范围是 0 -16383,哈希槽),将不同的哈希槽分布在不同的Redis节点上面进行管理,也就是说每个Redis节点只负责一部分的哈希槽。在对数据进行操作的时候,集群会对使用CRC16算法对key进行计算并对16384取模(slot = CRC16(key)%16383),得到的结果就是 Key-Value 所放入的槽,通过这个值,去找到对应的槽所对应的Redis节点,然后直接到这个对应的节点上进行存取操作。
- 缺点:“使用场景并不那么普遍” ( 容器化之后)。
默认情况下,redis集群的读和写都是到master上去执行的,不支持slave节点读和写,跟Redis主从复制下读写分离不一样,因为redis集群的核心的理念,主要是使用slave做数据的热备,以及master故障时的主备切换,实现高可用的。Redis的读写分离,是为了横向任意扩展slave节点去支撑更大的读吞吐量。而redis集群架构下,本身master就是可以任意扩展的,如果想要支撑更大的读或写的吞吐量,都可以直接对master进行横向扩展
六、Redis 的雪崩和穿透?Redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 Redis 的穿透?
6.1 缓存穿透
-
描述:缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。如,发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
-
解决方案:
- 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
6.2 缓存击穿
-
描述:缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
-
解决方案:
- 设置热点数据永远不过期。
- 加互斥锁
6.3 缓存雪崩
-
描述:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
-
解决方案:
- 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
- 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
- 设置热点数据永远不过期。
- 做二级缓存。
- 缓存失效后,通过加锁或队列控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其它线程等待。
七、Redis为什么快?
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
- 数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的,比如跳表;
- 采用单线程,避免了:上下文切换、CPU竞争、多线程的锁施加和释放、死锁导致的性能消耗;
- 网络I/O:多线程,使用多路 I/O 复用模型,非阻塞 IO;
八、Redis到底是单线程还是多线程?
-
redis到底是单线程还是多线程?
(1)如果仅仅聊Redis的核心业务部分(命令处理),答案是单线程
(2)如果是聊整个Redis,那么答案就是多线程 -
在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:
(1)Redis v4.0:引入多线程异步处理一些耗时较旧的任务,例如异步删除命令unlink
(2)Redis v6.0:在核心网络模型中引入 多线程,进一步提高对于多核CPU的利用率 -
Redis为什么如此执着核心业务必须是单线程?
(1)Redis是纯内存操作,执行速度非常快,性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升。
(2)多线程会导致过多的上下文切换,带来不必要的开销
(3)引入多线程会面临线程安全问题,必然要引入线程锁这样的安全手段,实现复杂度增高,性能也降低
九、Redis分布式锁
-
怎么用setNx实现同步锁?使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功;
-
setNx的死锁问题?为防止获取锁后程序异常导致其他线程/进程setNx命令总是返回0而进入死锁状态,需要为该key设置一个“合理”的过期时间释放锁,使用DEL命令将锁数据删除。
十、redis缓存淘汰策略
-
定时过期:给每个设置过期时间的key都创建一个定时器,到期就会立即清除。
优点:该策略可立即清除过期的数据,对内存很友好;
缺点:占用大量CPU资源去处理过期数据,影响缓存的响应时间和吞吐量。 -
惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。
优点:该策略可以最大化地节省CPU资源;
缺点:对内存非常不友好,可能出现大量过期key没再次被访问不被清除情况。 -
定期扫描过期字典:每隔一定的时间,会扫描一定数量的数据库expires字典中一定数量的key,并清除其中已过期的key。
优点:该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
十一、redis热key问题
11.1 热key问题?原因?
一般情况,我们使用的缓存Redis是多节点的集群版,对某个key读写时,会根据该key的hash计算出对应的slot(槽),根据这个slot就能找到与之对应的分片(一个master和多个slave组成的一组redis集群)来存取该K-V。但是在实际应用过程中,对于某些特定业务或者一些特定的时段(比如电商业务的商品秒杀活动),可能会发生大量的请求访问同一个key。所有的请求(且这类请求读写比例非常高)都会落到同一个redis server上,该redis的负载就会严重加剧,此时整个系统增加新redis实例也没有任何用处,因为根据hash算法,同一个key的请求还是会落到同一台新机器上,该机器依然会成为系统瓶颈2,甚至造成整个集群宕掉,若此热点key的value 也比较大,也会造成网卡达到瓶颈,这种问题称为 “热key” 问题。
11.2 怎么发现热key
redis 4.0以上版本支持了每个节点上的基于LFU的热点key发现机制:redis-cli –hotkeys命令,可以定时在节点中使用该命令来发现对应热点key。这个命令的执行时间较长,可以设置定时执行来统计。
11.3 热key解决方案
-
对特定key或slot做限流:最简单粗暴的方式,对于特定的slot或者热key做限流,该方案对业务是有损的,建议只用在出现线上问题,需要止损时才进行特定限流。
-
使用二级(本地)缓存:是一个最常用的解决方案,等本地缓存过期后再重新请求redis数据,降低redis集群压力。
缺点:存在数据不一致的问题。我们设置多长的缓存过期时间,就会导致最长有多久的线上数据不一致问题,这个缓存时间需要衡量自身的集群压力以及业务接受的最大不一致时间。 -
拆key
拆key优点:既能保证不出现热key问题,又能尽量的保证数据一致性呢。
方法:在放入缓存时就将对应业务的缓存key拆分成多个不同的key。如下图所示, 我们首先在更新缓存的一侧,将key拆成N份,比如一个key名字叫做"good_100",那我们就可以把它拆成四份,“good_100_copy1”、“good_100_copy2”、“good_100_copy3”、“good_100_copy4”,每次更新和新增时都需要去改动这N个key,这一步就是拆key。
对于service端来讲,我们就需要想办法尽量将自己访问的流量足够均匀,如何给自己即将访问的热key上加入后缀。比如,根据本机ip或mac地址做hash,之后的值与拆key的数量做取余,最终决定拼接成什么样的key后缀,从而打到哪台机器上;服务启动时的一个随机数对拆key的数量做取余。