数据库
2023-3-31
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property

 

服务器中的数据库

Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,数组的每个项都是一个redis.h/redisDb结构,每个redisdDb结构代表一个数据库:
 
在初始化服务器时,程序会根据服务器状态的dbnum属性来决定应该创建多少个数据库:
dbnum属性的值由服务器配置的database选项决定,该选项的值默认为16,所以Redis服务器默认会创建16个数据库:
notion image

切换数据库

每个Redis客户端都有自己的目标数据库,默认情况下,客户端连接的是第0个数据库。通过select命令切换数据库:
在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:
redisClient.db指针指向redisServer.db数组中的其中一个元素,被指向的元素就是客户端的目标数据库:
notion image
执行命令select 2,将目标数据库改为2号数据库:
notion image
 

数据库键空间

Redis是一个键值对(key-value pair)数据库服务器, 服务器中的每个数据库都由一个redis.h/redisDb结构表示, 其中,redisDb结构的dict字典保存了数据库中的所有键值对, 这个字典称为键空间(key space)
键空间和用户所见的数据库是直接对应的:
  • 键空间的键也就是数据库的键, 每个键都是一个字符串对象
  • 键空间的值也就是数据库的值, 每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象在内的任意一种Redis对象
 
如果在空白的数据库中执行以下命令:
那么在这些命令执行之后, 数据库的键空间:
notion image
  • alphabet是一个列表键, 键的名字是一个包含字符串 "alphabet"的字符串对象, 键的值则是一个包含三个元素的列表对象
  • book是一个哈希表键, 键的名字是一个包含字符串"book"的字符串对象, 键的值则是一个包含三个键值对的哈希表对象
  • message是一个字符串键, 键的名字是一个包含字符串"message"的字符串对象, 键的值则是一个包含字符串"hello world"的字符串对象
键空间是一个字典, 所有针对数据库的操作——添加一个键值对到数据库、删除一个键值对、获取某个键值对等等,都是通过对键空间字典进行操作来实现的
 

添加新键

添加一个新键值对到数据库, 实际上就是将一个新键值对添加到键空间字典里面, 其中键为字符串对象, 而值则为任意一种类型的Redis对象。
键空间将添加一个新的键值对, 这个新键值对的键是一个包含字符串 "date" 的字符串对象, 而键值对的值则是一个包含字符串"2013.12.1"的字符串对象:
notion image

删除键

删除数据库中的一个键, 实际上就是在键空间里面删除键所对应的键值对对象。
book以及它的值将从键空间中被删除:
notion image

更新键

对一个数据库键进行更新, 实际上就是对键空间里面键所对应的值对象进行更新, 根据值对象的类型不同, 更新的具体方法也会有所不同。
message的值对象将从之前包含"hello world"字符串更新为包含"blah blah"字符串:
notion image
如果继续执行以下命令:
键空间中book键的值对象(一个哈希对象)将被更新, 新的键值对page320会被添加到值对象里面:
notion image

对键取值

对一个数据库键进行取值, 实际上就是在键空间中取出键所对应的值对象, 根据值对象的类型不同, 具体的取值方法也会有所不同。·
GET命令将首先在键空间中查找键 message , 找到键之后接着取得该键所对应的字符串对象值, 之后再返回值对象所包含的字符串 "helloworld" :
notion image
 

其他键空间操作

除了添加、删除、更新、取值操作之外, 还有很多针对数据库本身的Redis命令, 也是通过对键空间进行处理来完成的。
  • 用于清空整个数据库的 FLUSHDB 命令, 是通过删除键空间中的所有键值对来实现的
  • 用于随机返回数据库中某个键的 RANDOMKEY 命令, 是通过在键空间中随机返回一个键来实现的
  • 用于返回数据库键数量的DBSIZE命令, 是通过返回键空间中包含键值对的数量来实现的
类似的命令还有EXISTS、 RENAME、 KEYS等等
 

读写键空间时的维护操作

当使用Redis命令对数据库进行读写时, 服务器不仅会对键空间执行指定的读写操作, 还会执行一些额外的维护操作, 其中包括:
  • 在读取一个键之后(读操作和写操作都要对键进行读取), 服务器会根据键是否存在, 以此来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数, 这两个值可以在 INFO stats 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看。
  • 在读取一个键之后, 服务器会更新键的 LRU (最后一次使用)时间, 这个值可以用于计算键的闲置时间, 使用命令 OBJECT idletime 命令可以查看键 key 的闲置时间。
  • 如果服务器在读取一个键时, 发现该键已经过期, 那么服务器会先删除这个过期键, 然后才执行余下的其他操作
  • 如果有客户端使用 WATCH 命令监视了某个键, 那么服务器在对被监视的键进行修改之后, 会将这个键标记为脏(dirty), 从而让事务程序注意到这个键已经被修改过,
  • 服务器每次修改一个键之后, 都会对脏(dirty)键计数器的值增一, 这个计数器会触发服务器的持久化以及复制操作执行
  • 如果服务器开启了数据库通知功能, 那么在对键进行修改之后, 服务器将按配置发送相应的数据库通知
 
 

