图解 Redis
String 是最基本的 key-value 结构,value是具体的值,不仅是字符串,也可以是数字(整数或浮点数),value 最多可以容纳的数据长度为512M。
应用场景
缓存对象:
SET user:1 '{"name":"koyal","age":"18"}'
MSET user:1:name koyal user:1:age 18 user:2:name xxx user:2:age 20
常规计数:计算访问次数、点赞、转发、库存数量等。
分布式锁:SET 命令有个 NX 参数可以实现 key不存在才插入,可以实现分布式锁。
共享 Session 信息:在分布式系统之下,各个服务器都会去同一个 Redis 获取相关的 Session 信息。
List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
应用场景
消息队列:消息队列在存取消息时,必须要满足三个需求,分别是消息保存、处理重复的消息和保证消息可靠性。
Hash 是一个键值对(key-value)集合,value的形式:value=[{field1,value1},...{fieldN,valueN}]
。Hash 特别适合存储对象。
应用场景
缓存对象:Hash类型的(key,field,value)的结构与对象的(对象id,属性,值)的结构相似,可以用来存储对象。
购物车:以用户 id 为 key ,商品 id 为 field,商品数量为 value,构成购物车的三要素。
Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
一个集合最多可以存储2^32-1个元素,可以交集、并集、差集等等。
应用场景
集合的无序、不可重复、支持并交差等特性,比较适合用来数据去重和保证数据唯一性,还可以用来统计多个集合的交集、错集和并集等(Set的差集、并集和交集的计算复杂度较高,在数据量大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞)
点赞:Set 类型可以保证一个用户只能点一个赞,key 为文章id,value 为用户id。
共同关注:Set 类型支持交集运算,可以用来计算共同关注的好友、公众号等。key 可以为用户id,value 为已关注的公众号id。
抽奖活动:Set 类型因为有去重功能,可以保证同一个用户不会中将两次。
Zset 类型(有序集合类型)相比 Set类型多个排序属性 score(分值),每个存储元素相当于有两个值组成的,一个是有序集合的元素值,一个是排序值。
应用场景
Zset 类型可以根据元素的权重来排序。
排行榜:学生成绩的排行榜,游戏积分排行榜等。
电话、姓名排序:使用有序集合中的ZRANGEBULEX
和ZREVRANGEBYLEX
可以帮助我们实现电话号码或姓名的排序。
BitMap,一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素,BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。
应用场景
BitMap 类型非常适合二值状态统计的场景,其只有0和1两种,在记录海量数据时,可以有效节省内存空间。
签到统计:可以用1表示签到,0表示未签到
判断用户登录态:可以用1表示登录,0表示未登录
Redis 2.8.9 版本新增的数据类型,是一种用于统计基数的数据集合类型,基数统计是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog的统计规则是基于概率完成的,不是非常准确,标准误算率是0.81%。
在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2^63个不同元素的基数,和元素越多就越消费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。
应用场景
GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。
应用场景
GEOADD
和GEORADIUS
这两个命令。Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。其支持消息的持久化、支持自动生成全局唯一ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。
应用场景:
Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件中,然后重启 Redis 的时候,先去读取这个文件中的命令,并且执行它,那么就恢复了缓存数据。
这种保存写操作命令到日志的持久化方式,就是 Redis 中的 AOF(Append Only File)持久化功能,只会记录写操作命令,读操作命令是不会被记录的,因为没有意义。
Redis 实现执行写操作后,才将该命令记录到 AOF 日志里的,这样做有两个好处:
AOF 持久化功能也有潜在风险:
AOF 将命令写回到硬盘的策略(在 redis.conf 配置文件中配置):
Always
:每次写操作命令执行完成后,同步 AOF 日志数据写会硬盘。Everysec
:每次写操作命令执行完成后,先将命令写入到 AOF 文件的内核缓冲区,然后每个一秒将缓冲区里的内容写回到硬盘。No
:每次写操作命令执行完成后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。Always 策略可以最大程度保证数据不丢失,由于它没执行一条写操作命令就同步 AOF 内容写回硬盘,所以是不可避免会影响主进程的性能。
No 策略相比于 Always 策略性能较好,但是操作系统写回硬盘的时机是不可预知的,如果 AOF 日志内容没有写回硬盘,一旦服务器宕机,就会丢失不定数量的数据。
Everysec 策略是一种折中的方式,避免了 Always 策略的性能开销,也比 No 策略更能避免数据丢失(如果上一秒的写操作命令日志没有写回到硬盘,发生了宕机,这一秒内的数据自然也会丢失。)
总结:
AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小也会越来越大,会带来性能问题,比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个回复的过程会很慢。
Redis 为了避免 AOF 文件越来越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 会启用 AOF 重写机制,来压缩 AOF 文件。
AOF 重写机制就是在重写时,读取当前数据库中的所有键值对,然后将每个键值对用一条命令记录到新的 AOF 文件,等到全部记录完后,就会将新的 AOF 文件替换调现有的 AOF 文件。
为什么重写 AOF 的时候,不直接复用现有的 AOF 文件,而是先写到新的 AOF文件在覆盖过去?
因为如果 AOF 文件重写过程中失败了,现有的 AOF 文件就会造成污染,可能无法用于恢复使用。
Redis 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
Redis 提供了两个命令来生成 RDB 文件,分别是 save
和bgsave
,区别在于是否在主线程中执行。
RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。
缺点:
在执行 bgsave 过程中,Redis 依然可以继续处理操作,也就是数据是能被修改的。关键的技术在于写时复制技术。
尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:
在 Redis 4.0 版本中,将 RDB 和 AOF 合体使用,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化,其工作在 AOF 日志重写过程。
使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。
但是这样的话文件可读性会变差,而且只有 Redis 4.0 之后的版本才支持混合持久化。
在使用 Always 策略的时候,主线程在执行命令后,会把数据写入到 AOF 日志文件,然后会调用fsync() 函数,将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。
当 AOF 日志写入了很多的大 Key,AOF 日志文件的大小会很大,那么很快就会触发 AOF 重写机制。
AOF 重写机制和 RDB 快照(bgsave命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。父进程对共享内存中的大 Key 进行修改,那么内核就会发生写时复制,会把物理内存复制一份,由于大 Key 占用的物理内存是比较大的,那么在复制物理内存这一过程中,也是比较耗时的,于是父进程(主线程)就会发生阻塞。
大 Key 除了会影响持久化之外,还会有以下的影响:
如何避免大 Key 呢?
最好在设计阶段,就把大 Key 拆分成一个一个小 Key。或者,定时检查 Redis 是否存在大 Key,如果该大 Key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 Key,因为该命令的删除过程是异步的,不会阻塞主线程。
Redis 是可以对 Key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
如何设置过期时间?
设置 key 过期时间的命令一共有 4 个
expire
:设置 key 在 n 秒后过期pexpire
:设置 key 在 n 毫秒后过期expireat
:设置 key 在某个时间戳(精确到秒)之后过期pexpireat
:设置 key 在某个时间戳(精确到毫秒)之后过期在设置字符串时,也可以同时对 key 设置过期时间,共有 3 中命令
set ex
:设置键值对的同时,同时指定过期时间(精确到秒)set px
:设置键值对的同时,同时指定过期时间(精确到毫秒)setex
:设置键值对的同时,同时指定过期时间(精确到秒)如何判定 key 已过期了?
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典中,过期字典中保存了数据库中所有 key 的过期时间。
字典实际上是哈希表,哈希表的最大好处就是让我们可以用O(1)的时间复杂度来快速查找,当查询一个 key 时,Redis 首先检查该 key 是否存在于过期时间中
过期删除策略有哪些?
常见的三种过期删除策略:
Redis 过期删除策略是什么?
Redis 选择 惰性删除和定期删除 这两种策略配合使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。
当 Redis 的运行内存已经超过 Redis 设置的最大内存之后,则会使用内存淘汰策略删除符合条件的 key,以此保障 Redis 高效的运行。
如何设置 Redis 最大运行内存?
在配置文件 redis.conf 中,可以通过参数maxmemory
来设定最大运行内存,只有在 Redis 的运行内存达到了我们设置的最大运行内存,才会触发内存淘汰策略。
不同的操作系统中,maxmemory 的默认值是不同的。
Redis 内存淘汰策略有哪些?
共八种,大体分为不进行数据淘汰和进行数据淘汰两类策略。
不进行数据淘汰的策略
进行数据淘汰的策略(细分为在设置了过期时间的数据中进行淘汰和在所有范围内进行淘汰)
在设置了过期时间的数据中进行淘汰
volatile-random
:随机淘汰设置了过期时间的任意键值volatile-ttl
:优先淘汰更早过期的键值volatile-lru
:(Redis 3.0 之前,默认的内存淘汰策略)淘汰所有设置了过期时间的键值中,最久未使用的键值volatile-lfu
:(Redis 4.0 之前新增的内存淘汰策略)淘汰所有设置了过期时间的键值中,最少使用的键值在所有范围内进行淘汰
allkeys-random
:随机淘汰任意键值allkeys-lru
:淘汰整个键值中最久未使用的键值allkeys-lfu
:(Redis 4.0 后新增的内存淘汰策略)淘汰整个键值中最少使用的键值LRU 算法和 LFU 算法有什么区别?
LRU 全称为 Least Recently Used 最近最少使用,会选择淘汰最近最少使用的数据。
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机去5个值(可配置),然后淘汰最久没有使用的那个。
Redis 实现的 LRU 算法的优点:
LRU 的问题,无法解决缓存污染问题。
LFU 全称为 Least Frequently Used 最仅最不常用,根据数据访问次数来淘汰数据,核心思想是:“如果数据过去被访问多次,那么将来被访问的频率也更高”。
LFU 算法相比于 LRU 算法的实现,多记录了数据的访问频次的信息。
Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。