「Redis」数据库空间模型

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis数据库空间模型

在关系型数据库如MySQL,数据库数据是按照行记录格式进行存储的。同理,我们常说Redis是一个键值对(Key-Value)构成的内存数据库,具体是以什么形式进行存储的,下面通过源码一看究竟。


数据结构


网络异常,图片无法展示
|

redis.h/redisServer 中记录了一个由 redis.h/redisDb 结构组成的数组,这里的每一个 redisDb 都是一个数据库,在Redis中默认数据库数量由 REDIS_DEFAULT_DBNUM 参数控制,默认是16。


typedef struct redisDb {


   // 数据库键空间,保存着数据库中的所有键值对

   dict *dict;                 /* The keyspace for this DB */


   // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳

   dict *expires;              /* Timeout of keys with a timeout set */


   // 正处于阻塞状态的键

   dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */


   // 可以解除阻塞的键

   dict *ready_keys;           /* Blocked keys that received a PUSH */


   // 正在被 WATCH 命令监视的键

   dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */


   struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */


   // 数据库号码

   int id;                     /* Database ID */


   // 数据库的键的平均 TTL ,统计信息

   long long avg_ttl;          /* Average TTL, just for stats */


} redisDb;


  • dict  以字典表数据结构存储真实的数据库数据
  • expires字典表数据结构存储的所有数据库键Key的过期时间数据


数据存储


网络异常,图片无法展示
|

之前剖析过 [Redis]数据结构与对象 ,其中底层数据结构的实现之一是 字典(hashtable) 。底层基础决定高层架构,因此Redis数据库的 构建、查找、新增、移除、过期 等也利用字典的特性完成的。
网络异常,图片无法展示
|


  • 键Key 每个键都是一个字符串对象
  • 值Value 每个值可以是字符串对象、列表对象、哈希对象、集合对象、有序集合对象的任意一种


数据查找


网络异常,图片无法展示
|

正是使用了字典表这种哈希表形式,使得Redis的键查找的时间复杂度为 O(1) ,查找处理速度非常快。


数据过期


惰性删除


网络异常,图片无法展示
|

Redis的过期删除策略在 redis.c/expireIfNeeded 中进行实现,所有对数据库 查询、删除、检查键 等操作都会经过该函数检查键是否过期,过期则进行移除操作。当检查到过期键存在时会还会进行AOF文件写入、通知Slave节点、发送事件订阅通知,最终进行键删除


/*

* 检查 key 是否已经过期,如果是的话,将它从数据库中删除。

* 返回 0 表示键没有过期时间,或者键未过期。

* 返回 1 表示键已经因为过期而被删除了。

*/

int expireIfNeeded(redisDb *db, robj *key) {


   // 取出键的过期时间

   mstime_t when = getExpire(db,key);

   mstime_t now;


   // 没有过期时间

   if (when < 0) return 0; /* No expire for this key */


   // 如果服务器正在进行载入,那么不进行任何过期检查

   if (server.loading) return 0;


   now = server.lua_caller ? server.lua_time_start : mstime();

   // 当服务器运行在 replication 模式时

   // 附属节点并不主动删除 key

   // 它只返回一个逻辑上正确的返回值

   // 真正的删除操作要等待主节点发来删除命令时才执行

   // 从而保证数据的同步

   if (server.masterhost != NULL) return now > when;


   // 运行到这里,表示键带有过期时间,并且服务器为主节点


   /* Return when this key has not expired */

   // 如果未过期,返回 0

   if (now <= when) return 0;


   /* Delete the key */

   server.stat_expiredkeys++;


   // 向 AOF 文件和附属节点传播过期信息

   propagateExpire(db,key);


   // 发送事件通知

   notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,

       "expired",key,db->id);


   // 将过期键从数据库中删除

   return dbDelete(db,key);

}


定期删除


网络异常,图片无法展示
|

Redis的定期删除策略在 redis.c/activeExpireCycle 函数实现,它会被Redis的周期性执行函数 redis.c/serverCron 函数执行时调用


  • 快模式 通过模式设定来计算函数超时时间,在快模式下默认为1000微妙,否则根据25%的CPU执行时间设置
  • 处理数量 数据库一般初始化16个,这里也会默认处理16个进行全扫描,在每个数据库redisdbexpires这个字典表中随机获取20个进行检查
  • 处理停止 当函数执行超时或已删除的过期键占当前总数据库带过期时间的键数量的25%则停止


