Redis知识点
2021-04-26 10:21:21 18 举报
AI智能生成
redis
作者其他创作
大纲/内容
基本数据类型
string
Redis会根据存储的数据及用户的操作指令自动选择合适的结构
int:存放整数类型;
SDS:存放浮点、字符串、字节类型;
sdshdr 底层是一个char数组
buf[]
len 已占用字符长度
free 剩余可用字符长度
最大容量为512M
为什么不直接用数组?——因为用动态扩容和缩容的需求
扩容过程
小于1M:free = len
为什么总空间为free + len +1?因为最后一个是\0
大于1M:分配free为1M
惰性空间释放:
当字符串缩短时,并没有真正的缩容,而是移动free的指针。
这样将来字符串长度增加时,就不用重新分配内存了
但这样会造成内存浪费,Redis提供了API来真正释放内存。
list
ziplist 压缩列表
当list元素个数少且元素内容长度不大时
压缩列表有点儿类似数组,通过一片连续的内存空间,来存储数据
它跟数组不同的一点是,它允许存储的数据大小不同。
每个节点上增加一个length属性来记录这个节点的长度,这样比较方便地得到下一个节点的位置。
linkedlist双向链表
其它场景
hash
压缩列表
当hash的元素个数少且内容长度不大时,使用压缩列表来实现。
字典 dict
散列表,类似Java的HashMap
使用链表法解决hash冲突
扩容和缩容:
rehash
渐进式rehash
将扩容操作穿插在插入操作的过程中,分批完成。
当负载因子触达阈值之后,只申请新空间,但并不将老的数据搬移到新散列表中。
当有新数据要插入时,将新数据插入新散列表中,并且从老的散列表中拿出一个数据放入到新散列表。
每次插入一个数据到散列表,都重复上面的过程。
经过多次插入操作之后,老的散列表中的数据就一点一点全部搬移到新散列表中了。
set
intset
整数,二分查找
插入的时候,要移动元素,所以时间复杂度是O(N)
字典
非整数类型
key为set的值,value为空
zset
压缩列表
元素个数不多且不大
因为要score排序,所以每次插入都要移动之后的数据
跳表
同list类似,Redis内部也不是直接使用的跳表,而是使用了一个自定义的数据结构来持有跳表。
当元素比较多时,使用跳表可以显著减少查找的次数。
redisObject
Redis对外暴露的是对象(数据类型),而每个对象都是用一个redisObject持有
通过不同的编码,映射到不同的数据结构。
有时候不同对象可能会底层使用同一种数据结构,比如压缩列表和字典等。
图片
其它数据类型
bitmaps
基于bit位的位置来记录信息的,利用了位运算的高性能
相比于list或者set,节省空间
可以使用一个可以扩容的整数数组来做,比如long数组。所以可以无限扩展。
如果太稀疏,这种场景下,其实不建议使用BitMap。
Redis提供了bitmaps对象。其实是使用的string对象,底层使用了SDS。所以会有512M的最大限制,即最多能存2^32个数据。如果你的id是long类型的,占64位,那可以使用两个bitmaps来存。
适合场景:
一个10G的文件,里面全部是自然数,一行一个,乱序排列,对其排序。在32位机器上面完成,内存限制为2G。
从0到n之间取出n个不同的数,找出漏掉的那个。注意:你的算法应当具有线性的时间复杂度。你能实现只占用常数额外空间复杂度的算法吗?
Java版本:java.util.BitSet
谷歌开源的EWAHCompressedBitMap解决了稀疏的问题
HyperLogLog(这玩意原理有些复杂,记住大概的原理和使用场景就行了)
大数据量下的基数计数场景。
基于数学的理论,概率论,有误差
基于伯努利分布的原理
基于LogLog Counting(LLC)改进而来。
空间复杂度只有log(log(N))。
redis中实现的HyperLogLog,只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。
因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。
如何降低误差?
分到不同的桶,计算几何平均数
调和平均数
“调和平均数”的结果会倾向于集合中比较小的数。而HLL正是在LLC的基础上,使用了调和平均数。
redis实现
Redis在2.8.9版本添加了HyperLogLog结构。
Redis首先对元素进行hash,生成64bit的数。
其中14bit用来分桶,其余50位用来计算n值。
每个桶所占用的元素是相对平均的。
它的实现中,设有16384个桶,即:2^14 = 16384,每个桶有6位,桶中存放的是这个桶的k_max的值,每个桶可以表达的最大数字是63,二进制为:111 111。所有桶加起来占用内存为=16834 * 6 / 8 / 1024 = 12K。
Redis在元素很少的时候使用的是稀疏矩阵,所以并不会用到12k。
布隆过滤器
解决去重问题(可容忍误差)
判断一个用户访问了一篇文章
判断一个ip访问了本网站
判断一个key是否被访问过
原理
布隆过滤器与BitMap类似,底层也是一个位数组。1表示有,0表示无。
但布隆过滤器比BitMap需要更少的内存
原理是使用多个不同的hash算法
把每次hash的结果下标都插入到相应的地方:
如果有判断某个元素是否在集合中,用同样的多个hash算法去看,是不是对应的下标都是1
误差
尤其是当hash函数比较少的时候
所以是存在一定的几率,要检查的元素实际上没有插入,但被其它元素插入影响,导致所有下标都为1。
所以布隆过滤器不能删除,因为一旦删除(即将相应的位置为0),就很大可能会影响其他元素。
如果空间越大,hash函数越多,结果就越精确,但空间效率和查询效率就会越低。
空间大小和集合大小不变的情况下,增加hash函数可以显著减小误差。
但一旦集合大小达到空间大小的25%左右后,增加hash函数带来的提神效果并不明显。这个时候应该增加空间大小。
redis实现
Redis的布隆过滤器不是原生自带的,而是要通过module加载进去。
https://github.com/RedisBloom/RedisBloom
布谷鸟过滤器:
对布隆过滤器的增强版。
解决了布隆过滤器的一些比较明显的缺点,比如:不能删除元素,不能计数等。
布谷鸟过滤器不用使用多个hash函数,所以查询性能更高。
在相同的误判率下,布谷鸟过滤器的空间利用率要明显高于布隆,空间上大概能节省40%多。
参考文章:https://juejin.im/post/5cfb9c74e51d455d6d5357db。
持久化机制
RDB(默认)
原理
通过快照的方式
达到触发条件时,Redis会自动将内存中所有数据以二进制方式生成一份副本并存储在硬盘上。
触发条件
主动
执行save和bgsave命令会进行持久化。
执行save会使Redis处于阻塞状态,不会响应任何其他客户端发来的请求,直到RDB快照文件执行完毕,需要谨慎使用。
bgsave即background save,后台保存。当执行bgsave命令时,Redis会fork出一个子进程来执行快照操作。需要注意的是,在fork子进程的过程中,Redis是阻塞的。而当子进程创建完成后,Redis就可以继续响应客户端的请求了。
子进程创建完成以后,返回“Background saving started”。子进程根据主进程的内存副本创建临时快照文件,当快照文件完成以后对原快照文件进行替换。替换完成后,子进程发送信号给主进程完成快照操作,主进程更新统计信息(info Persistence可查看),子进程退出。
被动
save m n规则触发
在指定的m秒内,Redis中有n个键发生改变,则自动触发bgsave。该规则默认在redis.conf中进行了配置,并且可组合使用,满足其中一个规则,则触发bgsave
flushall触发
flushall命令用于清空数据库,请慎用,当我们使用了则表明我们需要对数据进行清空,那Redis当然需要对快照文件也进行清空,所以会触发bgsave。
shutdown触发
Redis在关闭前处于安全角度将所有数据全部保存下来,以便下次启动会恢复。可以使用客户端连入执行shutdown命令,也可以直接使用脚本关闭Redis,都会在退出前先执行save。
shutdown命令还可以传递一个参数save/nosave。如果使用nosave参数,则不会进行持久化,直接退出。
主从复制触发
在Redis主从复制中,从节点执行全量复制操作,主节点会执行bgsave命令,并将rdb文件发送给从节点。
数据恢复
当Redis意外崩溃或者关闭再次启动时,此时AOF持久化未开启时(默认未开启),将使用RDB快照文件恢复数据。
AOF
默认关闭
原理
如果说RDB相当于数据库的定时备份(冷备),那AOF就相当于数据库的热备。
AOF可以将Redis执行的每一条写命令追加到磁盘文件中,在Redis启动时候优先选择从AOF文件恢复数据。
与RDB持久化相比,AOF持久化数据丢失更少,其消耗内存更少(RDB方式执行bgsve会有内存拷贝)。
AOF实现本质是基于Redis通讯协议,将命令以纯文本的方式写入到文件中。
持久化过程
1 追加写入
Redis将每一条写命令以Redis通讯协议添加至缓冲区aof_buf,这样的好处在于在大量写请求情况下,采用缓冲区暂存一部分命令随后根据策略一次性写入磁盘,这样可以减少磁盘的I/O次数,提高性能。
2 同步命令到硬盘
当写命令写入aof_buf缓冲区后,Redis会将缓冲区的命令写入到文件,Redis提供了三种同步策略,由配置参数appendfsync决定
no:不使用fsync方法同步,而是交给操作系统write函数去执行同步操作,在linux操作系统中大约每30秒刷一次缓冲。
always:表示每次有写操作都调用fsync方法强制内核将数据写入到aof文件。
everysec(推荐):数据将使用调用操作系统write写入文件,并使用fsync每秒一次从内核刷新到磁盘。 这是折中的方案,兼顾性能和数据安全,所以Redis默认推荐使用该配置。
3 文件重写(bgrewriteaof)
随着时间推移,AOF文件会越来越大
触发AOF文件重写条件的时候,Redis将使用bgrewriteaof对AOF文件进行重写
这样的好处在于减少AOF文件大小,同时有利于数据的恢复
重写策略
重复或无效的命令不写入文件
过期的数据不再写入文件
多条命令合并写入(当多个命令能合并一条命令时候会对其优化合并作为一个命令写入)
重写的过程(有点太深啦,详细的看文章吧。)
数据恢复
当AOF开启时候,Redis数据恢复优先选用AOF进行数据恢复。
混合(4.0提供,默认关闭)
原理
当开启混合持久化时,fork出的子进程先将共享的内存副本以RDB方式写入AOF文件
然后在将重写缓冲区的增量命令以AOF方式写入到文件
写入完成后通知主进程更新统计信息,并将新的AOF文件替换旧的的AOF文件。
简单的说:新的AOF文件前半段是RDB格式的全量数据,后半段是AOF格式的增量数据。
恢复
优先加载AOF文件。
AOF文件开头是RDB的格式,先加载RDB部分的内容,再加载剩余的AOF
AOF文件开头不是RDB的格式,直接加载整个AOF文件
过期策略(缓存淘汰策略)
因为前期redis是单线程的,所以不能简单粗暴地直接删除,怕阻塞太久
定期删除+惰性删除
触发
当Redis存储内存值接近或者超过maxmemory参数(maxmemory_policy)
策略
noeviction:拒绝写请求,正常提供读请求,这样可以保证已有数据不会丢失(默认策略);
volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key被淘汰,没有设置过期时间的key不会淘汰;
volatile-ttl:跟volatile-lru几乎一样,但是他是使用的key的ttl值进行比较,最先淘汰ttl最小的key;
volatile-random:其他同上,唯一就是他通过很随意的方式随机选择淘汰key集合中的key;
allkeys-lru:区别于volatile-lru的地方就是淘汰目标是全部key,没设置过期时间的key也不能幸免;
allkeys-random:这种方式同上,随机的淘汰所有的key。
volatile-lfu:对有过期时间的key采用LFU淘汰算法(4.0提供)
allkeys-lfu:对全部key采用LFU淘汰算法(4.0提供)
LRU
最初版
随机选三个Key,把idle time最大的那个Key移除。后来,把3改成可配置的一个参数,默认为N=5:maxmemory-samples 5
缺点:没有重复利用历史信息,上一轮选了,但没删除。下一轮可能又选到了它,但要重新计算idle time
缓冲池
当每一轮移除Key时,拿到了这个N个Key的idle time,如果它的idle time比 pool 里面的 Key的idle time还要大,就把它添加到pool里面去。
每次移除的Key并不仅仅是随机选择的N个Key里面最大的,而且还是pool里面idle time最大的
pool 里面的Key是经过多轮比较筛选的,它的idle time 在概率上比随机获取的Key的idle time要大,可以这么理解:pool 里面的Key 保留了"历史经验信息"。
在redisObject中的lru字段:
24 bits的lru是用来记录LRU time的
Java实现
LinkedHashMap实现了LRU算法
accessOrder 表示"最近最少未使用"的衡量标准。比如accessOrder=true,当执行java.util.LinkedHashMap#get元素时,就表示这个元素最近被访问了。
LFU(redis 4.0)
Least Frequently Used 最近频繁使用
因为LRU不精确
思路:可以为每个key维护一个计数器。每次key被访问的时候,计数器增大。计数器越大,可以约等于访问越频繁。
在redisObject中的lru字段:
高16 bits用来记录最近一次计数器降低的时间ldt,单位是分钟,低8 bits记录计数器数值counter。
也是用了缓冲池
三种集群
主从
从数据库启动时,会向主数据库发送sync命令,主数据库接收到sync后开始在后台保存快照rdb
在保存快照期间收到的命令缓存起来,当快照完成时,主数据库会将快照和缓存的命令一块发送给从
之后,主每收到1个命令就同步发送给从。
当客户端发送写执行给主,主执行完立即将结果返回客户端,并异步的把命令发送给从,从而不影响性能。
哨兵
Redis 2.8中提供了哨兵工具来实现自动化的系统监控和故障恢复功能。
哨兵的作用就是监控redis主、从数据库是否正常运行,主出现故障自动将从数据库转换为主数据库。
只需要配置其监控主数据库即可,哨兵会自动发现所有复制该主数据库的从数据库
根据优先级选,如果一样就选个id小的
集群
分布式存储,每台存储不同的内容
集群至少需要3主3从,且每个实例使用不同的配置文件,主从不用配置,集群会自己选。
缓存常见问题及解决方案
缓存穿透
描述
一直访问一个数据库不存在的数据,比如id为-1的数据,就会导致每次请求都会先去缓存查一次,然后再去数据库查一次
解决方案
对请求参数做校验,比如用户鉴权校验,id做基础校验,id <= 0的直接拦截。
如果查询到数据库没有值,也将对应的key存进缓存中,value为null。这样下次查询就直接从缓存返回了。但这里的key的缓存时间应该比较短,比如30s。防止后面在数据库插入了这条数据,而用户获取不到。
使用布隆过滤器,判断一个key是否已经查过了,如果已经查过了,就不去数据库查询。
缓存击穿
描述
一个key的访问量非常大,比如某秒杀活动,有1w/s的并发量。这个key在某一时刻过期,那这些大量的请求就会一瞬间到数据库,数据库可能会直接崩溃。
解决方案
对于热点数据,慎重考虑过期时间,确保热点期间key不会过期,甚至有些可以设置永不过期。
使用互斥锁(比如Java的多线程锁机制),第一个线程访问key的时候就锁住,等查询数据库返回后,把值插入到缓存后再释放锁,这样后面的请求就可以直接取缓存里面的数据了。
缓存雪崩
描述
缓存雪崩指的是,在某一时刻,多个key失效。这样就会有大量的请求从缓存中获取不到值,全部到数据库。还有另一种情况,就是缓存服务器宕机,也算做缓存雪崩。
解决方案
对每个key的过期时间设置一个随机值,而不是所有key都相同。
使用高可用的分布式缓存集群,确保缓存的高可用性,比如redis-cluster。
双写不一致
描述
发生写操作(更新)的时候或写操作之后,可能会存在数据库里面的值和缓存中的值不同的情况。
解决方案
先删除缓存,再更新数据库(但不能完全解决)
为什么更新的时候要先删除缓存,再更新数据库?因为如果先更新数据库,然后在删除缓存的时候失败了,就会造成缓存里面的值和数据库的值不一致。
把过期时间设置得比较低(业务场景允许)
使用队列辅助。先更新数据库,再删除缓存。如果删除失败,就放进队列。然后另一个任务从队列中取出消息,不断去重试删除相应的key。
使用队列读写操作串行化;写操作,删除缓存后,放进一个队列;然后另一个线程过来了,发现没有缓存,则把这个读操作也放进这个队列里面。(性能不ok)
redis常见问题
单线程还是多线程?
Redis4.0之前是单线程运行的;Redis4.0后开始支持多线程。
单线程原因?
开发和维护更简单,因为单线程模式方便开发和调试。
即使使用单线程模型也能够并发地处理多客户端的请求,主要是因为Redis内部使用了基于epoll的多路复用。
对于Redis来说,主要的性能瓶颈是内存或网络带宽,而非CPU。
多线程原因
为了一些操作不阻塞主线程
Redis在4.0以及之后的版本中引入了惰性删除(也叫异步删除),意思就是我们可以使用异步的方式对Redis中的数据进行删除操作
Redis 6.0中的多线程?(默认禁用,可配置线程数量)
将主线程的IO读写任务拆分给一组独立的线程去执行,这样就可以使用多个socket的读写并行化了,
但Redis的命令依旧是主线程串行执行的。
Redis的作者在2019年的RedisConf大会上提到,Redis6.0引入的多线程IO特性对性能的提升至少是一倍以上。
为什么性能高
基于内存操作
数据结构简单,专门设计
多路复用和非阻塞IO
单线程模型,避免上下文切换
官方使用的基准测试结果表明,单线程的Redis可以达到10W/S的吞吐量。
如何实现分布式锁
目的
为了保证多台服务器在执行某一段代码时保证只有一台服务器执行。
性质
互斥性。在任何时刻,保证只有一个客户端持有锁。
不能出现死锁。如果在一个客户端持有锁的期间,这个客户端崩溃了,也要保证后续的其他客户端可以上锁。
保证上锁和解锁都是同一个客户端。
redis实现
基于setnx命令
SET if not exists(如果不存在,则 SET)的简写。
加锁
setnx key value
解锁
del
如何防止死锁?
超时机制
缺点:不可重入
可重入的做法:用hash。但需要识别客户端标识
缺点:加锁失败,重试性能差
私发发布订阅优化
当加锁失败后,订阅锁释放的消息,自身进入阻塞状态。
当持有锁的客户端释放锁的时候,发布锁释放的消息。
当进入阻塞等待的其他客户端收到锁释放的消息后,解除阻塞等待状态,再次尝试加锁。
其他方案
使用MySQL,基于唯一索引。
使用ZooKeeper,基于临时有序节点。
如何实现分布式id生成器
用原子命令 比如incr或increby就行。
如何实现秒杀功能?
每完成一条秒杀记录的处理,就执行INCR key_num。一旦所有库存处理完毕,就结束该商品的本次秒杀,关闭工作线程,也不再接收秒杀请求。
RPUSH key value,插入秒杀请求。当插入的秒杀请求数达到上限时,停止所有后续插入。
LPOP key,后台启动多个工作线程。处理秒杀请求
如何实现排行榜?
使用zset数据结构即可
作为消息队列?
Redis自带的PUB/SUB机制
作为注册中心?
借助了redis来存储服务节点信息,使用redis的publish/subscribe功能来对应对服务节点信息变更
redis事务?
通过MULTI EXEC DISCARD WATCH四个原语实现
MULTI开启事务,之后的命令会被放到一个队列中
EXEC执行所有事务块内的命令
DISCARD 清空事务队列,并放弃执行事务
WATCH
为事务提供CAS行为。一旦某个key被删除或修改,之后的事务就不会执行
在事务失败时不进行回滚,而是继续执行余下的命令
如何保证原子性的?
单线程。。。
0 条评论
下一页
为你推荐
查看更多