前言
Redis 一般都用来当作缓存使用,大部分 key 都会设置过期时间。
Redis 中有一个过期字典
用来保存所有 key 的过期时间。当有查询请求进来会检查该 key 是否存在于过期字典中:
- 不在: 正常读取键值。
- 在:获取并判断该 key 的过期时间是否过期。若是已经过期则结束,若是还未过期则正常键值。
Redis 执行过程
:
- 客户端输入指令。
- 指令转为 redis 协议,发送给服务端。
- 服务端接收到协议后将其转为命令。
- 判断命令、授权信息。
- 执行命令并记录相关信息和数据统计。
基本数据类型及其应用场景
string - 字符串
-
应用场景
- key-value 缓存,一个键最大能存储512MB。
- 数值计算。
-
内部数据类型
- int:数字。
- embstr:字符串。
- raw:当字符串长度大于44字节时,会变为 raw 类型存储。
-
示例
127.0.0.1:6379> set name xyc_neil OK 127.0.0.1:6379> get name "xyc_neil"
hash - 哈希
-
应用场景
- 用户信息
- 商品详情
-
内部数据类型
- hashmap
- 存储
- 先将键值进行 hash 计算,得到存储键值对应的数组索引,再根据数组索引进行数据存储,有小概率会出现完全不相同的键值进行 hash 计算后,得到相同的 hash 值,这种情况称为 hash 冲突。
- hash 冲突一般通过链表的形式解决,相同的 hash 值会对应一个链表结构,每次 hash 冲突时,就把新元素插入链表的尾部。
- 查询
- 通过 hash 获得数组的索引值,根据索引值找到对应的元素。
- 判断元素和查找的键值是否相等,相等则成功返回数据,否则需要查看 next 指针是否还有对应其他元素,如果没有,则返回 null,如果有 nest 指针的话,重复此步骤。
- 存储
- hashmap
-
示例:
127.0.0.1:6379> hset userId name xyc_neil (integer) 1 127.0.0.1:6379> hgetall userId 1) "name" 2) "xyc_neil"
list - 列表
-
应用场景
- 消息队列
- 限流器
-
内部数据类型
- quicklist:快速列表是一个双向链表,双向链表中的每个节点是一个 ziplist(压缩表),在插入方向的头节点判断是否能插入,不能的话就新增一个 quicklistNode(快速列表节点)。
-
示例
127.0.0.1:6379> lpush list xyc_neil (integer) 1 127.0.0.1:6379> lpop list "xyc_neil"
set - 集合
-
应用场景
- 微博关注
- 抽奖信息
-
内部数据类型
- intset:元素都是整数的时候
- hashtable
- 元素超过一定个数的时候,默认是512个
- 元素非整数的时候
-
示例
127.0.0.1:6379> sadd set xyc_neil (integer) 1 127.0.0.1:6379> spop set "xyc_neil"
zset - 有序集合
-
应用场景
- 成绩排名
- 热词排序
-
内部数据类型
- ziplist
- 保存的元素个数小于128个
- 元素的长度都小于64字节
- skiplist
- ziplist
-
示例
127.0.0.1:6379> zadd zset 100 xyc_neil 60 zhangsan 80 lisi (integer) 3 127.0.0.1:6379> zrange zset 0 -1 withscores 1) "zhangsan" 2) "60" 3) "lisi" 4) "80" 5) "xyc_neil" 6) "100"
高性能
- 完全基于内存。
- 整个结构类似于
HashMap
,查找和操作复杂度为O(1)
。 - 读写模型是单线程的,避免了多线程的上下文切换和线程竞争造成的开销。
- 采用多路复用的高效非阻塞IO模型
数据一致性
- 场景:并发更新导致数据库与缓存不一致。
- 解决:
延迟双删
:先删除缓存,更再新数据库,过一会再删除一遍缓存。
过期策略
定时删除
- 概念:在给 key 设置过期时间的同时,设置一个定时器,当 key 过期了,定时器马上把该 key 删除。
- 优点:内存空间回收非常快。
- 缺点:对 CPU 不友好。
惰性删除
- 概念:当有请求操作 key 的时候,才检查这个 key 是否过期,如果过期则删除(可以配置异步删除或者同步删除),否则返回对应的数据。
- 优点:对 CPU 比较友好。
- 缺点:内存空间浪费。
定期删除
- 概念:每隔一段时间随机取出一些设置过期时间的 key 进行检查和删除。如果本轮已过期数量过多,则继续重复步骤,如果已过期数量较少,则停止。定期删除循环流程的时间上限25ms。
- 定期删除是定时删除和惰性删除的一个折中方案,减少CPU占用、内存也不会浪费太多。
Redis 默认采用定期删除 + 惰性删除。
淘汰策略
不淘汰数据
- no-enviction:禁止驱逐数据。当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,采用
no-enviction
策略可以保证数据不被丢失。
淘汰数据
-
在设置过期时间的数据中进行淘汰
- volatile-random:随机淘汰设置了过期时间的任意键值。
- volatile-ttl:优先淘汰更早过期的键值。
- volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值。
- volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值。
-
在所有数据中进行淘汰
- allkeys-random:随机淘汰任意键值。
- allkeys-lru:淘汰整个键值中最久未使用的键值。
- allkeys-lfu:淘汰整个键值中最少使用的键值。
Redis3.0之前默认采用volatile-lru,Redis3.0之后默认采用no-enviction。
持久化
RDB 持久化
-
概念:快照方式,将某一个时刻的内存数据,以二进制的方式写入磁盘。
-
触发方式:
- 自动触发:
- 配置 sava m n,以下是默认开启:
save 900 1
-- 900s内存在1个写操作。save 300 10
-- 300s内存在10个写操作。save 60 10000
-- 60s内存在10000个写操作。
- 主从同步的时候会自动触发 bgsave
- 配置 sava m n,以下是默认开启:
- 手动触发:
- save:阻塞当前 Redis,直到 RDB 持久化过程完成为止,若内存比较大则会造成长时间阻塞。
- bgsave:Redis 进程执行 fork 操作创建子进程,由子进程完成持久化,阻塞时间很短。
- 自动触发:
-
优点:
- 使用单独的子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能。
- RDB 文件存储的是压缩的二进制文件,占用内存更小,更适合作为备份文件、灾难恢复,同时 RDB 文件的加载速度远超于 AOF 文件。
-
缺点:
- RDB 是间隔一段时间进行持久化,如果 Redis 服务意外挂掉了,则会丢失一段时间内的数据。
- 每次都要创建子进程,频繁创建成本过高,甚至可能导致 Redis 短暂的停止服务,备份时需要将数据写入到一个临时文件,内存是原来的两倍。
- RDB 文件可读性很差。
AOF 持久化
-
概念:文件追加方式,记录所有的操作命令,并以文本的形式追加到文件中。
-
触发方式:
- 自动触发:
- always:每条命令都写入磁盘,最多丢失一条数据。
- everysec:每秒钟写入一次磁盘,最多丢失一秒的数据。
- no:不设置写入磁盘规则,采用默认30s写入一次磁盘。
- 手动触发:
- 执行bgrewriteaof命令。
- 自动触发:
-
优点:
- 保存的数据更加完整,AOF 普遍采用每秒保存一次的策略,即使发生了意外情况,最多只会丢失1s的数据。
- AOF 采用的是命令追加的写入方式,所以不会出现文件损坏的问题。
- AOF 文件可读性很好,它是把所有Redis键值操作命令,以文件的方式写入了磁盘。即使不小心使用 flushall 命令删除了所有键值信息,只要在 AOF 文件删除最后的 flushall 命令,重启Redis即可恢复之前误删的数据。
-
缺点:
- 对于相同的数据集来说,AOF 文件要大于 RDB 文件,数据恢复时也需要重新执行指令,在重启时恢复数据的时间往往会慢很多。
- 高并发场景下,AOF 性能会比较差。
-
AOF 机制重写
:当 AOF 文件比上一次重写时的文件大小增长100%并且文件大小不小于64MB时会对整个 AOF 文件进行重写。基于copy-on-write
(写时复制),全量遍历内存中数据,然后逐个序列到 AOF 文件中。重写过程中,对于新的变更操作将仍然被写入到原AOF
文件中,同时这些新的变更操作也会被 收集起来,当内存数据被全部写入到新的 AOF 文件之后,收集的新的变更操作也将会一并追加到新的 AOF 文件中,此后将会重命名新的 AOF 文件为 appendonly.aof, 此后所有的操作都将被写入新的 AOF 文件。如果在重写过程中,出现故障,将不会影响原 AOF 文件的正常工作,可以通过 bgrewriteaof 指令人工干预。
混合型持久化
-
概念:Redis4.x之后新增的方式,结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的持久化数据以 RDB 的形式写入文件的开头,再将后续的操作命令以AOF的格式存入文件,这样既能保证 Redis 重启时的速度,又能降低数据丢失的风险。
-
优点:
- 结合了 RDB 和 AOF 持久化的优点,开头为RDB格式,使得Redis可以快速重启,同时结合AOF的优点,降低了大量数据丢失的风险。
-
缺点:
- AOF 文件添加了 RDB 格式的内容,使得 AOF 文件的可读性变差。
- 兼容性差,如果开启混合存储,那么此混合存储文件,就不能用在Redis4.x之前的版本。
缓存穿透
- 场景:大量用户访问的数据,既不在缓存中,也不在数据库中。
- 业务误操作:缓存中的数据和数据库中的数据都被误删除。
- 黑客恶意攻击:故意大量访问某些读取不存在数据的业务。
- 解决:
- 做 IP 限流与黑名单。
- 缓存空值或者默认值。
- 使用布隆过滤器。
- 只能添加不能删除,想删除的话可以给该 key 设置标识,查到该标识就认为是没有缓存。
- bitmap 结构有极少概率出现不存在的 key 也命中。
缓存击穿
- 场景:热点 key 过期,DB 有数据,大量请求同时到达 DB。
- 解决:
- 热点 key 不设置过期时间,由后台更新缓存。
- 获取数据串行化:
- 查找缓存。
- 缓存命中就返回,否则抢锁。
- 抢锁不成功的 sleep,抢到锁的操作 DB,并写入缓存。
- sleep 后的请求重复以上步骤。
缓存雪崩
- 场景:大量热点 key 同时过期,DB 有数据,大量请求同时到达 DB。
- 解决:
- 热点 key 的过期时间随机错开。
- 热点 key 不设置过期时间,由后台更新缓存。
- 获取数据串行化:
- 查找缓存。
- 缓存命中就返回,否则抢锁。
- 抢锁不成功的 sleep,抢到锁的操作 DB,并写入缓存。
- sleep 后的请求重复以上步骤。
缓存预热
- 场景:缓存预热并不是一个问题,而是使用缓存时的一个优化方案,它可以提高前台用户的使用体验。缓存预热指的是在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,以节约用户的等待时间。
- 解决:
- 把需要缓存的方法写在系统初始化的方法中,这样系统在启动的时候就会自动的加载数据并缓存数据。
- 把需要缓存的方法挂载到某个接口上,手动触发缓存预热。
- 设置定时任务,定时自动进行缓存预热。