Redis(二)原理

一、发布订阅模式

1、基本命令

  • 订阅频道:可以一次订阅多个,subscribe channel-1 channel-2 channel-3
  • 向指定频道发布消息:publish channel-1 2673
  • 取消订阅:unsubscribe channel-1
  • 按规则(pattern)订阅频道:psubscribe topic*,*代表通配符,topic * 代表所有topic开头的频道。
  • 发布消息:public channel message 给channel发送消息message

二、事务

1、事务特性

  1. 按照进入队列的顺序执行
  2. 不会受到其他客户端的请求的影响
  3. 事务不能嵌套,多个multi命令效果一样

2、事务命令

  • multi:开启事务,multi执行后,可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中。
  • exec:执行事务,exec执行后,所有队列中的命令才会被执行,如果没有exec命令,所有的命令都不会被执行
  • discard:取消事务,discard执行后,队列会被清空,放弃执行
  • watch:监视key,如果被监视的key在exec之前被修改,事务会取消。watch采用乐观锁机制,当有多个客户端修改key的value时,watch会先与原值作比较,只有没被修改的情况下,才会更新。

3、事务问题

开启事务后,通常而言,当某一个语句发生错误时,所有命令都将失效,数据回滚。
在redis中,命令的错误可以分为两种:

  1. 编译时错误:当输入的命令无法通过编译时,所有的命令都将失效
  2. 运行时错误:当输入的命令通过编译,但是执行时出错,就仅有出错的及以后的命令失效,在此之前的命令依旧会被执行,不回滚

所以只有命令错误,才会回滚,所以其实redis事务无法保证原子性

三、Lua脚本

1、优点

  1. 批量执行命令
  2. 原子性
  3. 操作集合的复用

2、命令

一)在Redis中执行Lua脚本

eval lua-script key-num [key1 key2 key3……] [value1 value2 value3……]

  • eval代表执行lua语言的命令
  • lua-script代表Lua语言脚本内容
  • key-num表示参数中有多少个key,需要注意的是Redis中key是从1开始的,如果没有key的参数,那么写0。
  • [key1 key2 key3……]是key作为参数传递给Lua语言,也可以不填,但是需要和key-num的个数对应起来
  • [value1 value2 value3……]这些参数传递给Lua语言,它们是可填可不填的。

示例:eval “return ‘hello world’” 0

二)在Lua脚本中执行Redis命令

redis.call(command,key[param1,param2……])

  • command:命令,包括set,get,del等
  • key:被操作的键
  • param:代表给key的参数

示例1:eval “return redis.call(‘set’,‘hello’,‘world’)”
示例2:eval “return redis.call(‘set’,KEYS[1],ARGV[1])” 1 hello,world

这两个示例的结果是一样的,示例1写死传输值,示例2采用传参的方式,如果有更多的参数,可以继续增加。

三)Lua脚本文件

1)基础命令

在客户端中直接写Lua脚本很不方便,因此通常会把Lua脚本写在文件里,然后执行这个文件

  1. 创建Lua脚本文件:vim test.lua
  2. 创建脚本内容:redis.call(command,key[param1,param2……])
  3. 调用脚本文件(无参数):redis-cli --eval test.lua 0
  4. 调用脚本文件(有参数):redis-cli --eval test.lua [key1] [key2] , [argv1] [argv2] 注意,在key与key之间用空格分开,argv和argv之间用空格分开,在分割key和argv的逗号前后都有空格

2)案例 IP限流

每个用户在X秒内只能访问Y次

//计数+1,若key不存在会自动创建
local num = redis.call('incr',KEYS[1])
//如果是第一次访问,用第一个参数设置过期时间
if tonumber(num)==1 then
		redis.call('expire',KEYS[1],ARGV[1])
		return 1
	//如果不是第一次访问,跟第二个参数比较是否超出限制
	elseif tonumber(num)>tonumber(ARGV[2]) then
		//超限
		return 0
	else
		//没超限
		return 1
end

10秒钟限制访问20次
redis-cli --eval test.lua ip:127.0.0.1 , 10 20

