Redis的原理分析

目录

 

过期时间设置

过期删除的原理

发布订阅

持久化

RDB

AOF(append only file)

内存回收策略

Redis是单进程单线程,性能为什么这么快?

Lua脚本


  • 过期时间设置

EXPIRE key seconds

在Redis中提供了Expire命令设置一个键的过期时间,到期以后Redis会自动删除它。

EXPIRE命令的使用方法为:EXPIRE key seconds

EXPIRE 返回值为1表示设置成功,0表示设置失败或者键不存在。

其中seconds 参数表示键的过期时间,单位为秒,seconds必须是整数,所以最小是1秒。如果向要更精确的控制键的过期时间可以使用PEXPIRE命令,单位是毫秒。

TTL key

如果想知道一个键还有多久时间被删除,可以使用:TTL key

当键不存在时,TTL命令会返回-2,而对于没有给指定键设置过期时间的,通过TTL命令会返回-1。

PERSIST key

如果想取消键的过期时间设置(使该键恢复成为永久的),可以使用PERSIST命令。

  • 过期删除的原理

Redis 删除失效主键的方法主要有两种。

被动方法(passive way)

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

主动方法(active way)

周期性地从设置了失效时间的主键中选择一部分失效的主键删除。

对于那些从未被查询的key,即便它们已经过期,被动方式也无法清除。因此Redis会周期性地随机测试一些key,已过期的key将会被删掉。Redis每秒会进行10次操作,具体的流程:

  1. 随机测试 20 个带有timeout信息的key;
  2. 删除其中已经过期的key;
  3. 如果超过25%的key被删除,则重复执行步骤1;

这是一个简单的概率算法(trivial probabilistic algorithm),基于假设我们随机抽取的key代表了全部的key空间。

  • 发布订阅

Redis提供了发布订阅功能,可以用于消息的传输,Redis提供了一组命令可以让开发者实现“发布/订阅”模式(publish/subscribe)。该模式同样可以实现进程间的消息传递。

发布者向channel.1发一条消息,hello:PUBLISH channel.1 “hello”

这样就实现了消息的发送,该命令的返回值表示接收到这条消息的订阅者数量。因为在执行这条命令的时候还没有订阅者订阅该频道,所以返回为0。另外值得注意的是消息发送出去不会持久化,如果发送之前没有订阅者,那么后续再有订阅者订阅该频道,之前的消息就收不到了。

订阅者订阅消息:SUBSCRIBE channel.1

执行SUBSCRIBE命令后客户端会进入订阅状态。

  • 持久化

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

  • RDB

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

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

Redis会在以下几种情况下对数据进行快照

一.根据配置规则进行自动快照

Redis允许用户自定义快照条件,当符合快照条件时,Redis会自动执行快照操作,默认配置了三个规则。

save 900 1   -- 在900秒(15分)内有一个以上的键被更改则进行快照。

save 300 10

save 60 10000

save有两个参数,第一个参数是时间窗口,第二个是键的个数,每条快照规则占一行,规则之间是“或”的关系。

二.用户执行SAVE或者GBSAVE命令

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

save:当执行save命令时,Redis同步做快照操作,在快照执行过程中会阻塞所有来自客户端的请求。当redis内存中的数据较多时,通过该命令将导致Redis较长时间的不响应。所以不建议在生产环境上使用这个命令,而是推荐使用bgsave命令。

bgsave:bgsave命令可以在后台异步地进行快照操作,快照的同时服务器还可以继续响应来自客户端的请求。执行BGSAVE后,Redis会立即返回ok表示开始执行快照操作。

三.执行FLUSHALL命令

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

四.执行复制(replication)时

该操作主要是在主从模式下,redis会在复制初始化时进行自动快照,即使没有定义自动快照规则。

  • AOF(append only file)

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

默认情况下Redis没有开启AOF方式的持久化,可以在redis.conf中找到 appendonly yes启动AOF。

AOF的实现:

AOF文件以纯文本的形式记录Redis执行的写命令:

set foo 1

set foo 2

set foo 3

get

redis 会将前3条命令写入AOF文件中,我们会发现AOF文件的内容正是Redis发送的原始通信协议的内容,前面2条命令其实是冗余的,因为这两条的执行结果都会被第三条命令覆盖。随着执行的命令越来越多,AOF文件的大小也会越来越大,其实内存中实际的数据可能没有多少,这样就会造成磁盘空间以及redis数据还原的过程较长的问题。为了解决这个问题,Redis提供了AOF文件的重写机制。

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