void activeExpireCycle(int type) {

   /* This function has some global state in order to continue the work

    * incrementally across calls. */

   // 静态变量,用来累积函数连续执行时的数据

   static unsigned int current_db = 0; /* Last DB tested. */

   static int timelimit_exit = 0;      /* Time limit hit in previous call? */

   static long long last_fast_cycle = 0; /* When last fast cycle ran. */


   unsigned int j, iteration = 0;

   // 默认每次处理的数据库数量

   unsigned int dbs_per_call = REDIS_DBCRON_DBS_PER_CALL;

   // 函数开始的时间

   long long start = ustime(), timelimit;


   // 快速模式

   if (type == ACTIVE_EXPIRE_CYCLE_FAST) {

       /* Don't start a fast cycle if the previous cycle did not exited

        * for time limt. Also don't repeat a fast cycle for the same period

        * as the fast cycle total duration itself. */

       // 如果上次函数没有触发 timelimit_exit ,那么不执行处理

       if (!timelimit_exit) return;

       // 如果距离上次执行未够一定时间,那么不执行处理

       if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;

       // 运行到这里,说明执行快速处理,记录当前时间

       last_fast_cycle = start;

   }


   /* We usually should test REDIS_DBCRON_DBS_PER_CALL per iteration, with

    * two exceptions:

    *

    * 一般情况下,函数只处理 REDIS_DBCRON_DBS_PER_CALL 个数据库,

    * 除非:

    *

    * 1) Don't test more DBs than we have.

    *    当前数据库的数量小于 REDIS_DBCRON_DBS_PER_CALL

    * 2) If last time we hit the time limit, we want to scan all DBs

    * in this iteration, as there is work to do in some DB and we don't want

    * expired keys to use memory for too much time.

    *     如果上次处理遇到了时间上限,那么这次需要对所有数据库进行扫描,

    *     这可以避免过多的过期键占用空间

    */

   if (dbs_per_call > server.dbnum || timelimit_exit)

       dbs_per_call = server.dbnum;


   /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time

    * per iteration. Since this function gets called with a frequency of

    * server.hz times per second, the following is the max amount of

    * microseconds we can spend in this function. */

   // 函数处理的微秒时间上限

   // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默认为 25 ,也即是 25 % 的 CPU 时间

   timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;

   timelimit_exit = 0;

   if (timelimit <= 0) timelimit = 1;


   // 如果是运行在快速模式之下

   // 那么最多只能运行 FAST_DURATION 微秒

   // 默认值为 1000 (微秒)

   if (type == ACTIVE_EXPIRE_CYCLE_FAST)

       timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */


   // 遍历数据库

   for (j = 0; j < dbs_per_call; j++) {

       int expired;

       // 指向要处理的数据库

       redisDb *db = server.db+(current_db % server.dbnum);


       /* Increment the DB now so we are sure if we run out of time

        * in the current DB we'll restart from the next. This allows to

        * distribute the time evenly across DBs. */

       // 为 DB 计数器加一,如果进入 do 循环之后因为超时而跳出

       // 那么下次会直接从下个 DB 开始处理

       current_db++;


       /* Continue to expire if at the end of the cycle more than 25%

        * of the keys were expired. */

       do {

           unsigned long num, slots;

           long long now, ttl_sum;

           int ttl_samples;


           /* If there is nothing to expire try next DB ASAP. */

           // 获取数据库中带过期时间的键的数量

           // 如果该数量为 0 ,直接跳过这个数据库

           if ((num = dictSize(db->expires)) == 0) {

               db->avg_ttl = 0;

               break;

           }

           // 获取数据库中键值对的数量

           slots = dictSlots(db->expires);

           // 当前时间

           now = mstime();


           /* When there are less than 1% filled slots getting random

            * keys is expensive, so stop here waiting for better times...

            * The dictionary will be resized asap. */

           // 这个数据库的使用率低于 1% ,扫描起来太费力了(大部分都会 MISS)

           // 跳过,等待字典收缩程序运行

           if (num && slots > DICT_HT_INITIAL_SIZE &&

               (num*100/slots < 1)) break;


           /* The main collection cycle. Sample random keys among keys

            * with an expire set, checking for expired ones.

            *

            * 样本计数器

            */

           // 已处理过期键计数器

           expired = 0;

           // 键的总 TTL 计数器

           ttl_sum = 0;

           // 总共处理的键计数器

           ttl_samples = 0;


           // 每次最多只能检查 LOOKUPS_PER_LOOP 个键

           if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)

               num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;


           // 开始遍历数据库

           while (num--) {

               dictEntry *de;

               long long ttl;


               // 从 expires 中随机取出一个带过期时间的键

               if ((de = dictGetRandomKey(db->expires)) == NULL) break;

               // 计算 TTL

               ttl = dictGetSignedIntegerVal(de)-now;

               // 如果键已经过期,那么删除它,并将 expired 计数器增一

               if (activeExpireCycleTryExpire(db,de,now)) expired++;

               if (ttl < 0) ttl = 0;

               // 累积键的 TTL

               ttl_sum += ttl;

               // 累积处理键的个数

               ttl_samples++;

           }


           /* Update the average TTL stats for this database. */

           // 为这个数据库更新平均 TTL 统计数据

           if (ttl_samples) {

               // 计算当前平均值

               long long avg_ttl = ttl_sum/ttl_samples;

               

               // 如果这是第一次设置数据库平均 TTL ,那么进行初始化

               if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;

               /* Smooth the value averaging with the previous one. */

               // 取数据库的上次平均 TTL 和今次平均 TTL 的平均值

               db->avg_ttl = (db->avg_ttl+avg_ttl)/2;

           }


           /* We can't block forever here even if there are many keys to

            * expire. So after a given amount of milliseconds return to the

            * caller waiting for the other active expire cycle. */

           // 我们不能用太长时间处理过期键,

           // 所以这个函数执行一定时间之后就要返回


           // 更新遍历次数

           iteration++;


           // 每遍历 16 次执行一次

           if ((iteration & 0xf) == 0 && /* check once every 16 iterations. */

               (ustime()-start) > timelimit)

           {

               // 如果遍历次数正好是 16 的倍数

               // 并且遍历的时间超过了 timelimit

               // 那么断开 timelimit_exit

               timelimit_exit = 1;

           }


           // 已经超时了,返回

           if (timelimit_exit) return;


           /* We don't repeat the cycle if there are less than 25% of keys

            * found expired in the current DB. */

           // 如果已删除的过期键占当前总数据库带过期时间的键数量的 25 %

           // 那么不再遍历

       } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);

   }

}