四)缓存Lua脚本

1)基础命令

调用大量的脚本文件将带来很大负担,因此要缓存脚本内容。redis可以缓存Lua脚本并生成摘要码,后面可以通过摘要码来执行。

  1. 生成摘要值:script load 脚本内容
  2. 缓存脚本内容:evalsha “摘要值” 参数个数 参数1 参数2……

2)案例 自乘

  1. redis有incr这样的自增函数,但是无法直接计算乘积,但可以使用Lua脚本实现这个功能
local num=redis.call("get",KEYS[1])
if num==false then
	num=0
else
	num=tonumber(num)
end
num=num*tonumber(ARGV[1])
redis.call("set",KEYS[1],num)
return num
  1. 需要将这段Lua脚本变成单行,就需要在语句之间增加分号:

local num=redis.call(“get”,KEYS[1]); if num==false then num=0 else num=tonumber(num) end;num=num*tonumber(ARGV[1]);redis.call(“set”,KEYS[1],num);return num

  1. 接下来将这段命令执行

script load ‘上述命令’

  1. 得到摘要值,然后执行摘要值,获得key为‘number’的value乘以5的值

evalsha “摘要值” 1 number 5

五)脚本超时

Redis的指令执行是单线程的,如果执行一个比较长的Lua脚本,就会导致其他命令全部阻塞,当然这也是Lua脚本原子性的保证,但如果这个Lua脚本执行超时或者进入死循环该如何解决呢?

  1. 脚本有超时时间设置,默认为5秒,当这个脚本执行超过5秒,其他客户端的命令将不会阻塞等待,而是直接返回busy错误,但这依旧会阻止其他客户端执行命令。
  2. 如果执行的Lua脚本没有对Redis值的更改,可以直接执行:script kill命令来终止脚本的运行,使其他命令能正常执行。
  3. 如果执行的Lua脚本对Redis值进行过更改,script kill命令就无法终止脚本运行,因为Lua脚本具有原子性,如果进行数值修正后脚本终止就违反了其原子性。因此,想要结束这个脚本,只能执行shutdown nosave命令。
  4. 正常的关机操作命令是shutdown,shutdown nosave 无保存关机shutdown 正常关机的区别是,shutdown nosave不会进行持久化操作,这意味着在上一次快照后的数据库修改都会消失。

四、执行速度

Redis的执行速度可以达到QPS 10万的级别,非常迅速,那么是什么机制让它的速度如此惊人?

1、原因

  1. 纯内存KV
  2. 单线程命令执行
  3. 同步非阻塞I/O——多路复用

一)纯内存的优点

内存读取速度快,纯内存的KV数据库时间复杂度为O(1)

二)单线程的优点

  1. 没有创建线程,销毁线程的消耗
  2. 避免了上下文切换导致的CPU消耗
  3. 避免了线程之间的竞争问题

三)多路复用

1)传统I/O的同步阻塞

当应用程序执行read系统调用读取文件描述符(FD)时,如果这块数据已经存在于用户进程的页内存中,就直接从页内存中读取数据,如果数据不存在则先将数据从磁盘加载到内核缓冲区中,再从内核缓冲区拷贝到用户进程的页内存中。(两次拷贝,两次上下文切换)
当使用read/write对某个文件描述符进行读写时,如果当前FD不可读,系统就不会对其他的操作做出响应,从硬件设备复制数据到内核缓冲区是阻塞的,从内核缓冲区拷贝到用户空间也是阻塞的,直到复制完成,内核返回结果,用户进程才接触阻塞状态。

2)多路复用I/O

  • I/O指的是网路I/O
  • 多路:指的是多个TCP连接(stocket或channel)
  • 复用:指的是复用一个或多个线程

