分布式缓存技术Redis(三)原理分析

一、持久化

Redis支持两种方式的持久化,一种是RDB(Redis Database)方式,另一种是AOF(Append Only File)方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行命令后将命令本身记录下来。两种持久化方式可以单独使用其中一种,也可以两种结合使用。

1.RDB

当符合条件时,redis会单独创建(fork)一个进程来进行持久化操作,会先将数据写入到一个临时文件中,等到持久化过程结束了,再用这个临时文件替换上一次持久化的文件。整个过程中,主进程是不进行任何IO操作的,这确保了极高的性能。如果需要进行大规模的数据恢复,且对于数据恢复的完整性不是非诚敏感,那么RDB方式是比AOF方式更高效的方式。RDB的缺点是在最后一次持久化后来的新数据可能会部分丢失。

fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器)数值都和原进程一致。但是是一个全新的进程并且会作为原来进程的子进程。

redis会在以下几种情况下对数据进行快照
  1. 根据匹配规则进行自动快照
  2. 用户执行SAVE或者BGSAVE命令
  3. 执行FLUSHALL命令
  4. 执行复制(replication)时
根据规则进行自动快照

Redis允许用户自定义快照条件,当符合快照条件时,Redis会自动执行快照操作。快照的条件由用户在redis.conf配置文件中进行配置。格式如下:

save 900 1
save 300 10
save 60 1000

第一个参数是时间窗口(单位:秒),第二个参数是键的个数,也就是说在第一个时间参数配置范围内倍更改的键的个数大于后面参数时,即符合快照文件,以上是redis的默认三个规则,任意满足一个则进行快照。例如第一条代表在900秒内有一个以上的键被更改就进行快照。

用户执行SAVE或者BGSAVE

除了让redis自动快照以外,当我们对服务器进行重启或者服务器迁移时我们需要人工去干预备份。redis提供了两条命令来执行这个操作。

  1. save命令
    当执行save命令时,Redis同步做快照操作,在快照执行过程中会阻塞所有来自客户端的请求。当Redis内存中的数据较多时,通过该命令将导致Redis较长时间对客户端不可响应。所以不建议在生产环境上执行这个命令,而是推荐执行bgsave命令。
  2. bgsave命令
    通过bgsave命令可以在后台异步的进行快照操作,快照的同时服务器还可以继续响应来自客户端的请求。执行bgsave后,Redis立即返回OK表示开始执行快照操作。通过LASTSAVE命令可以获取最近一次成功执行快照的时间。
执行FLUSHALL命令

该命令会清除Redis在内存中的所有数据。执行该命令后,只要在Redis中的配置的快照规则不为空,也就是save规则存在,Redis就会执行一次快照操作。如果没有定义快照规则就不会执行快照操作。

执行复制(replication)时

该操作主要是在主从模式下,Redis会在复制初始化时进行自动快照。即使没有定义自动快照规则,并且没有手动执行过快照操作,它仍然会生成RDB快照文件。

2.AOF

当使用Redis存储非临时数据时,一般需要打开AOF持久化来降低进程终止导致的数据丢失。AOF可以将Redis执行的每一条命令追加到硬盘文件中,这一过程会降低Redis的性能,但大部分情况下这个影响是能够接受的,另外使用较快的硬盘可以提高AOF的性能。

开启AOF

appendonlyfile yes

默认情况下Redis没有开启AOF方式的持久化,可以在redis.conf文件中找到appendonlyfile参数开启AOF。开启AOF持久化后每执行一条会更改Reids内存中数据的命令后,Reids就会将这条命令写入硬盘中的AOF文件。AOF文件和RDB保存的路径一样都是通过redis.conf中的dir参数设置的,默认的文件名是appendonlyfile.aof。可以通过appendfilename参数修改文件名。

AOF的实现
AOF文件以纯文本形式记录Redis执行的命令,例如Redis客户端执行以下命令时,只会将前三条记录到aof文件中。
set test a
set test b
set test c
get test

通过vim工具可以查看到AOF文件记录的内容正是Redi客户端发送的原始通信协议内容,如果是记录的以上三条有效命令可以发现前两条都是冗余的,因为这两条的执行结果都会被第三条覆盖。随着执行的命令越来越多,AOF文件也会越来越大。其实内存中的数据可能没有多少,但是这样就会造成磁盘空间浪费以及Redis数据还原的过程比较长的问题。Redis也提供了可以优化这一问题的配置,当达到一定条件Reids就会自动重写AOF文件。另外还可以通过BGREWRITEAOF命令手动执行重写AOF文件,执行完后冗余的命令内容就会被删除。相比RDB在重新启动Redis时数据载入会慢一些,因为AOF是将命令逐条执行。

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