总结


  • Redis根据数据容量采用不同的编码格式、数据结构构建了五种对象类型,我们常说Redis是一个Key-Value型数据库,主要是因为它采用了字典表形式对不同数据类型数据进行了空间构建和存储,充分利用了哈希表快速寻址特点,使得数据查询、处理拥有较高性能,也充分利用字典表rehash特点进行数据底层存储扩容
  • 之前介绍过Redis是一个以事件驱动为模型的单线程运行的服务,它定义了时间、文件两种类型事件共同协作在一个单线程环境下工作。键过期维护存储在一个等同于数据存储的字典表中,为了不较大影响Redis处理性能和维护成本,采用了惰性删除和定期删除共存策略,精巧考究的设计能看出作者对性能使用的深度思考以及很多自适应算法的存在,非常值得学习


参考


《Redis设计与实现》

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
canal 缓存 NoSQL
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
根据对一致性的要求程度,提出多种解决方案:同步删除、同步删除+可靠消息、延时双删、异步监听+可靠消息、多重保障方案
Redis缓存与数据库如何保证一致性?同步删除+延时双删+异步监听+多重保障方案
|
8天前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
161 1
|
2月前
|
消息中间件 存储 NoSQL
剖析 Redis List 消息队列的三种消费线程模型
Redis 列表(List)是一种简单的字符串列表,它的底层实现是一个双向链表。 生产环境,很多公司都将 Redis 列表应用于轻量级消息队列 。这篇文章,我们聊聊如何使用 List 命令实现消息队列的功能以及剖析消费者线程模型 。
90 20
剖析 Redis List 消息队列的三种消费线程模型
|
1月前
|
NoSQL Redis 数据库
Redis单线程模型 redis 为什么是单线程?为什么 redis 单线程效率还能那么高,速度还能特别快
本文解释了Redis为什么采用单线程模型,以及为什么Redis单线程模型的效率和速度依然可以非常高,主要原因包括Redis操作主要访问内存、核心操作简单、单线程避免了线程竞争开销,以及使用了IO多路复用机制epoll。
41 0
Redis单线程模型 redis 为什么是单线程?为什么 redis 单线程效率还能那么高,速度还能特别快
|
2月前
|
存储 关系型数据库 MySQL
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
查询服务器CPU、内存、磁盘、网络IO、队列、数据库占用空间等等信息
111 5
|
2月前
|
Oracle NoSQL 关系型数据库
主流数据库对比:MySQL、PostgreSQL、Oracle和Redis的优缺点分析
主流数据库对比:MySQL、PostgreSQL、Oracle和Redis的优缺点分析
275 2
|
22天前
|
SQL 存储 关系型数据库
mysql 数据库空间统计sql
mysql 数据库空间统计sql
40 0
|
2月前
|
前端开发 IDE 数据库连接
ThinkPHP6 模型层的模型属性,表映射关系,以及如何在控制层中使用模型层和模型层中的简单CRUD
本文详细介绍了ThinkPHP6中模型层的使用,包括模型属性设置、表映射关系、以及如何在控制层中使用模型层进行CRUD操作。
ThinkPHP6 模型层的模型属性,表映射关系,以及如何在控制层中使用模型层和模型层中的简单CRUD
|
2月前
|
消息中间件 存储 NoSQL
18)Redis 的发布订阅模型
18)Redis 的发布订阅模型
29 0
|
3月前
|
存储 缓存 NoSQL
Redis内存管理揭秘:掌握淘汰策略,让你的数据库在高并发下也能游刃有余,守护业务稳定运行!
【8月更文挑战第22天】Redis的内存淘汰策略管理内存使用,防止溢出。主要包括:noeviction(拒绝新写入)、LRU/LFU(淘汰最少使用/最不常用数据)、RANDOM(随机淘汰)及TTL(淘汰接近过期数据)。策略选择需依据应用场景、数据特性和性能需求。可通过Redis命令行工具或配置文件进行设置。
71 2