键的生成时间与过期时间

生存时间

  • 客户端可以通过 EXPIRE 或 PEXPIRE 命令以秒或毫秒精度为数据库中某个键设置生存时间(Time To Live,TTL)。经过指定时间后,服务器自动删除生存时间为0的键;
    • expireEXPIRE key seconds;将键key的生存时间设置为 seconds 秒;
    • pexpirePEXPIRE key milliseconds;与 EXPIRE 命令的作用类似,将键key的生存时间设置为 milliseconds 毫秒;
  • 可以通过 SETEX 命令在设置字符串键同时设置过期时间;
  • 使用 TTL 或 PTTL 命令获取键的剩余生存时间,通过计算键的过期时间和当前时间之间的差来实现
    • ttlTTL key;以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)
    • pttlPTTL key;这个命令类似于 TTL 命令,但它以毫秒为单位返回 key 的剩余生存时间,而不是像 TTL 命令那样,以秒为单位
 

过期时间

  • 客户端设置过期时间的命令是 EXPIREAT 或 PEXPIREAT
    • expireatEXPIREAT key timestamp;将键 key 的过期时间设置为 timestamp 所指定的秒数时间戳;
    • pexpireatPEXPIREAT key milliseconds-timestamp;与 EXPIREAT 命令的作用类似,将键 key 的过期时间设置为 milliseconds-timestamp 所指定的毫秒数时间戳;
  • 过期时间是一个 UNIX 时间戳
  • 移除过期时间
    • persistPERSIST key;移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )
 
虽然有多种不同单位和不同形式的设置命令,但实际上EXPIREPEXPIRE 和 EXPIREAT 三个命令都是使用PEXPIREAT 命令来实现的:
notion image
redisDb 结构的expires字典保存了数据库中所有键的过期时间,称这个字典为过期字典:过期字典的键是一个指针,指向键空间的某个键对象。过期字典的值是一个long long类型的整数,保存了键所指向的数据库键的过期时间——一个毫秒精度的unix时间戳。
notion image
如何判断一个键是否过期?
notion image
获取过期字典中指定键的过期时间:
  • 如果为空,则表示没有设置过期时间,不过期
  • 如果有过期时间,拿当前时间和过期时间比较:如果当前时间 > 过期时间 ,表示该键已经过期了,否则该键还没有过期
 

过期键删除策略

  • 定时删除
    • 在设置过期时间的时候,同时设置一个定时器任务,在键到期之后立即删除。
      优点:可以保证过期 key 会被尽快删除,也就是内存可以被尽快地释放。因此,定时删除对内存是最友好的。
      缺点:因为删除需要耗费CPU的性能,如果在相近的时间内大量的键失效了,那么立即执行的删除操作无疑会给CPU带来比较大的负担。
  • 惰性删除
    • 在到期之后,并不会立即进行删除,而是当访问到这个数据的时候在进行删除。也就是说,如果某个key过期之后,如果一直没有被再次访问,那这个key的数据就会一直存在。这就导致数据库中可能存在大量的使用不到的过期key的存在,会大量占用服务器的内存资源。惰性删除策略由 db.c/expireIfNeeded 函数实现,流程如下:
      notion image
      优点:惰性删除策略对 CPU 时间最友好
      缺点:只要这个过期key一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好
  • 定期删除
    • 每隔一段时间访问整个过期字典 ,从中删除已经过期的key。这种方式是对定时删除惰性删除的一种折中的方法,但是要注意定时的时长、过期数据的增加速度、删除数据时耗费的时间,不然反而更加影响性能。定期删除策略由 redis.c/activeExpireCycle 函数实现,每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数会被调用。在规定的时间内,分多次遍历服务器各个数据库,从数据库的expires字典中随机检查一部分过期时间,删除其中过期键;
 

Redis 过期删除策略

Redis选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用CPU时间和避免内存浪费之间取得平衡。
Redis 是怎么实现惰性删除的?
Redis的惰性删除策略由db.c文件中的expireIfNeeded函数实现:
Redis 在访问或者修改 key 之前,都会调用 expireIfNeeded 函数对其进行检查,检查 key 是否过期:
  • 如果过期,则删除该 key,至于选择异步删除,还是选择同步删除,根据 lazyfree_lazy_expire 参数配置决定(Redis 4.0版本开始提供参数),然后返回 null 客户端;
  • 如果没有过期,不做任何处理,然后返回正常的键值对给客户端;
notion image
 
