0%

redis04_数据库

redis数据库

概述

redis数据库底层是用字典实现的,一个redisServer对象里面保存了redisDb对象(默认16个)。

redisDb对象通过字典保存所有的键值对。

数据库对象结构


数据库键空间

redisDb结构的dict字典保存了数据库中所有键值对,这个字典称为键空间

当使用redis命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作:

  • 读取一个键后,服务器会根据键是否存在,更新键空间命中(keyspace_hits)/不命中次数(keyspace_misses)。
  • 读取一个键后,会更新键的LRU时间
  • 如果服务器读取键发现该键已经过期,则会删除过期键。
  • 如果使用WATCH监控某个键,服务器对监控的键进行修改,键会被标记dirty
  • 每次修改一个键,会对dirty键计数器增1,这个计数器会触发持久化和复制操作。
  • 如果开启数据库通知功能,服务器会按配置发送相应数据库通知。

键空间通知使得客户端可以通过订阅频道或模式, 来接收那些以某种方式改动了 Redis 数据集的事件。

事件通过 Redis 的订阅与发布功能(pub/sub)来进行分发, 因此所有支持订阅与发布功能的客户端都可以在无须做任何修改的情况下, 直接使用键空间通知功能。

因为 Redis 目前的订阅与发布功能采取的是发送即忘(fire and forget)策略, 所以如果你的程序需要可靠事件通知(reliable notification of events), 那么目前的键空间通知可能并不适合你: 当订阅事件的客户端断线时, 它会丢失所有在断线期间分发给它的事件。

数据库通知

基于pub/sub功能实现的数据库通知。

过期策略

redisDB结构的expires字典保存了数据库中所有键的过期时间,这个字典称为过期字典

  • 过期字典的键是一个指针,指向键空间的某个键对象
  • 过期字典的值是个long long类型的整数,保存了毫秒精度的unix时间戳。

redis为什么要维护一个过期字典,而不是把属性保存到对象里面去?

  1. 为了节省空间,如果在对象里面,那么对于没有过期时间的key来说,每次都会浪费一个long long类型的空间。不一定所有键都有过期时间,时间换空间之间做的权衡(个人观点)。
  2. 和删除策略的实现有关,如果没有这个字典,定期删除的时候会很难命中设置了过期时间的键。

过期键删除策略

一般的删除策略有如下三种:

  1. 定时删除,设置过期删除的时候同时创建一个定时器
  2. 惰性删除,放任过期键不管,每次取键的时候,检查键是否过期,过期删除。
  3. 定期删除,每隔一段时间,对数据库进行检查,删除里面的过期键。

定时删除

定时删除策略对内存是最友好的,对CPU时间是最不友好的,会占用相当一部分cup时间。占用大量cpu时间,影响系统吞吐量。

惰性删除

每次取键的时候,做检查,如果过期就删除。对cpu时间最友好,对内存最不友好。浪费内存,有内存泄漏的风险。

定期删除

定期删除是上面两种策略的整合和这种,每隔一段时间执行一次删除操作,并通过限制删除操作的时长和频率来减少对cpu时间的影响。

难点是确定删除操作的时长和频率,太频繁或者时间太长,还是会对cpu时间造成影响。太少或者时间太短,起不到删除键的效果,造成内存浪费。

redis的过期删除策略(4.0之前)

redis通过惰性删除和定期删除两种策略配合,在CPU时间和避免空间浪费之间取得平衡。

每当redis服务器周期性操作serverCron函数时,会调用activeExpireCycle函数去做定期删除的操作。

  1. 每次运行,都从一定数量的数据库(默认16个数据库)中取出一定数量的随机键(默认每个数据库 20个键)进行检查,并删除过期键。
  2. 全局变量current_db会记录当前的数据库进度,如果上次到10就结束了,这次会从11号数据库开始。
  3. 随着函数的不断运行,服务器的所有数据库都会被检查一遍,current_db变量重置为0,然后又开始新一轮检查。

redis的过期删除策略(4.0)

Redis作为一个单线程模型的服务,当执行一些耗时的命令时,比如使用DEL删除一个大key(元素超大的集合类型key),或者使用FLUSHDB 和 FLUSHALL 清空数据库,会造成redis阻塞,影响redis性能,甚至导致集群发生故障转移。另外redis在删除过期数据或因内存超过容量淘汰内存数据时,也有可能因为大key导致redis阻塞。

为了解决以上问题,redis 4.0 引入了惰性删除lazyfree的机制,它可以将删除键或数据库的操作放在后台线程里执行,删除对象时只是进行逻辑删除,从而尽可能地避免服务器阻塞。

实现方式是异步线程,如果键过期,直接扔到异步线程中去删除,不会阻塞主线程。

原来是都在主线程里面做删除,现在是通过异步删除的方式实现。

AOF、RDB、和复制功能对过期键的处理

生成RDB文件时

在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对键做检查,过期键不会被保存到新的RDB文件中。

载入RDB文件

  • 如果是主服务器模式运行,加载过程中会检查过期键,过期的会被过滤。
  • 从服务器模式运行,所有键都会被加载,不过进行主从同步的时候,从服务器的数据库会被清空。

AOF文件写入

当服务器以AOF持久化模式运行时,如果键没有被定期或者惰性删除,AOF文件不会因为这个过期键而产生影响。(只有被定期或者惰性删除后,AOF文件中会追加一条删除命令)

AOF重写