auto-aof-rewrite-min-size 64mb:表示限制了允许重写的最小AOF文件大小,通常在AOF文件很小的情况下即使其中有很多冗余的命令我们也并不太关心。

另外,还可以通过BGREWRITEAOF 命令手动执行AOF,执行完以后冗余的命令已经被删除了。

在启动时,Redis会逐个执行AOF文件中的命令来将硬盘中的数据载入到内存中,载入的速度相对于RDB会慢一些。

  • AOF的重写原理

主进程会fork一个子进程出来进行AOF重写,这个重写过程并不是基于原有的aof文件来做的,而是有点类似于快照的方式,全量遍历内存中的数据,然后逐个重写到aof文件中。

在fork子进程这个过程中,服务端仍然可以对外提供服务,那这个时候重写的aof文件的数据和redis内存数据不一致了怎么办?不用担心,这个过程中,主进程的数据更新操作,会缓存到aof_rewrite_buf中,也就是单独开辟一块缓存来存储重写期间收到的命令,当子进程重写完以后再把缓存中的数据追加到新的aof文件。

当所有的数据全部追加到新的aof文件中后,把新的aof文件重命名为,此后所有的操作都会被写入新的aof文件。

如果在rewrite过程中出现故障,不会影响原来aof文件的正常工作,只有当rewrite完成后才会切换文件。因此这个rewrite过程是比较可靠的。

  • 内存回收策略

Redis中提供了多种内存回收策略,当内存容量不足时,为了保证程序的运行,这时就不得不淘汰内存中的一些对象,释放这些对象占用的空间。

noeviction:默认的策略,当内存使用达到阈值的时候,所有引起申请内存的命令会报错。

allkeys-lru:从数据集(server.db[i].dict)中淘汰最近最少使用的数据。如果我们的应用对缓存的访问都是相对热点数据,那么可以选择这个策略。

allkeys-random:随机移除某个key。如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。

volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。

volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。

volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。

  • Redis中的LRU使用机制

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

  • Redis是单进程单线程,性能为什么这么快?

Redis采用了一种非常简单的做法,单线程来处理来自所有客户端的并发请求,Redis把任务封闭在一个线程中从而避免了线程安全问题。那么redis单线程的效率如何呢?

官方的解释是,CPU并不是Redis的瓶颈所在,Redis的瓶颈主要在机器的内存和网络的带宽。那么Redis能不能处理高并发请求呢?当然是可以的,那就是多路复用机制。 【注意并发不等于并行,并发性I/O流,意味着能够让一个计算单元来处理来自多个客户端的流请求。并行性,意味着服务器能够同时执行几个事情,具有多个计算单元】

  • 多路复用

Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的。

  • Lua脚本

我们在使用redis的时候,会面临一些问题:

原子性问题

redis虽然是单一线程的,当时仍然会存在线程安全问题。当然,这个线程安全问题不是来源安于Redis服务器内部。而是Redis作为数据服务器,是提供给多个客户端使用的。多个客户端的操作就相当于同一个进程下的多个线程,如果多个客户端之间没有做好数据的同步策略,就会产生数据不一致的问题。

举个简单的例子,多个客户端的命令之间没有做请求同步,导致实际执行顺序可能会不一致,最终的结果也就无法满足原子性了。

效率问题

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

Lua

Redis中内嵌了对Lua环境的支持,允许开发者使用Lua语言编写脚本传到Redis中执行,Redis客户端可以使用Lua脚本,直接在服务端原子的执行多个Redis命令。

使用脚本的好处:

  1. 减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行。
  2. 原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说,编写脚本的过程中无需担心会出现竞态条件。
  3. 复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑。Lua是一个高效的轻量级脚本语言(javascript、shell、sql、python、ruby…),用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

在Lua脚本中调用Redis命令

redis.call(‘set’,’hello’,’world’)

local value=redis.call(‘get’,’hello’)

从Lua脚本中获得返回值

lua脚本的内容为: return redis.call(‘set’,KEYS[1],ARGV[1])

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 lua1 hello

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

猜你喜欢

转载自blog.csdn.net/u011212394/article/details/82811960