布隆过滤器
布隆过滤器(boom filter)可以看做是bitmap的拓展,是一个二进制数组里存放着0/1的值,如果是1就是集合中有这个数,否则没有这个数。使用了多个hash映射函数来减轻hash冲突的概率。插入和查询的时间复杂度为O(m),m为哈希函数的个数
- 优点:查询速度快,不占空间
- 缺点:因为布隆过滤器没法删除元素,所以随着元素的增加,误算率也在增加
String底层
-
String的内部value的存储结构一般是sds(Simple Dynamic String),但是如果一个String类型的value的值是数字,那么Redis内部会把它转成long类型来存储,从而减少内存的使用。
-
sds由len、free、buf[]组成
-
struct sdshdr{ int len; int free; char buf[]; }
-
性能高:len保存了buf数组中的长度,当要获取字符串长度时无需遍历可直接返回
-
内存会预分配,优化字符串的增长操作。当需要修改数据时,会先判断len是否满足,不满足则自动扩容空间,再进行修改,同时会为free分配空间。分配有两种策略:①当字符串修改后len小于1M,则free分配len大小的空间 ②当字符串修改后的大小大于1M,则free分配1M。下次再修改字符串时就先检查未使用的free空间是否满足,不满足再拓展。③二进制安全,不仅可以存放字符串还可以存放图片或者序列化对象等,不会像C语言一样’\0’去结束字符串
-
惰性空间回收,优化字符串的缩短操作。当缩短 SDS 字符串后,并不会立即执行回收多余的空间,如果后续有增长操作,则可直接使用。
list底层
(1)Redis3.2之前的底层实现方式:压缩列表ziplist 或者 双向循环链表linkedlist
当list存储的数据量较少时,会使用ziplist存储数据,也就是同时满足下面两个条件:
列表中数据个数少于512个
list中保存的每个元素的长度小于 64 字节
当不能同时满足上面两个条件的时候,list就通过双向循环链表linkedlist来实现了
ziplist使用一块连续的内存空间来存储数据,类似于数组,可以节省普通双向链表指针的内存空间。
(2)Redis3.2及之后的底层实现方式:quicklist
quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和ziplist的优点
set底层
底层实现方式:有序整数集合intset 或者 字典dict
当存储的数据同时满足下面这样两个条件的时候,Redis 就采用整数集合intset来实现set这种数据类型:
存储的数据都是整数
存储的数据元素个数小于512个
当不能同时满足这两个条件的时候,Redis 就使用dict来存储集合中的数据
- intset是一个由整数组成的有序集合,便于进行二分查找,性能更高
- dict是用哈希表实现
Hash底层
当数据量较少的情况下,hash底层会使用压缩列表ziplist进行存储数据,也就是同时满足下面两个条件的时候:
hash-max-ziplist-entries 512:当hash中的数据项(即filed-value对)的数目小于512时
hash-max-ziplist-value 64:当hash中插入的任意一个value的长度小于64字节
当不能同时满足上面两个条件的时候,底层的ziplist就会转成dict,之所以这样设计,是因为当ziplist变得很大的时候,它有如下几个缺点:
每次插入或修改会引发内存拷贝,从而降低性能。
当ziplist数据项过多的时候,在它上面查找指定的数据项就会性能变得很低,因为ziplist上的查找需要进行遍历。
总之,ziplist本来就设计为各个数据项挨在一起组成连续的内存空间,这种结构并不擅长做修改操作。一旦数据发生改动,就会引发内存realloc,可能导致内存拷贝。
zset底层
zset的编码有ziplist和skiplist两种
当满足以下两个条件时,使用ziplist
- 保存的元素少于128个
- 保存的所有元素大小都小于64字节
当不满足时,使用skiplist
这两个边界值是可以通过配置文件修改的
ziplist编码(压缩列表)
类似于一个数组,使用两个紧挨在一起的压缩列表结点来保存,第一个结点代表的是member,第二个结点代表的是score,按score进行升序排序。因为ziplist只能线性移动,所以查找元素的时间复杂度为O(n)
每个集合元素使用两个紧挨在一起的压缩列表结点来保存,第一个结点保存member,第二个结点保存score。
按score排序。因为ziplist的结点只能线性地移动,所以查找某个元素的时间复杂度为O(n).
ZADD key score member [[score member] [score member] ...]
skiplist编码
skiplist编码的底层是用一个zset的结构体,这个结构体包含一个字典和一个跳表
typedef struct zset{
//跳跃表
zskiplist *zsl;
//字典
dict *dice;
} zset;
字典的键是member,值是score,这样可以以O(1)的复杂度来查找member的score值
跳跃表是一个多层链表,底层是一个完整链表,层数是随机生成。
结点按score的值从小到大保存,先从上层查找,找到对应区间后再往下一层查找。整个过程类似于二分查找。所以搜索平均复杂度为O(logN),最坏为O(N).
这两种数据结构会通过指针来共享相同元素的成员和分值。不会产生重复的成员和分值,造成内存浪费。
结合两种是为了:使用字典来实现单一查询快O(1),使用跳表来实现范围查询快O(logn)
(说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。)
skiplist
1、在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。查找单个key,时间复杂度为O(log n)
2、跳表的本质是多层链表,最底层是完整的有序链表,从下往上随机抽析,每一个结点的层数都是随机生成的,通过抽析来过滤无效数据。
3、插入操作只需修改插入结点前后的指针,而不需要对很多结点进行调整,这就降低了插入操作的复杂度。这是skiplist的一个很重要的特性,这让他在插入性能上明显优于平衡树方案
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sMHtiEUC-1665923326168)(Redis%E8%80%83%E7%82%B9.assets/format,png-16496850248256-16496850283428.png)]
skiplist与哈希表、平衡树比较
- skiplist和平衡树(AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此哈希表只能做单个key的查找,不适宜范围查找,所谓范围查找,指的是查找大小在指定的两个值中间的所有结点。
- 在做范围查找时,AVL要比skiplist复杂。AVL在找到指定范围查找的小值后,还要用中序遍历去寻找比大值小的结点。而skiplist的范围查询就非常简单,只需要在找到最小值后,对第一层链表进行若干步遍历就可以实现。
- 平衡树的插入和删除会引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻结点的指针,操作简单又快速。
- 从内存占用上来看,skiplist比平衡树更灵活一些。一般来说,平衡树包含两个指针,指向左右孩子。而skiplist的每个结点包含的指针数目平均为1/(1-p),redis中p=1/4,那么平均每个结点包含1.33个指针,比平衡树更具优势
- AVL和skiplist的单个key查找时间复杂度都为o(logn),而哈希表接近O(1),性能更高。
- 从算法实现复杂度上来看,skiplist比AVL要简单的多
Redis为什么使用skiplist而不用平衡树
1、跳表内存占用更少,每个结点的指针个数是通过调整随机生成层的概率来改变的
2、zset经常使用范围查询,像zrange、zrevrange的范围操作,跳表里的双向链表可以十分方便地进行这类操作
3、跳表的实现更加简单
缓存问题
https://baijiahao.baidu.com/s?id=1686162143536500086&wfr=spider&for=pc
缓存穿透
缓存穿透是指查询一个不存在的数据,导致大量请求到达数据库,造成短时间内数据库承受大量请求崩掉
解决对策:
-
缓存空值,不会查数据库。
(但是如果大量的key穿透会占用很大的内存空间,而且缓存空值的期间如果数据库更新完毕会造成数据不一致)
-
采用布隆过滤器,用多个hash函数把key映射到二进制数组中,哈希计算不到1的数据就会被过滤器拦截掉,避免数据库压力。
缓存击穿
概述
大量的请求同时查询一个key时,此时这个key正好失效了,会导致大量的请求落到数据库上。
缓存击穿是查询缓存中失效的key,而穿透是查询不存在的key
解决对策
1、设置key不过期
2、互斥锁(分布式锁):
当key失效的时候,让一个线程读取数据并构建到缓存中,其他线程就先等待,直到缓存构建完后重新读取缓存即可。
缓存雪崩
概述
缓存雪崩是指缓存同一时间大面积的失效,大量请求会落到数据库上,造成数据库崩
解决方案
解决对策
1、key不过期/互斥锁
跟缓存击穿的思路一致,可以设置key不过期或者互斥锁的方式。但是这种太浪费内存和性能
2、在key的过期时间上加上随机值,让过期时间分散点
缓存预热
概述
系统上线后,先将相关的数据构建到缓存中,这样就可以避免用户请求时直接请求数据库
解决方案
- 定时刷新缓存
- 若缓存数据不大,可以直接在项目启动时加载
缓存降级
概述
缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据
Redis概念
什么是redis
高性能非关系型的键值对数据库,数据存在内存中,读写速度非常快,可以将数据写入磁盘,保证持久性。reids的操作是原子性的。
redis的优缺点
优点:
- 基于内存操作,读写速度很快
- redis是单线程的,可以避免多线程竞争的开销。(这里的单线程是指用一个线程去处理网络的所有请求。而redis运行时不止一个线程,持久化的时候会另起线程)
- 支持持久化,有AOF和RDB两种持久化方式
- 支持事务(不保证原子性,单条指令是原子性,事务不是,不支持回滚)
缺点:
- 数据容量受物理内存限制,不适合海量数据读写
- 扩容难度大
为什么redis这么快
- 基于内存
- 单线程,减少线程切换的开销和锁竞争
- 采用I/O多路复用(一个线程处理多个TCP连接),不在网络I/O上浪费过多时间
- 数据结构简单高效
Redis为何选择单线程
- 避免过多的上下文切换开销
- 实现简单,维护方便
Redis应用场景
- 缓存,对一些不经常更改的热点数据进行缓存,减轻数据库的压力
- 计数器,数据类型有相应的自增自减
- 做消息队列,List是一个双向链表,可以通过lpush和rpop实现队列
- set可以交并集实现共同好友,zset可以实现热帖排行榜
Memcached和redis的区别
- memcached不支持分布式,redis cluster支持分布式
- memcached结构单一,而redis支持多种数据类型
- memcached不支持数据持久化,重启就没,而redis支持持久化
redis的数据类型
基本数据类型:
- String:简单的键值对缓存,可以作为计数器使用
- List:有序可重复集合,底层是双向链表,可做简单的消息队列、粉丝列表等
- Set:无序去重集合,提供了交集并集差集的操作,可以用交集实现共同好友、共同关注
- Hash:键值对集合,Key一般为ID,然后value就是对应的详情了,如用户信息详情
- ZSet:有序去重集合,内部维护了一个score参数。可以实现热帖排行版
特殊的数据类型:
- Bitmap:二进制数组,基于bit存储,用0和1来记录状态,非常节省空间。
- Hyperloglog:根据伯努利实验思想采用一种基数算法来完成对总数的统计,概率数据结构,不存储元数据,只统计基数大小,去重方案误差率只有0.81%
- Geospatial:主要用于存储地理位置信息,适用于定位、查找附近的人等
ZSet和List的异同点:
相同点:
- 都是有序的
- 都可以获得某个范围内的元素
不同点:
- ZSet是基于跳跃链表和压缩链表组成,访问中间元素的复杂度是logn
- 而List是基于双向链表,访问中间元素的复杂度是n
- List没法简单地调整元素位置,但ZSet可以(通过更改分数)
- ZSet因为需要记录分数更加耗费内存
redis事务
事务原理是将操作命令先放入队列中然后再让redis依次执行
事务的生命周期:
- 使用MULTI开启事务
- 开启事务后命令话被插入到一个队列中。同时这个命令并不会被真的执行
- 使用EXEC进行提交任务
- 事务范围内某个命令出错不会影响其他命令的执行,不保证原子性
redis事务特性
redis事务总是具有acid中的隔离性和一致性(单线程),当选择aof持久化时也有一定的持久性
不具备原子性的原因是:事务内某个命令出错不会回滚,会继续执行接下去的命令
redis具备隔离性吗
redis是单线程,他能保证事务在执行过程中不会被中断
redis事务是原子性的吗
单条命令是,事务不是,因为事务队列里一条命令执行失败其他命令扔会执行
redis的持久化机制
redis有两种持久化方式:RDB和AOF
RDB(Redis Database)
redis默认的持久化方式,在一定时间内将内存的数据以快照的方式存到硬盘中,生成dump.rdb
优点:
- redis加载RDB恢复数据要快于AOF
- 使用单独的子进程来持久化,主进程不会进行任何I/O操作,保证了redis的性能
缺点:
- 数据安全性低,RDB是隔一段时间进行持久化,无法实时持久化。若redis发生故障,会发生数据丢失
- 存在老版本redis无法兼容新版本的rdb格式问题
触发方式:
-
手动触发:用户执行SAVE或BGSAVE命令
-
被动触发:1、配置文件里修改,SAVE 100 10,就是100s内至少有10个健被修改时进行快照
2、进行全量复制时
3、shutdown时若无开启AOF则自动执行RDB
AOF(Append Only-file)
redis每次的写命令都记录到独立的日志中,当重启redis时会从日志里恢复数据
优点:
- AOF安全性更好,可以更好的保证数据不丢失,可以配置AOF每秒写入一次,如果redis挂掉最多只丢失1s的数据
- AOF以append-only的模式写入,没有磁盘寻址的开销,写入性能非常高
缺点:
- AOF的文件比RDB的要大,且恢复速度缓慢
如何选择合适的持久化方式
通常应该同时使用两种持久化方案,以保证数据安全
- 如果数据可以承受几分钟的数据丢失,使用RDB
- 如果只希望数据在服务运行时存在,可以不使用持久化
- 如果数据很重要,建议RDB和AOF都开启
(若两个都开启,redis恢复数据会优先使用aof,因为aof更加完整)
过期键的删除策略
- 定时过期:设置了过期时间的key都会有一个计时器,到过期时间就会删除。对内存友好,但是会占用大量CPU资源去处理过期数据
- 惰性过期:只有当访问key时,才会判断该key是否过期,过期则删除。对CPU友好但是对内存不友好
- 定期过期:每隔一段时间会会扫描一定数量的key,并删除其中过期了的key,折中方案。
redis同时使用了惰性和定期两种策略
redis内存淘汰策略
redis内存超过最大内存后会触发内存淘汰策略
- volatile-lru:在设置过期时间的键空间中,移除最近最少使用的key
- allkeys-lru:在全局键空间中移除最近最少使用的key
- volatile-ttl:在设置过期时间的键空间中移除有更早过期时间的key
- volatile-random:在设置已过期的键空间中任意删除key
- allkeys-random:在全局键空间中任意删除key
- no-eviction:禁止删除数据,当内存不足时写入会报错
redis4.0之后添加了两种:
- volatile-lfu:在设置过期时间的键空间中移除最少使用的key
- allkeys-lfu:在全局键空间中移除最少使用key
缓存与数据库一致性
集群方案(部署方式)
redis主从
主库写从库读
原理:
- 当从库和主库建立起主从关系后,会向主库发送sync命令
- 主库收到SYNC命令后,会开始在后台保存快照(RDB持久化),并将期间收到的写命令缓存起来
- 当快照完成后,主库会将快照文件和缓存的命令发给从库
- 从库接收到后会将快照文件加载进本地,再加载到内存中并执行收到的缓存命令
- 之后,主库收到写命令时就会将命令发给从库,保证数据的一致性
- 如果网络故障断开主从连接,会自动重连,连接后主库会将缺失的那部分数据复制到从库
哨兵模式
主从复制不能自动故障转移,达不到高可用性。
哨兵解决了这个问题,哨兵可以自动切换主从结点
客户端连接redis的时候先连接哨兵,哨兵会告诉客户端主节点的地址让客户端连上redis
若哨兵检测到redis主节点宕机,会选择某个变现良好的从节点成为主节点,并通过发布订阅模式通知其他从节点,让他们切换主机。
原理:
- 每个哨兵以每秒一次的频率向它所监控的master、slave发送ping命令
- 如果一个节点有效回复超过ping的指定值,则会被标记为主观下线
- 如果被标记为主观下线的是主节点,则在监控主节点的所有哨兵都要以每秒一次的频率确认主节点是否真正进入主观下线状态
- 当有足够数量的哨兵(数量大于配置文件的数量)在指定时间内确定master进入主观下线状态,则master会被标记为客观下线。若没有足够数量的哨兵同意已下线,则客观下线状态会被解除。若ping命令得到有效回复,则master的主观下线状态会被解除
- 哨兵节点会选举出哨兵leader,负责故障转移工作
- 若主节点宕机,则哨兵节点会推选出优秀的从节点成为主节点,并通过发布订阅模式通知其他节点,让他们切换主机
哨兵作用:
- 集群监控:负责监控主从节点是否正常工作
- 消息通知:若有节点出现故障,会通知给客户端
- 故障转移:让优秀的从节点代替主节点
- 配置中心:若故障发生,会通知从节点新的主节点地址
注意点:
- 哨兵至少需要3个实例,来保证自己的健壮性
- 哨兵+redis主从,是不保证数据零丢失的,只能保证redis集群的高可用性
官方的redis cluster
实现redis的分布式存储,每个结点存储不同的内容,解决主节点的写能力、容量受限于单机配置的问题
redis怎么实现消息队列
- 使用一个list,生产者用LPUSH将消息放进列表,消费者用RPOP从列表中取出任务,若RPOP没有信息时可以sleep一段时间,然后检查有无信息。若不想使用sleep,可以用BRPOP,若List中无元素就会一直阻塞连接,直到有新元素加入。
- redis还可以用发布/订阅模式实现一个生产者多个消费者,缺点就是消费者下线时生产信息也会丢失
redis怎么实现延时队列
使用zset,拿时间戳作为score,消息内容作为key,调用zadd来生产信息,消费者用zrangebyscore获取n秒之前是数据进行处理
分布式锁
可以用SETNX实现或者用RedLock实现
RedLock
redis官方推出的分布式锁
- 能够实现互斥,永远只有一个client能拿到锁
- 避免死锁,最终client都可能拿到锁
- 只要大部分redis节点存活就可以提供正常服务
己的健壮性 - 哨兵+redis主从,是不保证数据零丢失的,只能保证redis集群的高可用性
官方的redis cluster
实现redis的分布式存储,每个结点存储不同的内容,解决主节点的写能力、容量受限于单机配置的问题
redis怎么实现消息队列
- 使用一个list,生产者用LPUSH将消息放进列表,消费者用RPOP从列表中取出任务,若RPOP没有信息时可以sleep一段时间,然后检查有无信息。若不想使用sleep,可以用BRPOP,若List中无元素就会一直阻塞连接,直到有新元素加入。
- redis还可以用发布/订阅模式实现一个生产者多个消费者,缺点就是消费者下线时生产信息也会丢失
redis怎么实现延时队列
使用zset,拿时间戳作为score,消息内容作为key,调用zadd来生产信息,消费者用zrangebyscore获取n秒之前是数据进行处理
分布式锁
可以用SETNX实现或者用RedLock实现
RedLock
redis官方推出的分布式锁
- 能够实现互斥,永远只有一个client能拿到锁
- 避免死锁,最终client都可能拿到锁
- 只要大部分redis节点存活就可以提供正常服务