五种基本类型及底层实现
String
String是最简单的数据类型,一般用于复杂的计数功能的缓存:微博数,粉丝数等。
底层实现方式:动态字符串sds 或者 long 。
String的内部存储结构一般是sds(Simple Dynamic String),但是如果一个String类型的value的值是数字,那么Redis内部会把它转成long类型来存储,从而减少内存的使用。
使用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
Hash
Hash适合用于存储对象,因为一个对象的各个属性,正好对应一个hash结构的各个field,可以方便地操作对象中的某个字段。
底层实现方式:压缩列表ziplist或者字典dict。
当数据量较少的情况下,hash底层会使用压缩列表ziplist进行存储数据,也就是同时满足下面两个条件的时候:
- hash-max-ziplist-entries 512:当hash中的数据项(即filed-value对)的数目小于512时
- hash-max-ziplist-value 64:当hash中插入的任意一个value的长度小于64字节
当不能同时满足上面两个条件的时候,底层的ziplist就会转成dict
使用场景:存储部分更改数据,如用户信息、会话共享、缓存对象、购物车
List
list 的实现为一个双向链表,经常被用作队列使用,支持在链表两端进行push和pop操作,时间复杂度为O(1);同时也支持在链表中的任意位置的存取操作,但是需要对list进行遍历,时间复杂度为O(n)。
- Redis3.2之前的底层实现方式:压缩列表ziplist 或者 双向循环链表linkedlist
当list存储的数据量较少时,会使用ziplist存储数据,也就是同时满足下面两个条件:- 列表中数据个数少于512个
- list中保存的每个元素的长度小于 64 字节
当不能同时满足上面两个条件的时候,list就通过双向循环链表linkedlist来实现了
- Redis3.2及之后的底层实现方式:quicklist
quicklist是一个双向链表,而且是一个基于ziplist的双向链表,quicklist的每个节点都是一个ziplist,结合了双向链表和ziplist的优点
使用场景:微博的关注列表;粉丝列表;最新消息排行榜;消息队列,以完成多程序之间的消息交换(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据);可以利用lrange命令,做基于redis的分页功能等
Set
set是一个存放不重复值的无序集合,可做全局去重的功能,提供了判断某个元素是否在set集合内的功能,这个也是list所不能提供的。基于set可以实现交集、并集、差集的操作,计算共同喜好,全部的喜好,自己独有的喜好等功能。
底层实现方式:有序整数集合intset 或者 字典dict
当存储的数据同时满足下面这样两个条件的时候,Redis 就采用整数集合intset来实现set这种数据类型:
- 存储的数据都是整数
- 存储的数据元素个数小于512个
当不能同时满足这两个条件的时候,Redis 就使用dict来存储集合中的数据
使用场景:聚合计算(并集、交集、差集)场景计算共同喜好,全部的喜好,自己独有的喜好;比如点赞、共同关注、抽奖活动等
Sorted Set
Sorted set 相比 set 多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用,取TOP N操作。另外,sorted set可以用来做延时任务。最后一个应用就是可以做范围查找。
底层实现方式:压缩列表ziplist 或者 zset
当 sorted set 的数据同时满足下面这两个条件的时候,就使用压缩列表ziplist实现sorted set
- 元素个数要小于 128 个,也就是ziplist数据项小于256个
- 集合中每个数据大小都小于 64 字节
当不能同时满足这两个条件的时候,Redis 就使用zset来实现sorted set,这个zset包含一个dict + 一个skiplist。dict用来查询数据到分数(score)的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)
使用场景:排序场景,比如排行榜、电话和姓名排序、比较复杂的数据结构,一般用到的场景不算太多等
四种特殊类型及使用场景
BitMap(2.2 版新增)(位图)
二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等; (布隆过滤器在Redis4.0之前,只能通过bitmap来实现;在Redis4.0之后,官方提供了module能力,这时候,官方提供的RedisBloom才算正式出道)
HyperLogLog(2.8 版新增)(基数统计)
海量数据基数统计的场景,比如百万级网页 UV 计数等
GEO(3.2 版新增)(经纬度)
存储地理位置信息的场景,比如滴滴叫车
Stream(5.0 版新增)(消息队列)
相比于基于 List 类型实现的消息队列,有以下特性:
- 支持消息的持久化
- 支持自动生成全局唯一 ID
- 支持 ack 确认消息的模式
- 支持消费组模式等,让消息队列更加的稳定和可靠
Redis为什么这么快?
Redis是基于内存运行的高性能 K-V 数据库,官方提供的测试报告是单机可以支持约10w/s的QPS
- 完全基于内存,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销。
- 数据结构简单,对数据操作也简单。Redis中的数据结构是专门进行设计的,每种数据结构都有一种或多种数据结构来支持。Redis正是依赖这些灵活的数据结构,来提升读取和写入的性能。
- 采用单线程,省去了很多上下文切换的时间以及CPU消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗。
- 使用基于IO多路复用机制的线程模型,可以处理并发的连接。
这里强调的单线程,指的是网络请求模块使用一个线程来处理,即一个线程处理所有网络请求,其他模块仍用了多个线程(如持久化线程)
Redis6.0 的多线程
- Redis6.0 之前为什么一直不使用多线程?Redis使用单线程的可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
- Redis6.0 为什么要引入多线程呢?因为Redis的瓶颈不在内存,而是在网络I/O模块带来CPU的耗时,所以Redis6.0的多线程是用来处理网络I/O这部分,充分利用CPU资源,减少网络I/O阻塞带来的性能损耗。
- Redis6.0 如何开启多线程?默认情况下Redis是关闭多线程的,可以在conf文件进行配置开启:io-threads-do-reads yesio-threads 线程数## 官方建议的线程数设置:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数,尽量不超过8个。
- 多线程模式下,是否存在线程并发安全问题?如图,一次redis请求,要建立连接,然后获取操作的命令,然后执行命令,最后将响应的结果写到socket上。
在redis的多线程模式下,获取、解析命令,以及输出结果这两个过程,可以配置成多线程执行的,因为它毕竟是我们定位到的主要耗时点,但是命令的执行,也就是内存操作,依然是单线程运行的。所以,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行,也就不存在并发安全问题。
Redis NIO epoll模式 ![](https://i-blog.csdnimg.cn/blog_migrate/7033a198a0a0b38216a559548c4e28f4.png)
这里引入两个概念,mmap和零拷贝(sendfile),这些都是操作系统内核帮我们做的事情。
- mmap是内存地址映射,大白话讲就是用户态的内存地址空间和内核态的内存地址空间做映射关系。比如上图,在用户态的时候我们有个文件描述符epfd,当我们调用内核态将epfd传递进去后,通过mmap技术,内核就和用户态使用同个epfd来监听数据。不会向select模型那样独立在内核开辟个内存空间存epfd。
- 量拷贝的概念是当数据到达内核后,用户态要来拉取数据进行操作,不用在用户态独立在开辟个空间存这些数据,内核态和用户态共享这块内存空间。对应内核态提供的接口是sendfile。
epoll是这样做的,用户态把fd传递给内核态,内核态提供mmap技术,知道用户态需要监听哪些数据。mmap里面主要有两大块,红黑树结构来存fd,链表来存内核监听到哪些fd有数据,用户态一直监听mmap空间中链表,只要有数据把链表中的fd拿出来,再调用内核的read方法读取数据。这里使用零拷贝技术,所以不用把内核空间拷贝到用户空间,性能得到提高。
Redis序列化方式
- JdkSerializationRedisSerializer这是RestTemplate类默认的序列化方式。优点:反序列化时不需要提供类型信息(class),缺点:需要实现Serializable接口存储的为二进制数据序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。
- StringRedisSerializer是StringRedisTemplate默认的序列化方式,key和value都会采用此方式进行序列化,是被推荐使用的,对开发者友好,轻量级,效率也比较高。
- GenericToStringSerializer需要调用者给传一个对象到字符串互转的Converter
- Jackson2JsonRedisSerializer优点:速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。缺点:此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象),其在反序列化过程中用到了类型信息。
- GenericJackson2JsonRedisSerializer与Jackson2JsonRedisSerializer大致相同,会额外存储序列化对象的包名和类名。
Redis的三大缓存问题
缓存穿透
概念:业务系统查询缓存和数据库中都不存在的数据。当通过接口发送数据查询请求,请求的数据在缓存中没有,转而所有请求转向数据库,但是数据库中也不存在该数据。此时,缓存起不到保护数据库的作用,就像被穿透了一样,这种现象称为缓存穿透。
危害:如果黑客大量发起不存在的数据请求,海量的请求转向数据库,数据库承受不了流量洪峰,数据库压力剧增,可能导致系统瘫痪。在目前的业务系统中,比较脆弱的就是IO部分。
解决方案:
- 前后端参数校验
- 缓存空值
--之所以会发生缓存穿透,是因为缓存中不存在这些空数据的key,才会导致所有请求转向数据库。如果当数据库返回为空时,把这些空值对应的key缓存到Redis,并合理设置过期时间。 - 使用布隆过滤器
--布隆过滤器可以用于检索一个元素是否在一个集合中,它的优点是空间效率和查询时间比一般的算法要好得多。当业务系统发出查询请求的时候,首先去布隆过滤器中查询该key是否存在。如果不存在,则说明数据库中也不存在该数据,因此不需要查询,直接返回null。如果存在,则继续执行后续的流程,先前往Redis缓存中查询,缓存中没有的话再前往数据库中的查询。
方案选择:
- 对于黑客的恶意攻击,查询的key值往往不同,而且数据量巨大。使用缓存空值的方法不大可取,可以使用布隆过滤器进行海量请求的过滤。
- 如果查询的空数据,key值重复率高,可以选择缓存空值的方法。
布隆过滤器
布隆过滤器是一种空间效率很高的随机数据结构,专门用来检测集合中是否存在特定的元素。布隆过滤器由一个长度为m比特的位数组与k个独立的哈希函数组成的数据结构。位数组初始化均为0,所有的哈希函数都可以分别把输入数据尽量均匀地散列。当要向布隆过滤器中插入一个元素时,该元素经过k个哈希函数计算产生k个哈希值,以哈希值作为位数组中的下标,将所有k个对应的比特值由0置为1。当要查询一个元素时,同样将其经过哈希函数计算产生哈希值,然后检查对应的k个比特值:如果有任意一个比特为0,表明该元素一定不在集合中;如果所有比特均为1,表明该元素有可能性在集合中。
由于可能出现哈希碰撞,不同元素计算的哈希值有可能一样,导致一个不存在的元素有可能对应的比特位为1,这就是所谓“假阳性”(false positive)。相对地,“假阴性”(false negative)在BF中是绝不会出现的。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。 所以,布隆过滤器认为不在的,一定不会在集合中;布隆过滤器认为在的,不一定存在集合中。
- 优点:节省空间:不需要存储数据本身,只需要存储数据对应hash比特位时间复杂度低:插入和查找的时间复杂度都为O(k),k为哈希函数的个数
- 缺点:存在假阳性:布隆过滤器判断存在,但可能出现元素实际上不在集合中的情况;误判率取决于哈希函数的个数,对于哈希函数的个数选择。不支持删除元素:如果一个元素被删除,但是却不能从布隆过滤器中删除,这也是存在假阳性的原因之一
缓存击穿
概念:某个热点key存在与Redis缓存中,在它过期的一瞬间,大量查询该key的请求转向数据库,造成瞬间数据库的数据达到洪峰,压力剧增。这时缓存像是被击穿了一样,称为缓存击穿现象。
危害:海量的请求转向数据库,可能导致数据库服务器宕机,整个应用系统不能正常工作。
解决方案:
- 使用互斥锁
--系统吞吐量下降 - 设置缓存数据永不过期
--这里的不过期可以是设置永不过期,或者是在数据快要过期时,有异步的线程去构建缓存的热点数据,达到永不过期的效果。 - 设置接口限流与熔断,降级
方案选择:
- 互斥锁方案,完全依赖于第一个线程进行缓存重建,其余线程查询新缓存中的数据。一旦构建缓存中过程出现问题,会出现线程死锁和线程池阻塞的风险。而且,这种方案会降低业务系统的吞吐量。
- 设置永不过期方案,可能会出现问题,实际上已经不存在热点 key 产生的一系列危害,会存在数据不一致的情况,同时代码复杂度会增大。
缓存雪崩
概念:大量的热点数据在同一时间过期,导致Redis缓存在同一时刻集体失效。同一时刻,对不同的过期key进行访问,引起雪崩的现象,造成瞬时数据库请求量大,压力骤增,数据库可能崩溃。
危害:海量的请求转向数据库,可能导致数据库服务器宕机,整个应用系统不能正常工作。
解决方案:
- 优化缓存过期时间。将热点数据的过期时间打散。雪崩的原因,在于同一时刻,大量热点数据同时过期,我们只要给热点数据设置过期时间加个随机值。
- 保持Redis缓存层的高可用性。使用Redis 哨兵模式或者Redis 集群部署方式,即便个别Redis 节点宕机,整个缓存层依然可以使用。
- 设置缓存数据永不过期。与防止缓存击穿的方案一致,这里的不过期可以是设置永不过期,或者是在数据快要过期时,有异步的线程去构建缓存的热点数据,达到永不过期的效果。
- 使用互斥锁重建缓存。根据 key 去缓存层查询数据,当缓存层为命中时,对 key 加锁,然后从存储层查询数据,将数据写入缓存层,最后释放锁。若其他线程发现获取锁失败,则让线程休眠一段时间后重试。
- 使用限流和降级组件。假如有一个资源不可用,可能会造成所有线程在获取这个资源时异常,造成整个系统不可用,可以降级补充热点数据,降级在高并发系统中是非常正常的。Hystrix是一款开源的“防雪崩工具”,它通过 熔断、降级、限流三个手段来降低雪崩发生后的损失。
Redis预热
概念:在刚启动的缓存系统中,如果缓存中没有任何数据,如果依靠用户请求的方式重建缓存数据,那么对数据库的压力非常大,而且系统的性能开销也是巨大的。
危害:海量的请求转向数据库,可能导致数据库服务器宕机,整个应用系统不能正常工作。
解决方案:
- 数据量不大时,项目启动时,自动进行初始化。
- 写个修复数据脚本,手动执行该脚本。
- 写个后门接口,提前预热对应的数据到缓存中。
Redis的大key
概念:首先大key不是key很大而是key对应的value值很大。一般而言如果String类型值大于10KB,Hash,Set,Zset,List类型的元素的个数大于5000个都可以称之为大key 。
危害:
- 客户端超时等待。由于Redis执行命令是单线程处理,然后在操作大key时会比较耗时,那么就会阻塞Redis,从客户端这一视角来看就是很久很久都没有响应。
- 引发网络阻塞。每次获取大key产生的流量较大,如果一个key的大小是1MB,每秒访问量为1000,那么每秒会产生1000MB的流量这对于普通千兆网卡是灾难的。
- 阻塞工作线程。如果使用del删除大key,会阻塞工作线程这样就没有办法处理后续的命令。
- 内存分布不均匀。集群模型在slot分片均匀的情况下,会出现数据和查询倾斜情况,部分有大key的Redis节点占用内存多,QPS比较大。
定位大key
- redi-cli –bigkeys
使用时注意事项:
最好在从节点上执行该命令或者在Redis实例业务压力的低峰阶段进行扫描查询,因为key很多时会很慢。
不足之处 ——这个方法只能返回每种类型中最大的那个bigkey,无法得到大小排到前N位的bigkey ——对于集合类型来说,这个方法只统计集合元素的多少,而不是实际占用的内存量。因为一个集合中元素个数多,并不一定占用内存就多。 - 使用SCAN命令查找大key
使用SCAN命令对数据库进行扫描。然后用TYPE命令获取返回的每一个key的类型对于String类型,可以直接使用STRLEN命令获取字符串长度,也就是占用的内存空间字节数对于集合类型来说可以使用MEMORY USAGE命令,查询有关键值对占用的内存空间。 - 使用RdbTools
使用第三方开源工具,可以解析Redis快照,找到其中的大key。
删除大key
- 分批次删除
使用SCAN扫描key,比如删除Hash,先取100字段删除删除再取 - 异步删除
在Redis4.0版本开始,可以采用异步删除法用unlink命令代替del删除这样Redis会将这个key放入到一个异步线程中进行删除,这样不会阻塞主线程 - 被动删除
前面两种都是主动删除,这一种是通过配置参数,当认为需要删除的时候就删除了
——lazyfree-lazy-eviction:表示当 Redis 运行内存超过 maxmeory 时,是否开启 lazy free 机制删除;
——lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除;
——lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除; ——slave-lazy-flush:针对 slave (从节点) 进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。 建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,这样就可以有效的提高主线程的执行效率。
如何避免大key
- 对大key进行拆分
将一个Big Key拆分为多个key-value这样的小Key,并确保每个key的成员数量或者大小在合理范围内,然后再进行存储,通过get不同的key或者使用mget批量获取。 - 对大key进行清理
对Redis中的大Key进行清理,从Redis中删除此类数据。Redis自4.0起提供了UNLINK命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的Key,通过UNLINK,你可以安全的删除大Key甚至特大Key。 - 监控Redis内存、网络带宽、超时等指标
通过监控系统并设置合理的Redis内存报警阈值来提醒我们此时可能有大Key正在产生,如:Redis内存使用率超过70%,Redis内存1小时内增长率超过20%等。 - 压缩value
使用序列化、压缩算法将key的大小控制在合理范围内,但是需要注意序列化、反序列化都会带来一定的消耗。如果压缩后,value还是很大,那么可以进一步对key进行拆分。
Redis的持久化方式
AOF 持久化
AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,在 redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重新构建整个数据集。Redis默认情况是不开启AOF的。重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。
AOF是执行完命令后才记录日志的。为什么不先记录日志再执行命令呢?这是因为Redis在向AOF记录日志时,不会先对这些命令进行语法检查,如果先记录日志再执行命令,日志中可能记录了错误的命令,Redis使用日志回复数据时,可能会出错。正是因为执行完命令后才记录日志,所以不会阻塞当前的写操作。但是会存在两个风险。
——执行完命令还没记录日志时,宕机了会导致数据丢失。
——AOF不会阻塞当前命令,但是可能会阻塞下一个操作。
这两个风险最好的解决方案是折中妙用AOF机制的三种写回策略 appendfsync:
- always,同步写回,每个子命令执行完,都立即将日志写回磁盘。
- everysec,每个命令执行完,只是先把日志写到AOF内存缓冲区,每隔一秒同步到磁盘。
- no:只是先把日志写到AOF内存缓冲区,有操作系统去决定何时写入磁盘。
always同步写回,可以基本保证数据不丢失,no策略则性能高但是数据可能会丢失,一般可以考虑折中选择everysec。
AOF优点
- 数据保证。我们可以设置fsync策略,一般默认是everysec,也可以设置每次写入追加,所以即使服务死掉了,也最多丢失一秒数据。
- 自动缩小。当aof文件大小到达一定程度的时候,后台会自动的去执行aof重写,此过程不会影响主进程,重写完成后,新的写入将会写到新的aof中,旧的就会被删除掉。但是此条如果拿出来对比rdb的话还是没有必要算成优点,只是官网显示成优点而已。
AOF缺点
- 性能相对较差。它的操作模式决定了它会对redis的性能有所损耗。
- 体积相对更大。尽管是将aof文件重写了,但是毕竟是操作过程和操作结果仍然有很大的差别,体积也毋庸置疑的更大。
- 恢复速度更慢。AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。
RDB持久化
RDB,就是把内存数据以快照的形式保存到磁盘上。和AOF相比,它记录的是某一时刻的数据,并不是操作。RDB持久化,是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。
RDB优点
- 体积更小。相同的数据量rdb数据比aof的小,因为rdb是紧凑型文件。
- 恢复更快。因为rdb是数据的快照,基本上就是数据的复制,不用重新读取再写入内存。
- 性能更高。父进程在保存rdb时候只需要fork一个子进程,无需父进程的进行其他io操作,也保证了服务器的性能。
RDB缺点
- 故障丢失。因为rdb是全量的,我们一般是使用shell脚本实现30分钟或者1小时或者每天对redis进行rdb备份,(注,也可以是用自带的策略),但是最少也要5分钟进行一次的备份,所以当服务死掉后,最少也要丢失5分钟的数据。
- 耐久性差。相对aof的异步策略来说,因为rdb的复制是全量的,即使是fork的子进程来进行备份,当数据量很大的时候对磁盘的消耗也是不可忽视的,尤其在访问量很高的时候,fork的时间也会延长,导致cpu吃紧,耐久性相对较差。
持久化方案选择
- 如果Redis只是用来做缓存服务器,比如数据库查询数据后缓存,那可以不用考虑持久化,因为缓存服务失效还能再从数据库获取恢复。
- 如果你要想提供很高的数据保障性,那么建议你同时使用两种持久化方式。
- 如果你可以接受灾难带来的几分钟的数据丢失,那么可以仅使用RDB。
- 如果只使用AOF,优先使用everysec的写回策略。
- 通常的设计思路是利用主从复制机制来弥补持久化时性能上的影响。即Master上RDB、AOF都不做,保证Master的读写性能,而Slave上则同时开启RDB和AOF(或4.0以上版本的混合持久化方式)来进行持久化,保证数据的安全性。
——推荐是两者均开启。
——如果对数据不敏感,可以选单独用 RDB。
——如果只是做纯内存缓存,可以都不用。
Redis的删除策略
Redis开辟了一个空间用来存放值的地址和其过期时间,删除策略是为了在内存和cpu之间找到一个平衡,一旦平衡被打破会降低redis的性能,引发服务器宕机和内存泄漏,一般来说,过期数据通常是在cpu闲暇之余被删除的。
Redis中的过期数据删除情况:redis服务器当中有很多的操作需要被执行,执行会导致CPU的工作大大的增加,当内存的空间还足够时,已被删除的数据的内存空间并未直接释放,而是对客户端的指令先执行。
Redis中的数据删除策略包括定时删除、惰性删除、定期删除。
定时删除(时间换空间)
定时删除时给key都设置一个过期的时间,当达到删除时间节点时,立即执行对key的删除。
优点:节约内存,到时间执行删除,释放内存空间;
缺点:CPU资源占用率过高,当其他任务在执行时,会导致两者同时进行,会影响两者的效率,redis服务器的响应时间和指令吞吐量收到影响;
总结:用处理器的性能换取存储空间(时间换空间),适用范围:小内存,强CPU。
惰性删除(空间换时间)
当要删除的数据到达给定时间时,先不进行删除操作;等待下一次访问时,若数据已过期则进行删除,客户端返回不存在,数据未过期,则返回数据。
优点:CPU的使用率大大降低,减轻其压力;
缺点:内存空间占用率较高,会存在长期占用内存的数据;
总结:使用存储空间换取处理器性能(空间换时间),适用范围:大内存,弱CPU。
定期删除(折中)
定时删除是对CPU和内存消耗取得一个折中方案,通过每隔一段时间执行一次删除过期key的操作,并且通过限制删除操作执行的时长和频率来减少删除操作对CPU造成的影响,但是限制删除操作执行的时长和频率需要合理地设置,否则可能会退化为成定时删除或惰性删除。
- Redis启动服务器初始化时,读取配置server.hz的值,默认为10
- 每秒钟执行server.hz次serverCron()
- activeExpireCycle()对每个expires[*]逐一进行检测,每次执行250ms/server.hz
- 对某个expires[*]检测时,随机挑选W个key检测
- 如果key超时,删除key;如果一轮中删除的key的数量>W*25%,循环该过程; 如果一轮中删除的key的数量≤W25%,检查下一个expires[],0-15循环W取值=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP属性值
- 参数current_db用于记录activeExpireCycle() 进入哪个expires[*] 执行
- 如果activeExpireCycle()执行时间到期,下次从current_db继续向下执行
总结:周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度,周期性抽查存储空间 (随机抽查,重点抽查)
优点:
- CPU性能占用设置有峰值,检测频度可自定义设置;
- 内存压力不是很大,长期占用内存的冷数据会被持续清理
内存占用 | CPU占用 | 特征 | |
定时删除 | 节约内存,无占用 | 不分时段占用CPU资源,频度高 | 时间换空间 |
惰性删除 | 内存占用严重 | 延时执行,CPU利用率高 | 空间换空间 |
定期删除 | 内存定期随机清理 | 每秒花费固定的CPU资源维护内存 | 随机抽查,重点抽查 |
在redis中会使用惰性删除和定期删除两种方式。
Redis的内存淘汰策略
逐出算法:Redis使用内存存储之前,通常会调用freeMemoryIfNeeded()检测内存是否充足。如果内存不满足新加入数据的最低存储要求,redis要临时删除一些数据为当前指令清理存储空间。当需要删除数据时,不会全库扫描,而是会随机从数据库中挑选出特定数目数据,然后按照淘汰算法,淘汰部分数据称为逐出算法。
然而,逐出过程不是100%清理足够的空间,需反复尝试,当处理完所有数据后依然不够,将出现如下错误信息:
影响数据逐出的相关配置
- maxmemory:redis可使用内存占物理内存的最大比例,默认为0,表示不限制redis使用内存。生产环境中根据需求设定,通常设置在50%以上;
- maxmemory-samples:每次选取待删除数据的个数,选取数据时并不会全库扫描,导致严重的性能消耗,降低读写性能。因此采用随机获取数据的方式作为待检测删除数据;
- maxmemory-policy:达到最大内存后的,对被挑选出来的数据进行删除的算法;
Redis提供8种数据淘汰策略
- volatile-lru:从已设置过期时间的数据集中挑选最近最少使⽤的数据淘汰。
- volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
- allkeys-lru:当内存不⾜以容纳新写⼊数据时,在键空间中,移除最近最少使⽤的 key(常用)。
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
- volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
- no-eviction:禁⽌驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错OOM。
- volatile-lfu:从已设置过期时间的数据集中挑选最不经常使⽤的数据淘汰。(4.0版本后增加)
- allkeys-lfu:当内存不⾜以容纳新写⼊数据时,在键空间中,移除最不经常使⽤的 key。(4.0版本后增加)