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 便会创建一个 client 对象来表示一个客户端连接,后续收到该客户端的所有命令,都会基于创建的 client 进行。Redis 在为新连接创建 client 时,便会为其分配数据库,即 redisDb。代码如下所示:

client*createClient(intfd) {

client*c=zmalloc(sizeof(client));

   ......

selectDb(c,0);

   ......

}

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

intselectDb(client*c,intid) {

if(id<0||id>=server.dbnum)

returnC_ERR;

c->db=&server.db[id];

returnC_OK;

}

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

使用select命令

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

数据库的内部结构

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

typedefstructredisDb{

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 */

intid;/* Database ID */

longlongavg_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 存放数据的核心便是这些字典了

typedefstructdictEntry{

void*key;

union{

void*val;

uint64_tu64;

int64_ts64;

doubled;

}v;

structdictEntry*next;

}dictEntry;

typedefstructdictType{

uint64_t(*hashFunction)(constvoid*key);

void*(*keyDup)(void*privdata,constvoid*key);

void*(*valDup)(void*privdata,constvoid*obj);

int(*keyCompare)(void*privdata,constvoid*key1,constvoid*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. */

typedefstructdictht{

dictEntry**table;

unsignedlongsize;

unsignedlongsizemask;

unsignedlongused;

}dictht;

typedefstructdict{

dictType*type;

void*privdata;

dicththt[2];

longrehashidx;/* rehashing not in progress if rehashidx == -1 */

unsignedlongiterators;/* 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 一共支持 5 种基础数据结构:

string:字符串

list:列表

hash:字典

set:集合

zset:有序集合

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

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

voidsetGenericCommand(client*c,intflags,robj*key,robj*val,robj*expire,intunit,robj*ok_reply,robj*abort_reply) {

   ......

setKey(c->db,key,val);

   ......

}

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

voidsetKey(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()插入数据的逻辑:

voiddbAdd(redisDb*db,robj*key,robj*val) {

sdscopy=sdsdup(key->ptr);

intretval=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 存放到哈希桶中。

到此,<key, value>就被成功的存储到数据库中了。

至于从 Redis 中读取数据,那就更加简单了,也就是根据 key,从 redisDb 的 dict 中找到对应的 dictEntry,并返回 dictEntry 中存放的 value。

总结

总结下上面的源码分析:

(1)Redis 默认会创建 16 个数据库:redisDb,每个数据库之间数据隔离。

(2)Redis 默认为每个客户端分配第 0 号索引的 redisDb,客户端可以调用 select 命令切换需要使用的数据库。

(3)redisDb 内部采用了 HashTable 结构存放数据。

存放到client对应的redisDb中

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

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 存放到哈希桶中。

到此,<key, value>就被成功的存储到数据库中了。

查找:根据 key,从 redisDb 的 dict 中找到对应的 dictEntry,并返回 dictEntry 中存放的 value。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容