Redis复习笔记-进阶篇

Redis复习笔记-进阶篇

发布订阅模式

订阅频道

消息的生产者和消费者是不同的客户端,在Redis中通过channel(频道)模型进行关联。订阅者可以订阅多个channel,消息的发布者可以给指定的channel发布消息,只要有消息到达了channnel,所有订阅了这个channel的订阅者都会收到这条消息。

Redis复习笔记-进阶篇

subscribe channel-1 channel-2 channel-3
//一次订阅多个频道

publish channel-1 2673
//发布者可以向指定频道发布消息(并不支持一次向多个频道发送消息,可以在业务代码中添加多条)

unsubscribe channel-1
//取消订阅(不能在订阅状态下使用)

按规则(Pattern)订阅频道:

支持 ?和 * 占位符。?代表一个字符,* 代表0个或者多个字符。

psubscribe *sport  
//适配所有以sport结尾的频道名称

一般来说,考虑到性能和持久化因素,不建议使用Redis的发布订阅功能来实现MQ。Redis的一些内部机制用到了发布订阅功能。


Redis事务

Redis单个命令是原子性的,但是为了确保多个命令作为一个不可分割的处理序列,就需要使用Redis事务。Redis事务具有三个特点:

  1. 按照进入队列的顺序执行
  2. 不会受到其他客户端的请求的影响
  3. 事务不能嵌套,多个multi命令效果一样
multi	//开启事务
exec	//执行事务,在exec没有被调用时,所有队列中的命令都不会被执行
discard //取消事务,清空任务队列,放弃执行
watch	//监视

为了防止事务过程中某个key的值被其他客户端请求修改,带来非预期的结果,在Redis中就提供了一个watch命令。用于多个客户端更新变量的时候,跟原值作比较,只有它没有被其他线程修改的情况下,才更新为新的值。它可以为Redis事务提供CAS乐观锁行为。

可以使用watch命令监视一个或者多个key,如果事务开启之后,至少有一个被监视的key在exec执行之前被修改了,那么整个事务都会被取消(key提前过期除外)。可以使用unwatch进行取消。

在multi命令之前先对要watch的key进行监视,然后开启事务,exec命令开始执行,如果返回nil,则代表监视的值已经被其他客户端进行了修改,事务取消。

事务可能遇到的问题

第一种,在执行exec之前发生错误:

一般是命令存在语法错误,编译器出错。事务会被拒绝运行,也就是队列中所有的命令都不会得到执行。

第二种,在执行exec之后发生错误:

可能是类型出错,但是只有这条命令没有被执行,对于其余命令并没有影响。

为什么没有回滚机制?

无论是哪种错误都不应该发生在生产环境内,回滚也不能解决代码的问题。


Lua 脚本

一种轻量级脚本语言,使用C来编写,跟数据的存储过程有点类似。

优势:

  • 一次发送多条命令,减少网络开销
  • Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性
  • 对于复杂的组合命令,可以放在文件中,实现命令复用

调用Lua脚本:

Redis复习笔记-进阶篇

在Lua脚本中调用Redis命令:

redis.call(command,key,[param1 , param2 ...])
//command 是命令
//key 是被操作的键
//param1,param2 代表给key的参数

例子:执行 set yang 21 就应该编写命令:eval "return redis.call('set','yang','21') 0"

通常我们会把Lua脚本放在文件里面,然后执行这个文件。

Lua脚本文件:

Redis复习笔记-进阶篇

缓存Lua脚本:

Lua脚本如果比较长的时候,如果每次调用脚本都需要将整个脚本传给Redis服务端,就会产生较大的网络开销,为了解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以直接通过摘要码来执行Lua脚本。

缓存方式:

script load "return 'Hello World'"
//使用script load命令在服务端缓存lua脚本并生成一个摘要码

evalsha "437fda89fwe89f8s982323jh1283s382" 0
//通过摘要码执行缓存的脚本

脚本超时:

由于Redis的指令执行本身是单线程的,如果执行Lua脚本超时或者进入了死循环,就没有办法继续提供服务了。

每个脚本默认有一个超时时间为5s,与配置文件中配置项:lua-time-limit 5000有关,超过5s,其他客户端的命令便不会等待,直接返回"BUSY"错误。当然也不可以一直拒绝其他客户端的命令。Redis还提供了命令可以使用:

script kill
//终止脚本的执行,并不是所有脚本都可以kill,那些对Redis的数据进行了修改(SET,DEL等)是不可以通过这种方式来停止脚本运行的。如果使用了这种方式,则会返回UNKILLABLE错误。遇到这种情况,只能通过shutdown nosave命令,该操作不会进行持久化操作,意味着发生在上一次快照后的数据库修改全部都会被丢失。

为什么这么设计?为什么包含修改的脚本不能中断?

因为要保证脚本运行的原子性,如果脚本执行了一部分就被终止,那就违背了脚本原子性的目标。


为什么Redis这么快?

根据实际测试,Redis的QPS在十万左右,在高性能的服务器上性能还能更强。