Redis 是怎么实现定期删除的?
定期删除策略的做法:每隔一段时间「随机」从数据库中取出一定数量的key进行检查,并删除其中的过期key
 
这个间隔检查的时间是多长呢?
Redis中,默认每秒进行10次过期检查一次数据库,可通过Redis的配置文件redis.conf进行配置,配置键为hz它的默认值是 hz 10。
特别强调下,每次检查数据库并不是遍历过期字典中的所有 key,而是从数据库中随机抽取一定数量的 key 进行过期检查。
 
随机抽查的数量是多少呢?
定期删除的实现在expire.c文件下的activeExpireCycle函数中,其中随机抽查的数量由ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP定义的,它是写死在代码中的,数值是 20。也就是说,数据库每轮抽查时,会随机选择 20 个 key 判断是否过期。
 
Redis 的定期删除的流程:
  1. 从过期字典中随机抽取 20 个 key;
  1. 检查这 20 个 key 是否过期,并删除已过期的 key;
  1. 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
可以看到,定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。
notion image

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

  • 生成 RDB 文件
    • 在执行 SAVE 或 BGSAVE 命令创建一个新的 RBG 文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的 RDB 文件中;
  • 载入 RDB 文件
    • 载入 RDB 文件时:若服务器以主服务器模式运行,过期键不载入;若服务器以从服务器模式运行,所有键都会载入;
  • AOF 文件写入
    • 当服务器以 AOF 持久化模式运行时,数据库中的过期键没有被真的删除(被惰性删除或定期删除删除)的时候,不会对AOF文件造成影响。当过期键被惰性删除或定期删除后,程序会向 AOF 文件追加(append)一条 DEL 命令,显示记录该键已被删除;
  • AOF 重写 与 RDB 文件类似,在执行 AOF 重写过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的 AOF 文件中;
  • 复制
    • 当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制;
      主服务器:当惰性删除策略或定期删除策略发现键过期的时候,会删除过期键,并通知从服务器删除;
      从服务器:发现了过期键也不会进行处理,只有当主服务器发送删除指令的时候才会进行删除;
      Redis这样做是为了保证主从服务器上数据的一致性。
 
 

内存淘汰策略

过期删除策略,是删除已过期的 key,而当 Redis 的运行内存已经超过 Redis 设置的最大内存之后,则会使用内存淘汰策略删除符合条件的 key,以此来保障 Redis 高效的运行。

设置 Redis 最大运行内存

在配置文件redis.conf中,可以通过参数 maxmemory <bytes> 来设定最大运行内存,只有在 Redis 的运行内存达到了我们设置的最大运行内存,才会触发内存淘汰策略。 不同位数的操作系统,maxmemory的默认值是不同的:
  • 在 64 位操作系统中,maxmemory的默认值是 0,表示没有内存大小限制,不管用户存放多少数据到Redis中,Redis也不会对可用内存进行检查,直到Redis实例因内存不足而崩溃也无作为。
  • 在 32 位操作系统中,maxmemory的默认值是 3G,因为32位的机器最大只支持 4GB 的内存,而系统本身就需要一定的内存资源来支持运行,所以 32 位操作系统限制最大 3 GB 的可用内存是非常合理的,这样可以避免因为内存不足而导致Redis实例崩溃。
 

内存淘汰策略

  • 不进行数据淘汰的策略
    • noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,则会触发 OOM,但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。
  • 进行数据淘汰的策略
    • 细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略:
    • 在设置了过期时间的数据中进行淘汰:
      • volatile-random:随机淘汰设置了过期时间的任意键值
      • volatile-ttl:优先淘汰更早过期的键值
      • volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值
      • volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值
    • 在所有数据范围内进行淘汰:
      • allkeys-random:随机淘汰任意键值
      • allkeys-lru:淘汰整个键值中最久未使用的键值
      • allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值
 
Redis是如何实现 LRU 算法的
LRU 全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据。
传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:
  • 需要用链表管理所有的缓存数据,这会带来额外的空间开销;
  • 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
 
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个
Redis 实现的 LRU 算法的优点:
  • 不用为所有的数据维护一个大链表,节省了空间占用;
  • 不用在每次数据访问时都移动链表项,提升了缓存的性能;
 
LRU算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在Redis缓存中很长一段时间,造成缓存污染。因此,在Redis 4.0之后引入了LFU算法来解决这个问题。
Redis是如何实现LFU算法的
LFU 全称是 Least Frequently Used 翻译为最近最不常用,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
 
LFU 算法相比于 LRU 算法的实现,多记录了「数据的访问频次」的信息。Redis 对象的结构如下:
 
Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。
在 LRU 算法中,Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
在 LFU 算法中,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),低 8bit 存储 logc(Logistic Counter)。
notion image
  • ldt 是用来记录 key 的访问时间戳;
  • logc 是用来记录 key 的访问频次,它的值越小表示使用频率越低,越容易淘汰,每个新加入的 key 的logc 初始值为 5。