它的基本原理就是不再由应用程序自己监视连接,而是让内核监视文件描述符。客户端在操作的时候,会产生具有不同事件类型的socket,在服务端,I/O多路复用程序会把消息放入队列中,然后通过文件事件分派器转发到不同的事件处理器中。
在这里插入图片描述
多路复用有很多的实现,以select为例,当用户进程调用了多路复用器,进程会被阻塞。内核会监视多路复用器负责的所有socket,当任何一个socket的数据准备好了,多路复用器就会返回。这时候用户进程再调用read操作,把数据从内核缓冲区拷贝到用户空间。
所以,I/O多路复用的特点是通过一种机制让一个进程能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读就绪(readable)状态,select()函数就可以返回。

3)Redis的多路复用I/O方案

多路复用需要操作系统的支持,Redis的多路复用,提供了一下几种选择

  • evport是 Solaris系统内核提供支持的
  • epoll是LINUX系统内核提供支持的
  • kqueue是 Mac 系统提供支持的
  • select是POSIX提供的,一般的操作系统都有支撑

4)多线程I/O

Redis 6.0版本中提到的多线程并非是一般意义上的多线程,而是多线程I/O,服务端的数据返回给客户端,需要从内核空间copy数据到用户空间,然后回写到socket,这个过程非常耗时,所以多线程I/O指的是回写到socket这个步骤可以多线程执行,命令请求依然是保持单线程的,所以不会存在线程安全问题。

五、内存回收

1、过期策略

  1. 立即过期(主动淘汰):每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存友好,但是会占用大量的cpu资源处理过期数据,影响缓存的速度。
  2. 惰性过期(被动淘汰):只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省cpu资源,却对内存不友好,可能会出现大量过期key没有被访问而导致大量内存被占用
  3. 定期过期:每隔一段时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已经过期的key。该策略是前两者的一个折中方案,可以在不同情况下保证cpu和内存资源达到平衡最优效果。
  4. Redis中实际使用了惰性过期和定期过期两种策略,并不是实时地清除过期的key

2、淘汰策略

一)策略种类

Redis的淘汰策略是指,当内存使用达到最大内存极限的时候,需要使用淘汰算法来决定清理掉那些数据,以保证新数据的存入。
redis提供的策略根据前缀可以分为两种

  • volatile是针对设置了ttl的key
  • allkeys是针对所有key

redis提供的策略根据后缀可以分为三种

  • LRU(Least Recently Used):最近最少使用,判断最近被使用的时间,目前最远的数据优先被淘汰。
  • LFU(Least FrequentlyUsed):最不常使用,按照使用频率删除,4.0版本新增
  • random:随机删除

Redis全部的淘汰策略

  • volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够内存为止,如果没有可删除对象,回退到noeviction策略
  • allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够内存为止。
  • volatile-lfu:在带有过期时间的键中选择最不常用的
  • allkeys-lfu:在所有的键中选择最不常用的,不管数据有没有设置超时属性。
  • volatile-random:在带有过期时间的键中随机选择
  • allkeys-random:随即删除所有键,直到腾出足够内存为止
  • volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据,如果没有,回退到noeviction策略
  • noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory ,此时Redis只响应读操作。
  • 如果没有设置ttl或者没有符合前提条件的key被淘汰,那么volatile-lru,volatile-random,volatile-ttl相当于noeviction(不做内存回收)

动态修改淘汰策略:

redis> config set maxmemory-policy volatile-lru

二)LRU的实现

1)LRU的淘汰原理

LRU算法是一种很常见的算法,只需要hashmap和链表就可以实现,设置链表长度,每次新增或者取出数据就放入头结点,超过链表长度就删除尾结点。
但是Redis是KV形式的存储方式,采用传统的LRU算法将增加不必要的开销。

1)Redis的LRU淘汰实现

