Redis是如何存取数据的

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介:

一:前言

这段时间事情比较多,难得抽出时间,便接着上篇文章《Redis是如何建立连接和处理命令的》,继续往下分析。Redis 本质就是数据库,要想深入了解Redis,那数据存取这一块肯定是大头。不过得益于 Redis 优良简洁的设计,数据存取倒没有那么复杂,源码读起来也比较轻松。

二:Redis 的数据库

Redis 对数据库进行了抽象,在 Redis 源码中,承担数据库角色的叫 redisDb。我们暂且无需去了解 redisDb 的内部结构,我们可以站在一个更加宏观的角度去初步了解它,这样能得到一个更全局的认识。

Redis服务可以同时配置多个 redisDb,每个redisDb的数据是相互隔离的。那么怎么配置多个 redisDb 呢?有过 redis 实战经验的同学肯定会说,这太简单了,我们只需要在 redis 的配置文件中配置 databases 即可。redis 默认配置的 redisDb 数量为 16。

Redis 用 redisServer 表示服务,redisServer 中有个数组 db,用来记录所有的 redisDb。当 Redis 进程启动后,便会在 initServer()中按照配置的 redisDb 数量,初始化好 Redis 服务的所有数据库。

server.db = zmalloc(sizeof(redisDb)*server.dbnum);

既然初始化了多个数据库,而每个数据库之间的数据又是隔离的,那么当客户端发存取命令的时候,Redis 服务又怎么知道使用那个数据库呢?诸位请往下看。

在上一篇文章中,我有提到过,每当一个新的客户端连接到 Redis 后,Redis 便会创建一个 client 对象来表示一个客户端连接,后续收到该客户端的所有命令,都会基于创建的 client 进行。

Redis 在为新连接创建 client 时,便会为其分配数据库,即 redisDb。代码如下所示:

client *createClient(int fd) {
    client *c = zmalloc(sizeof(client));
    ......
    selectDb(c,0);
    ......
}

selectDb(c,0)即为 client 分配 redisDb,第二个参数标志所分配的数据库在Redis服务中的索引,即第几个数据库。selectDb()逻辑很简单:

int selectDb(client *c, int id) {
    if (id < 0 || id >= server.dbnum)
        return C_ERR;
    c->db = &server.db[id];
    return C_OK;
}

现在我们知道了,事实上,所有客户端默认使用的都是Redis服务中的第一个redisDb。那么Redis 服务初始化这么多数据库干嘛呢?不是白费资源吗?

Redis 客户端有个 select 命令,使用 select 命令就可以选择使用那个 redisDb,这样不同客户端之间就实现了数据隔离。如调用 select 2,redis 服务在收到命令后,就会将该连接的数据库切换到索引为 2 的 redisDb。

三:数据库的内部结构

从宏观角度认识 redisDb 之后,我们便可以进入 redisDb 内部一探究竟。

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    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 */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

乍一看,redisDb 内部包括了好几个 dict,即字典。从注释来看,这些字典各有各的用处,如 dict 用来存放键值对,expires 用来存放key的超时时间。由此可见,Redis 存放数据的核心便是这些字典了。

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;