Redis这么快的原因总结:

  • 纯内存结构,采用了hashtable实现的KV结构的内存数据库,时间复杂度为O(1)

  • 请求处理单线程:单线程指的是处理客户端请求是单线程的,可以将它称作主线程。4.0版本后还引入了一些线程处理其他的事情,比如清理脏数据,无用连接的释放,大key的删除等。单线程的好处是:1. 没有创建线程,销毁线程带来的性能损耗 2. 避免了上下文切换导致的CPU消耗 3. 避免了线程之间带来的竞争问题。

  • 多路复用机制:使用了多路复用处理并发连接

在Redis中单线程已经够用了,Redis的瓶颈不在CPU上,更有可能是在内存或者网络带宽上。也因为如此,不要在生产环境上运行长命令:比如:keys *,flushall,flushdb等,否则会导致请求被阻塞。

具体的多路复用以及单线程相关的底层知识点就不再这里叙述了,体量太大了,理解即可。


内存回收

过期策略

  • 立即过期(主动淘汰):每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除,该策略可以立即清除过期的数据,对内存很友好,但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期(被动淘汰):只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存不友好,极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用了大量内存。
  • 定期过期 :每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折衷方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

如果所有的key都没有设置过期属性,Redis内存满了怎么办?

淘汰策略

在内存使用达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。

  • 最大内存设置:redis.conf 参数配置:#maxmemory<bytes> 如果不设置maxmemory或者设置为0,32位系统最多使用3GB内存,64位系统不限制内存。也可以通过config动态修改:config set maxmemory 2GB
  • 淘汰策略:通过配置 maxmemory-policy 进行决定使用哪种策略:

Redis复习笔记-进阶篇

Redis复习笔记-进阶篇

**LRU : **Least Recently Used:最近最少使用,判断最近被使用的时间,目前最远的数据优先被淘汰。

LFU :Least Frequently Used: 最不常用,按照使用频率删除,4.0版本增强。

random : 随机删除。

config set maxmemory-policy xxxxxx-xxx
//动态修改淘汰策略

如果没有设置ttl或者没有符合前提条件的key被淘汰,那么volatile-lru,volatile-random,volatile-ttl相当于noeviction(不做内存回收)

建议使用volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的key。

LRU 淘汰原理

LRU是一个很常见的算法,比如InnoDB的Buffer Pool 也用到了LRU。

**传统的LRU : **通过链表+HashMap实现,设置链表长度,如果新增或者被访问,就移动到头节点,超过链表长度,末尾的节点被删除。

Redis复习笔记-进阶篇

**Redis LRU : **对传统的LRU算法进行了改良,通过随机采用来调整算法的精度。如果淘汰策略使用的是LRU,那么就会从配置文件中读取配置的采样个数:maxmemory_samples(默认是5个),然后随机从数据库中选择读到个数个key,淘汰其中热度最低的key对应的缓存数据。所以采样数配置的越大,就越能精确的查找到待淘汰的缓存数据,但是也消耗更多的CPU计算,执行效率降低。

如何找出热度最低的数据?

Redis中所有对象结构都有一个lru字段,且使用了unsigned 的低24位,这个字段用来记录对象的热度。对象被创建时会记录lru值。在被访问的时候会更新lru的值。但并不是获取系统当前的时间戳,而是设置为全局变量server.lrulock的值。

Redis复习笔记-进阶篇

server.lrulock的值是怎么来的?

Redis中有个定时处理的函数serverCron,默认每100毫秒调用函数updateCachedTime更新一次全局变量sever.lrulock的值,它记录的是当前unix的时间戳。

为什么不获取精确的时间而是放在全局变量中呢?

这样函数查询key调用lookupKey中更新数据的lru热度值时,就不用每次调用系统时间函数Time,可以调高执行效率。

评估热度:

Redis复习笔记-进阶篇

函数评估指定对象的lru热度,方法就是对象的lru的值和全局的server.lrulock的差值越大(越久没有得到更新),该对象热度越低。server.lrulock只有24位,按秒为单位来表示才能存储194天,当超过24bit能表示的最大值时,就会从头开始计算。在这种情况下,可能会出现对象的lru大于server.lrulock情况,那么就将两个相加而不相减来求最久的key。

为什么不使用传统的LRU实现呢?

需要额外的数据结构,消耗资源。而Redis LRU算法在采样数为10的时候,已经能接近传统的LRU算法了。

除了消耗资源之外,传统的LRU还存在什么问题?

因为传统LRU,没有做随机采样,所以有可能访问频率高但是最近一次访问没有访问频率低的最后一次访问时间近。而结果就是将访问频率高的key删除了。

LFU 淘汰原理

Redis复习笔记-进阶篇

当Redis使用LFU淘汰策略时,原本用于记录LRU热度的字段LRU_BITS这24 bits将被分为两个部分:

  • 高16位用来记录访问时间(单位为分钟,ldt: last decrement time)
  • 低8位用来记录访问频率,简称counter(logc: logistic counter)