在Redsi的LRU算法中,会采用随机抽样调整算法精度。
根据设置maxmemory_samples(采样数值,默认为5),随机从数据库中选择设置数目个key,淘汰其中热度最低的key对应的缓存数据,所以采样数值越大就越能精确的查找到待淘汰的缓存数据,但是也将消耗更多的性能。
在Redis的redisObject中,有24位是专门来实现LRU或LFU算法的,它存储了该对象最后一次被命令程序访问的时间,只需要得到该时间与当前时间的长度就可以知道该对象的热度。但是这个时间不是实时存储的,因为在被访问时获取时间也是需要消耗性能的,所以这个时间将会从Redis的一个全局变量中获得。
这个全局变量将由一个定时任务赋予值,这个定时任务每过100ms执行一次,这样就避免了获取时间的额外性能开销。
在进行热度计算时,实际上也是把对象中的最后访问时间与这个全局变量取差值,得到该对象的热度。
这个全局变量只有24位,也就意味着按秒为单位只能存储194天,当超过能表示的最大时间时,就会从头开始计算,在此时,可能会出现对象的LRU大于全局变量的情况,那么再要计算热度就不再取差值而是取和值。

三)LFU的实现

在上文提到的24位存储位上存储LFU时,其被分为两部分

  • 高16位用来记录访问时间(单位为分钟,ldt,last decrement time)
  • 低8位用来记录访问频率,简称counter(logc,logistic counter),counter是用于基于概率的对数计数器实现的,8位可以表示百万次的访问频率。
  • 对象被读写时,LFU的值会被更新。

在这里的计数并非访问一次就+1,而是根据一个参数增长因子,lfu-log-factor,这个参数越大,增长速率就越慢。
在一段时间之内不再被访问后,热度会衰减,减少的值由衰减因子lfu-decay-time(分钟)来控制,如果值是1的话,N分钟没有访问,计数器就要减少N。lfu-decay-time越大,衰减越慢。

六、持久化方案

Redis的速度非常快很大部分原因是它所有的数据都存在内存中,一旦出现断电或宕机,内存中的数据都会丢失。为了重启之后数据不丢失,Redis提出了持久化方案。

1、持久化策略

  1. RDB:Redis DataBase,记录快照
  2. AOF:Append Only File,记录日志

2、RDB

RDB是Redis默认的持久化方案,优先级低于AOF,也就是当RDB和AOF同时设置后,AOF生效。
RDB策略会在满足一定条件时,把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。Redis重启会通过加载dump.rdb文件恢复数据。

一)RDB触发

1)自动触发

a)配置规则触发

redis.conf,SNAPSHOTTING,其中定义了触发把数据保存到磁盘的触发频率。如果不需要rdb方案,注释save或者配制成空字符串""。

  • save 900 1 :900秒内至少有1个KEY被添加或修改
  • save 300 10 :300秒内至少有10个KEY被添加或修改
  • save 60 10000 :60秒内至少有10000个KEY被添加或修改

上面的配置是不冲突的,只要满足任一条件都会触发快照。
lastsave命令可以查看最近一次成功生成快照的时间。
rdb配置

  • dir ./:dir是文件路径,rdb文件在默认在启动目录下,命令 config get dir 获取
  • dbfilename dump.rdb:dbfilename是文件名称,
  • rdbcompression yes:rdbcompression是是否以LZF压缩rdb文件,开启压缩可以节省存储空间,但是会消耗一些CPU的计算时间,默认开启
  • rdbchecksum yes:rdbchecksum是开启数据校验,使用CRC64算法来进行数据校验,但是这样会增加CPU大概10%的性能消耗,如果想获取到最大性能提升,可以选择关闭。

出了根据配置条件触发,还有两种自动触发方式

b)shutdown 触发,保证服务正常关闭
c)fiushall,rdb文件是空的,没有什么意义

2)手动触发

如果我们需要迁移数据或者重启服务,就需要手动触发RDB快照。

a)save

save命令在生成快照时会阻塞当前Redis服务器,Redis不能处理其他命令,如果内存中的数据比较多,会造成Redis长时间阻塞。

b)bgsave

bgsave命令在Redis后台异步进行快照操作,快照同时还可以响应服务端请求。
具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束,它不会记录fork之后产生的数据,阻塞只发生在fork阶段,一般时间很短。

一)RDB文件的优势和劣势

1)优势

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

2)劣势

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

3、AOF

如果数据相对重要,希望将损失降到最小,可以使用AOF策略。
AOF默认不开启,采用日志的形式来记录每一个写操作,并追加到文件中,开启后,执行更改数据的命令式,就会把命令写到AOF文件中。
Redis重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。