注意,logc 并不是单纯的访问次数,而是访问频次(访问频率),因为 logc 会随时间推移而衰减的
在每次 key 被访问时,会先对 logc 做一个衰减操作,衰减的值跟前后访问时间的差距有关系,如果上一次访问的时间与这一次访问的时间差距很大,那么衰减的值就越大,这样实现的 LFU 算法是根据访问频率来淘汰数据的,而不只是访问次数。访问频率需要考虑 key 的访问是多长时间段内发生的。key 的先前访问距离当前时间越长,那么这个 key 的访问频率相应地也就会降低,这样被淘汰的概率也会更大。
对 logc 做完衰减操作后,就开始对 logc 进行增加操作,增加操作并不是单纯的 + 1,而是根据概率增加,如果 logc 越大的 key,它的 logc 就越难再增加。
所以,Redis 在访问 key 时,对于 logc 是这样变化的:
  1. 先按照上次访问距离当前的时长,来对 logc 进行衰减;
  1. 然后,再按照一定概率增加 logc 的值
redis.conf 提供了两个配置项,用于调整 LFU 算法从而控制 logc 的增长和衰减:
  • lfu-decay-time 用于调整 logc 的衰减速度,它是一个以分钟为单位的数值,默认值为1,lfu-decay-time 值越大,衰减越慢;
  • lfu-log-factor 用于调整 logc 的增长速度,lfu-log-factor 值越大,logc 增长越慢。
 
 
可以使用config get maxmemory-policy命令,来查看当前Redis的内存淘汰策略:
设置内存淘汰策略有两种方法:
  • 通过“config set maxmemory-policy <策略>”命令设置。它的优点是设置之后立即生效,不需要重启Redis服务,缺点是重启Redis之后,设置就会失效
  • 通过修改Redis配置文件修改,设置“maxmemory-policy <策略>”,它的优点是重启 Redis 服务后配置不会丢失,缺点是必须重启 Redis 服务,设置才能生效。
 
 
 
 
 
 
 
 

数据库通知

  • 数据库通知是 Redis 2.8 版本新增的功能;
  • 数据库通知可以让客户端通过订阅给定的频道或模式,来获知数据库中键的变化,以及数据库中命令的执行情况;
  • Redis 命令对数据库进行修改后,服务器会根据配置向客户端发送数据库通知;
 
两类通知类型:
  • 键空间通知 key-space notification:关注 “某个键执行了什么命令”;SUBSCRIBE channel:message
  • 键事件通知 key-event notification:关注 “某个命令被什么键执行了”;SUBSCRIBE channel:del
Redis默认是不开启键空间通知的,如果要开启有两种方式:
1. 在配置文件中中配置notify-keyspace-events属性,若设置为KEA会开启所有可能的事件
2. 在客户端配置:使用命令config set notify-keyspace-events "KEA"开启所有通知
 
 
服务器配置的 notify-keyspace-events 选项决定服务器所发送通知的类型:
  • AKE:发送所有类型的键空间和键事件通知;
  • AK:发送所有类型的键空间通知;
  • AE:发送所有类型的键事件通知;
  • K$:只发送字符串有关的键空间通知;
  • EL:只发送列表键有关的键事件通知;
发送数据库通知的功能是由 notify.c/notifyKeyspaceEvent 函数实现;
 
 
通知的相关命令
  • 频道的订阅与退订
    • subscribe:SUBSCRIBE channel [channel ...];订阅给定的一个或多个频道的信息;
    • subscribe:SUBSCRIBE channel:message;关注 “某个键执行了什么命令”,返回键空间通知;
    • subscribe:SUBSCRIBE channel:del;关注 “某个命令被什么键执行了”,返回键事件通知;
    • unsubscribe:UNSUBSCRIBE [channel [channel ...]];退订给定的频道;
  • 模式的订阅与退订
    • psubscribe:PSUBSCRIBE pattern [pattern ...];订阅一个或多个符合给定模式的频道。以 /* 作为匹配符;
    • punsubscribe:PUNSUBSCRIBE [pattern [pattern ...]];退订所有给定模式;
  • 发送消息
    • publish:PUBLISH channel message;信息 message 发送到指定的频道 channel ;
  • 查看订阅信息
    • pubsub channels:PUBSUB CHANNELS [pattern];列出当前的活跃频道。给出 pattern 参数,那么只列出和给定模式 pattern 相匹配的那些活跃频道;、
    • pubsub numsub:PUBSUB NUMSUB [channel-1 ... channel-N];返回给定频道的订阅者数量, 订阅模式的客户端不计算在内。;
    • pubsub bumpat:PUBSUB NUMPAT;返回订阅模式的数量;
 
  • Redis
  • 对象的空转时长RDB持久化
    目录