counter 是用基于概率的对数计数器实现的,8位可以表示百万次的访问频率。对象被读写的时候,lfu的值会被更新。

Redis复习笔记-进阶篇

这里增长并不是访问一次就加一,增长的速率由一个参数决定,lfu-log-factor 越大,counter的增长就越慢。而这个参数是通过配置文件来决定的:

# lfu-log-factor 10

如果一段时间热度高,就一直保持这个热度也是不行的。体现不了整体频率,所以,没有访问的时候,计数器需要递减。减少的值由衰减因子:lfu-decay-time(分钟)来控制,如果值为1,N分钟没有访问,计数器就需要减少N。衰减因子越大,衰减就越满。可以通过配置项进行配置衰减因子大小:

# lfu-decay-time 1

持久化机制

RDB 快照 (Redis DataBase)

RDB是Redis默认的持久化方案(如果开启了AOF,优先使用AOF)。当满足一定的条件的时候,会把当前内存种的数据写入磁盘,生成一个快照文件dump.rdb。Redis重启的时候会加载这个文件来恢复数据。

什么时候写入rdb文件?

  • 自动触发: 配置规则触发,在redis.conf,SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。

    save 900 1 #900秒内至少有一个key被修改
    save 300 10 #300秒内至少有10个key被修改
    save 60 10000 #60秒内至少有10000个key被修改
    
    #注意以上三个规则不冲突,同时生效
    

    如果不需要使用rdb方案,就将save注释或者配置成空字符串""。

    使用lastsave命令可以查看最近一次成功生成快照的时间。

    除了配置触发生成RDB,还有两种自动的触发方式:

    1. shutdown 触发,保证服务器正常关闭
    2. flushall ,rdb文件是空的,没什么意义

Redis复习笔记-进阶篇

  • 手动触发:如果我们需要重启服务器服务或者迁移数据,这个时候就需要手动触发RDB快照保存,Redis提供了两条命令:
    1. **save : ** 在生成快照的时候会阻塞服务器,Redis不能处理其他命令,如果内存中的数据比较多,会造成Redis长时间的阻塞。生产环境不建议使用这个命令。
    2. **bgsave : ** Redis会在后台异步进行快照操作,同时还可以相应客户端请求。具体操作是:Redis进程执行fork操作创建子进程(copy-on-write),RDB持久化过程由子进程负责,完成后自动结束。不会记录fork之后产生的数据,阻塞只发生在fork阶段,一般时间很短。

优势和劣势

优势:

  1. RDB是一个十分紧凑的文件,它保存了redis在某个时间点上的数据集。这种文件十分适合用于进行备份和灾难恢复。
  2. 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有的保存工作,主进程不需要进行任何磁盘IO操作。
  3. RDB在恢复大数据集时的速度比AOF恢复的速度要快。

**劣势: **

  1. RDB方式数据没办法做到实时持久化/秒级持久化。每次bgsave时运行都要执行fork操作创建子进程,频繁执行成本过高。
  2. 在一定间隔时间做一次备份,所以如果redis意外宕机的话,就会丢失最后一次快照之后的所有修改。

如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化!


AOF (Append Only File)

Redis默认不开启。AOF采用日志的形式来记录每个写操作,并追加到文件中。开启以后,执行更改Redis数据的命令时,就会把命令写入到AOF文件中。Redis重启时会根据日志文件的内容把记录的指令从前到后执行一次以完成对数据的恢复工作。

**AOF配置 : **

# 开关
appendonly no
# 文件名
appendfilename "appendonly.aof"

Redis复习笔记-进阶篇

数据都是实时持久化到磁盘吗?

由于操作系统的缓存机制,AOF数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。什么时候把缓冲区的内容写入到AOF文件?

参数 说明
appendfsync everysec AOF 持久化策略(硬盘缓存到磁盘),默认everysec
no 表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不太安全
always 表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低。
everysec 表示每秒执行一次fsync,可能会丢失这1s的数据,兼顾安全行和效率。(通常选择这个)

文件越来越大,怎么办?

为了解决这个问题,Redis新增了重写机制。当AOF文件大小超过了所设定的阈值,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。

也可以使用命令bgrewriteaof来重写,AOF文件重写并不是对原文件进行重新整理,而是直接读取服务现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。

# 重写触发机制
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

Redis复习笔记-进阶篇

重写过程中,AOF文件被更改了怎么办?

Redis复习笔记-进阶篇

另外,在配置文件中有两个与AOF相关的参数:

Redis复习笔记-进阶篇

优势和劣势

**优势: **

  1. AOF持久化的方法提供了很多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也就丢失1s的数据。

**劣势: **

  1. 对于具有相同数据的Redis,AOF文件通常会比RDF文件体积更大(RDB存的时数据快照)。
  2. 虽然AOF提供了多种同步频率,默认情况下,每秒同步一次的频率也需要比较高的性能。在高并发情况下,RDB比AOF具有更好的性能保证。
上一篇:Redis从入门到精通-Redis-持久化之AOF


下一篇:【3. Redis高级特性】