auto-aof-rewrite-percentage表示的是当目前的AOF文件超过上一次重写时AOF文件大小的百分比大小是就再次重写。如果之前没有重写过,则以启动时AOF文件大小为依据。

auto-aof-rewrite-min-size表示限制了允许重写的最小的AOF文件大小,因为通常在AOF文件很小时有多少条冗余命令并不会造成什么影响。

AOF的重写原理

重写的流程是这样的,主进程会fork一个子进程出来进行重写,这个重写过程并不是基于原有的AOF文件来做的,而是有点类似于快照的方式,全量遍历内存中的数据,然后逐个序列到AOF文件中。在fork子进程这个过程中,服务端仍然可以对外提供服务,那这个时候重写的AOF文件。但不必担心重写的aof文件的数据和Redis内存数据不一致,因为这个过程中redis主进程的数据更新操作,会缓存到aof_rewrite_buf中,业就是单独开辟一块缓存来存储重写期间收到的命令,当子进程重写完以后再把缓存中的数据追加到AOF文件中。当所有的数据全部追加到新的AOF文件中后,把新的AOF文件重命名为当前AOF文件。此后就完成了AOF的重写。

二、过期删除

在Redis中提供了expire命令设置一个键的过期时间,到期以后Redis会自动删除它。这个过期删除的方法在Redis中分为两种实现,消极方法和积极方法。

消极方法(passive way)

在Redis键被访问时如果发现它已经失效,那么就删除它。

积极方法(active way)

周期性的从设置了失效时间的键中选择一部分失效的进行删除,对于那么从未被查询到的key,即便它们已经过期,消极方法也无法删除。因此Redis会周期性的随机测试一些key,已过期的key将会被删除,Redis每秒会进行10次操作,具体的操作流程如下:

  1. 随机测试20个带有过期时间的key。
  2. 删除其中已经过期的。
  3. 如果超过25%的设置了过期时间的key被删除,则重复步骤1。

三、回收策略

Redis中提供了多种内存回收策略,当内存容量不足时,为了保证程序的运行,这是就不得不淘汰内存中一些key,释放这些key占用的空间。其中默认的策略是noevication策略,当内存达到阀值时,所有引起申请内存空间的命令会报错。还有以下不同场景的策略:

  1. allkeys-lru
    从数据集中挑选最近最少使用的key淘汰。
    适用场景:应用对缓存的访问都是相对热点数据。
  2. allkeys-random
    随机淘汰某个key。
    适用场景:应用对缓存数据的访问概率相等。
  3. volatile-random
    从已设置过期时间的数据集中任意选择key淘汰。
  4. volatile-lru
    从已设置过期时间的数据集中挑选最近最少使用的key淘汰。
  5. volatile-ttl
    从已经设置过期时间的数据集中挑选将要过期的进行淘汰。

实际上Redis时间的LRU并不是可靠的LRU,也就是名义上我们使用LRU算法淘汰内存数据,但实际上被淘汰的key并不一定是真正的最近最少使用的数据。这里涉及到一个权衡问题,如果需要在所有的数据中搜索符合条件的数据,那么一定会增加Redis的性能开销,Redis是单线程应用,所以耗性能的操作会更谨慎。为了在一定成本内实现相对符合条件的LRU,早期的Reids版本采用的是采样抽取的LRU,也就是放弃了从所有数据中搜索数据改为采用空间抽取。Redis3.0之后作者对于采样LRU进行了优化,目的是为了在一定成本内更符合真实的LRU算法。

四、发布订阅

Redis提供了发布订阅的功能,可以用于消息的传输。Redis提供了一组命令可以让开发者实现"发布订阅"模式(publish/subscribe)。该模式同样可以实现进程间的消息传递,它的实现原理是发布/订阅模式包含两个角色,分别是发布者和订阅者。ding订阅者可以订阅一个或多个频道,而发布者可以向指定的频道发布消息,所有的订阅该频道的订阅者都会收到消息。

发布者命令

#语法:PUBLISH channel message
#比如向testChannel发送一条hello消息
PUBLISH testChannel "hello"

这样就是实现了该消息的发送,该命令的返回值表示接受到这条消息的订阅者数量。如果在执行这条命令的时候还没有订阅者订阅这个频道就会返回0。另外发出去的消息不支持持久化。

订阅者命令

#语法:SUBSCRIBE channel [channel ...]
#比如订阅testChannel频道
SUBSCRIBE testChannel

订阅者在执行命令订阅这个频道后就可以就收之后这个频道发布者推送的消息。另外该命令可以同时订阅多个频道。订阅的channel也支持正则表达式。