一)AOF配置

  • appendonly on:appendonly是开关,Redis默认只开启RDB持久化,开启AOF需要修改为yes
  • appendfilename “appendonly.aof”:appendfilename是文件名,路径也是通过dir参数配置,获取命令 config get dir

二)写入时机

由于操作系统的缓存机制,AOF没有真正的写入磁盘,而是进入系统磁盘缓存,什么时候把缓冲区内容写入AOF文件?

  • appendfsync everysec :表示每秒执行一次fsync,可能会导致丢失1s的数据。默认开启
  • appendfsync always:表示每次写入都执行fsync,以保证数据同步到磁盘,效率很低
  • appendfsync no:表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快但是不安全。

三)压缩AOF文件

AOF文件随着写入命令的增加,一定会越来越大,为了解决这个问题,Redis增加了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。
重写命令:bgrewriteaof
AOF文件重写并不是对原文件进行重新整理,而是直接读取服务器现有键值对,然后用一条命令去代替之前记录这个键值对的多条命令,最后生成一个新的文件来替换旧的。

1)触发机制

  • auto-aof-rewrite-percentage:默认值为100,aof自动重写配置,当目前aof文件大小超过上一次重写的aof文件大小的百分之几进行重写,即当aof文件增长到一定大小时,Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得到AOF文件大小的两倍(默认设置为100,因此是两倍)时,自动启动新的日志重写进程。
  • auto-aof-rewirte-min-size:默认64M,设置允许重写的最小aof文件大小,避免达到约定的百分之几时aof文件依然很小需要频繁重写的情况。

2)实时同步

如果AOF文件正在重写,新的命令写入,原本的AOF文件被更改应该如何处理?
当子进程重写AOF文件时,主进程将进行如下操作:

  1. 处理命令请求
  2. 将写命令追加到现有的AOF文件中
  3. 将写命令追加到AOF重写缓存中

这样当同步完成时,再从重写缓存中读取新的写入命令,就保持了实时一致性。

四)其他设置

  • no-appendfsync-on-rewrite:在AOF重写或者写入RDB文件的时候,会执行大量的I/O,此时对于everysec和always的AOF模式来说,执行fsync会造成阻塞时间过长,no-appendfsync-on-rewrite默认设置为no,如果对延时有很高的要求,这个字段可以设置为yes,否则还是设置为no,这样对于持久化特性来说是更安全的选择。设置为yes表示重写期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入。Linux的默认fsync策略是30s,可能丢失30s数据。
  • aof-load-truncated:AOF文件可能在尾部是不完整的,当Redis启动的时候,AOF文件的数据被载入内存。重启可能发生在Redis所在的主机操作系统宕机后,尤其是在ext4文件系统没有加上data=ordered选项时出现这种现象。Redis宕机或者异常终止不会造成尾部不完整现象,可以选择让Redis退出,或者导入尽可能多的数据。如果设置是yes,当截断的AOF文件被导入的时候,会自动发布一个log给客户端然后load。如果设置是no,用户必须手动Redis-check-aof修复AOP文件才可以。默认yes

五)数据恢复

Redis重启后就会进行AOF文件恢复

六)优势与劣势

1)优势

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

2)劣势

  • 对于具有相同数据的Redis,AOF文件通常比RDB文件体积更大。
  • 虽然AOF提供了多种同步频率,默认情况下性能也较为不错,但高并发情况下还是RDB性能更加优秀。

4、两种策略对比

如果可以忍受一小段时间内数据丢失,RDB是更好的解决方案,定时生成RDB快照非常便于进行数据库备份,并且恢复速度更快,否则就只能选择AOF。
一般情况建议不要仅使用一种持久化策略,而是应该两种一起使用,在这种情况下,当Redis重启时会休闲载入AOF文件来恢复原始数据,因为AOF文件恢复的数据集将比RDB更加完整。

猜你喜欢

转载自blog.csdn.net/jiayibingdong/article/details/115186990