在执行AOF重写的过程中,程序会检查过期键,已过期的键不会写到AOF文中。

复制

服务器在复制模式下,从服务器的过期键删除动作由主服务器控制,从服务器只有接到主服务器发来的DEL命令后,才会删除键。


内存淘汰机制(LRU,LFU)

在生产环境中我们是不允许 Redis出现交换行为的,为了限制最大使用内存,Redis 提 供了配置参数 maxmemory 来限制内存超出期望大小。

当实际内存超出 maxmemory 时,Redis 提供了几种可选策略 (maxmemory-policy) 来让 用户自己决定该如何腾出新的空间以继续提供读写服务。

常见的内存淘汰算法

FIFO(先入先出)

FIFO (First In FIrst Out) 是最简单的算法,原理跟名字一样,“如果一个数据最先进入缓存中,则应该最早淘汰掉”。把缓存中的数据看成一个队列,最先加入的数据位于队列的头部,最后加入位于队列的尾部。当缓存空间不足需要执行缓存淘汰操作时,从队列的头部开始淘汰。

LRU(最近最少被使用)

LRU (Least Recently Used) 的核心思想是基于“如果数据最近被访问过,它在未来也极有可能访问过”。同样把缓存看成一个队列,访问一个数据时,如果缓存中不存在,则插入到队列尾部;如果缓存中存在,则把该数据移动到队列尾部。当执行淘汰操作时,同样从队列的头部开始淘汰。 Java 中可以直接使用 LinkedHashMap 来实现。

LFU

LFU (Least Frequently Used)的核心思想是“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”,会记录数据访问的次数,当需要进行淘汰操作时,淘汰掉访问次数最少的数据。

其他

  1. 2Q(Two Queues):同时采用 FIFO 和 LRU 两个队列,首次访问数据时加入到 FIFO 队列中,如果数据在 FIFO 队列移除之前被再次访问,数据会被移动到 LRU 队列中。
  2. LRU-K :是一种 LRU 算法的增强版,在 LRU 维护队列的基础上,再添加一个队列维护数据访问的次数,由原来访问 1 次会被添加到缓存中,改为访问 K 次才会被加入到缓存中。
  3. ARC:在IBM Almaden研究中心开发,这个缓存算法同时跟踪记录LFU和LRU,以及驱逐缓存条目,来获得可用缓存的最佳使用。

LRU

volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的key 进行淘汰。

noeviction

不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这是默认的淘汰策略。

volatile-lru

尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过
期时间的 key 不会被淘汰。

volatile-ttl

跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl
越小越优先被淘汰。

volatile-random

跟上面一样,不过淘汰的 key 是设置了过期时间的key 集合中随机的 key。

allkeys-lru

这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。

allkeys-random

跟上面一样,不过淘汰的策略是随机的 key。

LFU(4.0之后)

Redis在4.0里引入了一个新的淘汰策略–LFU模式。

LFU模式下,Redis对象头中的lru字段来存储key的热度。

volatile-lfu

allkeys-lfu

理解内存

内存消耗

内存使用统计

要了解Redis自身使用内存的统计数据,可通过执行info memory 命令获取内存相关指标。


1
2
3
4
5
6
7
需要重点关注的指标有:used_memory_rss和used_memory以及它们的比 值mem_fragmentation_ratio

当mem_fragmentation_ratio>1时,说明used_memory_rss-used_memory多出 的部分内存并没有用于数据存储,而是被内存碎片所消耗。
如果两者相差很大,说明碎片率严重。

当mem_fragmentation_ratio<1时,这种情况一般出现在操作系统把Redis 内存交换(Swap)到硬盘导致。
出现这种情况时要格外关注。

内存消耗划分

出现高内存碎片问题时常见的解决方式如下:

  • 数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用 数字类型或者固定长度字符串等,但是这要视具体的业务而定,有些场景无 法做到。
  • 安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可 用架构,如Sentinel或Cluster,将碎片率过高的主节点转换为从节点,进行 安全重启。
  • Redis4版本之后开始支持内存碎片的清理

清理内存碎片

1
2
3
4
5
6
7
8
//默认情况下自动清理碎片的参数是关闭的,可以按如下命令查看
127.0.0.1:6379> config get activedefrag
1) "activedefrag"
2) "no"
//启动自动清理内存碎片

127.0.0.1:6379> config set activedefrag yes
OK

内存分配

Redis 为了保持自身结构的简单性,在内存分配这里直接做了甩手掌柜,将内存分配的 细节丢给了第三方内存分配库去实现。目前 Redis 可以使用 jemalloc(facebook) 库来管理内 存,也可以切换到 tcmalloc(google)。

内存优化

缩减键值对象

降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长 度。

key长度:如在设计键时,在完整描述业务情况下,键值越短越好。如 user:{uid}:friends:notify:{fid}可以简化为u:{uid}:fs:nt:{fid}。

value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二 进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性 避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工 具来降低字节数组大小。

字符串优化

编码优化

内存回收机制

Redis 并不总是可以将空闲内存立即归还给操作系统。

如果当前 Redis 内存有 10G,当你删除了 1GB 的 key 后,再去观察内存,你会发现 内存变化不会太大。原因是操作系统回收内存是以页为单位,如果这个页上只要有一个 key 还在使用,那么它就不能被回收。

Redis 虽然无法保证立即回收已经删除的 key 的内存,但是它会重用那些尚未回收的空 闲内存。