五、多路复用

Redis采用单线程处理来自客户端的请求,把任务封闭在一个线程中从而避免了线程安全问题。官网解释CPU并不是Redis的瓶颈所在,Redis的瓶颈主要是机器的内存和网络的带宽。因为Redis是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入和输出都是阻塞的,所以IO操作在一般情况下不能直接返回,这会导致某一文件的IO阻塞从而导致整个进程无法对其他客户提供服务,而IO多路复用就是为了解决这个问题而出现,也意味着Redis可以处理高并发请求。

IO模型
  1. 同步阻塞IO(Blocking IO):传统IO。
  2. 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。
  3. IO多路复用(IO Multiplexing):即经典的Reactor设计模式,也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种IO模型。
  4. 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

同步和异步指的是用户线程和内核的交互方式。
阻塞和非阻塞指的是用户线程调用内核IO操作的方式是阻塞和非阻塞。

同步和异步可以理解为Java中用多线程做异步处理,通过多线程执行一个流程,主线程不用等待。而阻塞和非阻塞可以理解为假如在同步流或者异步流中做IO操作,如果缓冲区数据还没准备好,IO的这个过程是否会阻塞。简单来说同步异步是针对客户端应用程序的等待,阻塞与非阻塞是针对系统IO的等待。

六、Redis中的Lua脚本

原子性问题

虽然Redis是单线程的,但是在某些时候还是可能出现线程安全问题,这个问题并不是源自于Redis服务器内部。而是Redis作为数据服务器是提供给多个客户端使用,多个客户端的操作就相当于同一个进程下的多个线程,如果客户端之间没有做好数据的同步策略,那么就有可能出现数据不一致问题。多个客户端的命令之间没有做请求同步,导致实际执行顺序可能会不一致,最终也就不能无法满足原子性。

效率问题

Redis本身的吞吐量是非常高的,因为它是基于内存的数据库。在实际使用过程中,有一个非常重要的因素影响Redis的吞吐量,那就是网络。我们在使用Redis实现某些特定功能的时候,很可能需要多个命令或者多个数据类型的交互才能完成,那么这种多次网络请求对性能影响比较大。当然Redis也做了一些优化,比如提供了pipeline管道操作,但是它有一定的局限性,就是执行的多个命令和响应之间是不存在相互依赖关系的。所以我们需要一种机制能够编写一些具有业务逻辑的命令,减少网络请求。

Lua

Lua是一个高效的轻量级脚本语言(javascript、shell、sql、python、ruby…),用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Redis中内嵌了对Lua的支持,允许开发者用Lua语言编写脚本传到Redis中执行,Redis客户端可以使用Lua脚本直接在服务端原子性的执行多个Redis命令。并且Lua脚本给Redis带来了如下好处:

  1. 减少网络开销,在Lua脚本中可以把多个命令放在一个脚本中一次性传输运行。
  2. 原子操作,Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入这样就保证了多个命令的原子性。
  3. 复用性,客户端发送的脚本会永久存储在Redis中,这意味着其他客户端可以复用这个脚本来完成相同的逻辑。

eval命令

#语法 eval [脚本内容] [key参数的数量] [key...] [value...]
#例子1:存储一个key为luaTest,value为hello的脚本执行
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 luaTest hello
#例子2:获取一个key为luaTest的脚本执行
eval "return redis.call('get',KEYS[1])" 1 luaTest

注意:eval命令是根据key参数的数量,也就是上面例子中的1来将后面所有参数分别存入脚本中KEYS和ARGV两个表类型的全局变量。当脚本不需要任何参数时也不能省略这个参数。如果没有参数则为0。

evalsha命令

当lua脚本内容比较长的时候,每次调用脚本需要把脚本内容传送给Redis服务端比较占用网络带宽。所以Redis提供了evalsha命令允许开发者通过脚本内容的sha1摘要来执行脚本。该命令用法和eval命令一样,不过执行时将脚本内容替换成了脚本内容的sha1摘要。所以我们在调用eval命令之前,可以限制性evalsha命令,如果提示脚本不存在,再调用eval命令执行脚本。

  1. Redis在执行script load命令时会计算脚本的SHA1摘要并记录在脚本缓存中。
  2. 执行evalsha命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了就执行脚本,否则返回“NOSCRIPT No matching script,Please use EVAL”。
#语法 script load [脚本内容]
#计算缓存脚本,并执行该命令会返回一串sha1摘要
script load "return redis.call('get','luaTest')"
#调用摘要,执行脚本
evalsha "d26812081ece472bc02d9f6161983119fe2498f2" 0

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值