typedef struct dictType {
    uint64_t (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

dict 中利用 dictht 来存放数据,dictht 其实就是 HashTable,本质也是通过计算 key 的 hash 值,将数据分布到不同的桶之中。 这里比较有趣的是,一个 dict 中有两个 dictht,按道理只要一个 dictht 用来存放数据不就够了吗?其实平时用来存放数据的也就是 ht[0],只有当要进行 rehash 的时候,才会使用 ht[1],临时作为一个新的HashTable,存放新增数据。ht[0]中的存量数据会 rehash 到 ht[1] 中,等到 rehash 完成,ht[0] 就会再指向 ht[1] 的 dictht,完成职责交换。

dictht 中存放着一个二维指针:dictEntry **table ,第一维指针用来指向 dictEntry 链表,第二维指针指向dictEntry 链表中的某个dictEntry,dictEntry本身是也一个链表,记录着hash(key)相同的元素。

总结下,也就是说真正用来存放数据的就是 dictEntry,而 dictht 作为HashTable,将数据根据 key hash,存放到不同的 dictEntry 中,并通过 table 这个二维指针管理所有 dictEntry。

四:数据存取流程

聊完了数据库,再来聊聊Redis 是如何从数据库存取数据的。

Redis 一共支持 5 种基础数据结构:

  • string:字符串
  • list:列表
  • hash:字典
  • set:集合
  • zset:有序集合

今天我们从最简单的数据结构 string 入手,窥探下 Redis 内部设计。

在 Redis 客户端调用命令,Redis 服务收到命令后便会调用命令对应的处理函数,如调用 set a A ,Redis 对应的命令处理函数便为 t_string.c 中的 setCommand()。 setCommand()解析命令附带标志后,便调用了 setGenericCommand()处理数据。

void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    ......
    setKey(c->db,key,val);
    ......
}

setGenericCommand()中用来存放数据就一行代码,即 setKey(c->db,key,val),将数据存放到 client 对应的 redisDb 中。接下来的要看的逻辑,便就是 redisDb 如何存入 了。

void setKey(redisDb *db, robj *key, robj *val) {
    if (lookupKeyWrite(db,key) == NULL) {
        dbAdd(db,key,val);
    } else {
        dbOverwrite(db,key,val);
    }
    incrRefCount(val);
    removeExpire(db,key);
    signalModifiedKey(db,key);
}

setKey()首先会查询数据库中是否已存在相同的key,如果不存在,就调用 dbAdd()插入数据,否则调用 dbOverwrite()覆盖掉旧数据。

废话不多说,我们直接看 dbAdd()插入数据的逻辑:

void dbAdd(redisDb *db, robj *key, robj *val) {
    sds copy = sdsdup(key->ptr);
    int retval = dictAdd(db->dict, copy, val);
    ......
}

上面逻辑主要分为两步:
(1)调用sdsdup(),将 key 的 C 字符串转化为 redis 自定义的 sds 字符串,之所以将字符串由普通的字符数组转化为 sds,主要就是为了效率考虑,sds 规避了普通字符数组的很多问题。
(2)调用dictAdd(),将 sds 类型的key ,和 val 一起存入redisDb 的字典 dict 中。dictAdd()逻辑也不复杂,代码如下:

int dictAdd(dict *d, void *key, void *val)
{
    dictEntry *entry = dictAddRaw(d,key,NULL);

    if (!entry) return DICT_ERR;
    dictSetVal(d, entry, val);
    return DICT_OK;
}

dictAdd()同样分两步走:
(1)从dict 中找到 key 对应的哈希桶。
(2)调用dictSetVal(),将 value 存放到哈希桶中。到此,就被成功的存储到数据库中了。至于从 Redis 中读取数据,那就更加简单了,也就是根据 key,从 redisDb 的 dict 中找到对应的 dictEntry,并返回 dictEntry 中存放的 value。

五:总结

总结下上面的源码分析:
(1)Redis 默认会创建 16 个数据库:redisDb,每个数据库之间数据隔离。
(2)Redis 默认为每个客户端分配第 0 号索引的 redisDb,客户端可以调用 select 命令切换需要使用的数据库。
(3)redisDb 内部采用了 HashTable 结构存放数据。
今天我们通过最基本的数据结构 string 学习了Redis 存取数据的核心逻辑,其实 Redis 在数据存放过程中还有很多细节,如渐进式 rehash、过期 key 惰性删除等。有兴趣的同学可以关注下我的专栏,后续文章中我将继续分析 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
相关文章
|
4天前
|
缓存 NoSQL 关系型数据库
13- Redis和Mysql如何保证数据⼀致?
该内容讨论了保证Redis和MySQL数据一致性的几种策略。首先提到的两种方法存在不一致风险:先更新MySQL再更新Redis,或先删Redis再更新MySQL。第三种方案是通过MQ异步同步以达到最终一致性,适用于一致性要求较高的场景。项目中根据不同业务需求选择不同方案,如对一致性要求不高的情况不做处理,时效性数据设置过期时间,高一致性需求则使用MQ确保同步,最严格的情况可能涉及分布式事务(如Seata的TCC模式)。
51 6
|
4天前
|
存储 NoSQL 算法
09- Redis分片集群中数据是怎么存储和读取的 ?
Redis分片集群使用哈希槽分区算法,包含16384个槽(0-16383)。数据存储时,通过CRC16算法对key计算并模16383,确定槽位,进而分配至对应节点。读取时,根据槽位找到相应节点直接操作。
73 12
|
4天前
|
NoSQL Redis
05- Redis的数据淘汰策略有哪些 ?
Redis 提供了 8 种数据淘汰策略:挥发性 LRU、LFU 和 TTL(针对有过期时间的数据),挥发性随机淘汰,以及全库的 LRU、LFU 随机淘汰,用于在内存不足时选择删除。另外,还有不淘汰策略(no-eviction),允许新写入操作报错而非删除数据。
310 1
|
4天前
|
存储 NoSQL Redis
04- Redis的数据过期策略有哪些 ?
Redis的数据过期策略包括**惰性删除**和**定期删除**。惰性删除在取出key时检查是否过期,节省CPU但可能延迟清理。定期删除则每隔一定时间删除一批过期key,通过限制操作频率减少CPU影响。默认每秒扫描10次,随机抽取20个键,若25%已过期则继续检查,最大执行时间25ms。Redis使用这两种策略的结合以平衡内存和CPU使用。
17 1
|
4天前
|
NoSQL Redis
03- Redis的数据持久化策略有哪些 ?
Redis的数据持久化包括两种策略:RDB(全量快照)和AOF(增量日志)。RDB在指定时间间隔将内存数据集保存到磁盘,而AOF记录所有写操作形成日志。从Redis 4.0开始,支持RDB和AOF的混合持久化,通过设置`aof-use-rdb-preamble yes`。
21 1
|
4天前
|
存储 监控 负载均衡
保证Redis的高可用性是一个涉及多个层面的任务,主要包括数据持久化、复制与故障转移、集群化部署等方面
【5月更文挑战第15天】保证Redis高可用性涉及数据持久化、复制与故障转移、集群化及优化策略。RDB和AOF是数据持久化方法,哨兵模式确保故障自动恢复。Redis Cluster实现分布式部署,提高负载均衡和容错性。其他措施包括身份认证、多线程、数据压缩和监控报警,以增强安全性和稳定性。通过综合配置与监控,可确保Redis服务的高效、可靠运行。
25 2
|
4天前
|
存储 监控 NoSQL
Redis处理大量数据主要依赖于其内存存储结构、高效的数据结构和算法,以及一系列的优化策略
【5月更文挑战第15天】Redis处理大量数据依赖内存存储、高效数据结构和优化策略。选择合适的数据结构、利用批量操作减少网络开销、控制批量大小、使用Redis Cluster进行分布式存储、优化内存使用及监控调优是关键。通过这些方法,Redis能有效处理大量数据并保持高性能。
22 0
|
4天前
|
缓存 NoSQL 算法
17- 数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?
保证Redis中的20w数据为热点数据,可以通过设置Redis的LFU(Least Frequently Used)淘汰策略。这样,当数据库有1000万数据而Redis仅能缓存20w时,LFU会自动移除使用频率最低的项,确保缓存中的数据是最常使用的。
67 8
|
4天前
|
存储 NoSQL 关系型数据库
【Redis】Redis的特性和应用场景 · 数据类型 · 持久化 · 数据淘汰 · 事务 · 多机部署
【Redis】Redis的特性和应用场景 · 数据类型 · 持久化 · 数据淘汰 · 事务 · 多机部署
15 0
|
4天前
|
NoSQL Redis 数据库
Redis实现数据持久性主要依赖两种机制
【5月更文挑战第15天】Redis持久化包括RDB快照和AOF日志。RDB通过定时内存数据快照生成文件,恢复速度快但可能丢失部分数据;AOF记录每次写操作,实时性好但文件大、恢复慢。混合持久化兼顾两者优点,提供数据安全与性能平衡。用户可按需选择或组合使用策略。
7 2