memcached学习笔记——存储命令源码分析下篇

上一篇回顾:《memcached学习笔记——存储命令源码分析上篇》通过分析memcached的存储命令源码的过程,了解了memcached如何解析文本命令和mencached的内存管理机制。

本文是延续上一篇,继续分析存储命令的源码。接上一篇内存分配成功后,本文主要讲解:1、memcached存储方式;2、add和set命令的区别。

memcached存储方式

哈希表(HashTable)

哈希表在实践中使用的非常广泛,例如编译器通常会维护的一个符号表来保存标记,很多高级语言中也显式的支持哈希表。 哈希表通常提供查找(Search),插入(Insert),删除(Delete)等操作,这些操作在最坏的情况下和链表的性能一样为O(n)。 不过通常并不会这么坏,合理设计的哈希算法能有效的避免这类情况,通常哈希表的这些操作时间复杂度为O(1)。 这也是它被钟爱的原因。

memcached是通过一个HashTable来存储所有的item(注:memcached的slab模型是管理内存的,HashTable是用来存储数据的),memcached中HashTable的哈希冲突解决方法就是链接法,memcached的如此高效查询可以归功于HashTable。

memcached的HashTable是声明和操作在assoc.c文件中,下面我们先看看memcached的HashTable声明和相关操作定义

 1 /* primary_hashtable是主要的HashTable */
 2 static item** primary_hashtable = 0;
 3
 4 /* 如果memcached对HashTable进行扩展,那么旧的数据就会被存放在old_hashtable */
 5 static item** old_hashtable = 0;
 6
 7 /* HashTable中保存item的数量 */
 8 static unsigned int hash_items = 0;
 9
10 /* expanding是标记HashTable是否进行了扩展,如果进行了扩展,那么进行查询的时候就会在primary_hashtable和old_hashtable中查询,否则只会在primary_hashtable中查询 */
11 static bool expanding = false;
12
13 /* HashTable初始化函数 */
14 void assoc_init(const int hashpower_init);
15 /* HashTable查询操作 */
16 item *assoc_find(const char *key, const size_t nkey, const uint32_t hv);
17 /* HashTable插入操作 */
18 int assoc_insert(item *item, const uint32_t hv);
19 /* HashTable删除操作 */
20 void assoc_delete(const char *key, const size_t nkey, const uint32_t hv);

memcached内存分配成功后,返回新item,接着把这item保存到HashTable中,complete_nread_ascii > store_item > do_store_item

在complete_nread_ascii(memcached.c)中store_item(thread.c)根据返回的结果,向客户端返回本次命令的最终结果

 1 static void complete_nread_ascii(conn *c) {
 2     assert(c != NULL);
 3
 4     item *it = c->item;
 5     int comm = c->cmd;
 6     enum store_item_type ret;
 7
 8     pthread_mutex_lock(&c->thread->stats.mutex);
 9     c->thread->stats.slab_stats[it->slabs_clsid].set_cmds++;
10     pthread_mutex_unlock(&c->thread->stats.mutex);
11
12     if (strncmp(ITEM_data(it) + it->nbytes - 2, "\r\n", 2) != 0) {
13         out_string(c, "CLIENT_ERROR bad data chunk");
14     } else {
15       ret = store_item(it, comm, c);  // memcached存储item操作
16
17
18     //........
19
20
21       switch (ret) {
22       case STORED:
23           out_string(c, "STORED");       // 存储成功后客户端得到的结果
24           break;
25       case EXISTS:
26           out_string(c, "EXISTS");
27           break;
28       case NOT_FOUND:
29           out_string(c, "NOT_FOUND");
30           break;
31       case NOT_STORED:
32           out_string(c, "NOT_STORED");  // 我们通过add存储一个已经存在的key的时候会得到这样的结果
33           break;
34       default:
35           out_string(c, "SERVER_ERROR Unhandled storage type.");
36       }
37
38     }
39
40     //.........
41 }

store_item(thread.c)

 1 enum store_item_type store_item(item *item, int comm, conn* c) {
 2     enum store_item_type ret;
 3     uint32_t hv;
 4
 5     // memcached根据hash算法计算出当前item的key的hash值hv
 6     hv = hash(ITEM_key(item), item->nkey, 0);
 7
 8     item_lock(hv);
 9     // memcached存储item的核心操作
10     ret = do_store_item(item, comm, c, hv);
11     item_unlock(hv);
12     return ret;
13 }

do_store_item(memcached.c)

 1 enum store_item_type do_store_item(item *it, int comm, conn *c, const uint32_t hv) {                                        //comm 是命令
 2     char *key = ITEM_key(it);
 3
 4     // 通过key和hv哈希值查询HashTable(assoc_find,后面会讲解)中是否已经存在对应的item
 5     item *old_it = do_item_get(key, it->nkey, hv);
 6
 7     // 存储的结果,初始值是NOT_STORED
 8     enum store_item_type stored = NOT_STORED;
 9
10     item *new_it = NULL;
11     int flags;
12
13     if (old_it != NULL && comm == NREAD_ADD) {
14         /* add only adds a nonexistent item, but promote to head of LRU */
15         // 数据项不为空, 更新时间
16         // 如果调用的是add命令并且,之前已经存在了相应的key的,那么就只是修改使用时间,stored的值还是NOT_STORED,
17         // 所以调用add来添加已经存在的项,会得到NOT_STORED的结果,key对应的value没有改变,在complete_nread输出信息
18         do_item_update(old_it);
19
20     } else if (!old_it && (comm == NREAD_REPLACE
21         || comm == NREAD_APPEND || comm == NREAD_PREPEND))
22     {
23
24         // ..........
25
26     } else if (comm == NREAD_CAS) {
27
28         // ............
29
30     } else {
31         // old_it为空,并且comm为add、set、replace、append;或者old_it不为空,并且comm为set、replace、append
32         /*
33          * Append - combine new and old record into single one. Here it‘s
34          * atomic and thread-safe.
35          */
36         if (comm == NREAD_APPEND || comm == NREAD_PREPEND) {
37
38             // ..........
39
40         }
41
42         // 存储的结果,初始值是NOT_STORED
43         if (stored == NOT_STORED) {
44             if (old_it != NULL)
45                 item_replace(old_it, it, hv);    // old_it不为空,并且命令为set:在HashTable中用新的item it替换旧的item old_it
46             else
47                 do_item_link(it, hv);      // old_it为空,并且命令为add、set:那么就把item it插入到HashTable中
48
49             c->cas = ITEM_get_cas(it);
50
51             stored = STORED;   // 修改存储的结果
52         }
53     }// end if
54
55
56     // .........
57
58     return stored;
59 }

最后我们看看assoc_find函数,HashTable的查询操作

 1 item *assoc_find(const char *key, const size_t nkey, const uint32_t hv) {
 2     item *it;
 3     unsigned int oldbucket;
 4
 5     // 正如我们上面:expanding是标记HashTable是否进行了扩展,如果进行了扩展,那么进行查询的时候就会在primary_hashtable和old_hashtable中查询,否则只会在primary_hashtable中查询
 6     // 这里通过key和hv哈希值,先定位item所在链表
 7     if (expanding &&
 8         (oldbucket = (hv & hashmask(hashpower - 1))) >= expand_bucket)
 9     {
10         it = old_hashtable[oldbucket];
11     } else {
12         it = primary_hashtable[hv & hashmask(hashpower)];
13     }
14
15     item *ret = NULL;
16     int depth = 0;
17     // 遍历上面找到的链表it,从it中查询key对应的item
18     // 返回找到的item ret,否则就返回NULL
19     while (it) {
20         if ((nkey == it->nkey) && (memcmp(key, ITEM_key(it), nkey) == 0)) {
21             ret = it;
22             break;
23         }
24         it = it->h_next;
25         ++depth;
26     }
27     MEMCACHED_ASSOC_FIND(key, nkey, depth);
28     return ret;
29 }

add和set命令的区别

从do_store_item函数中可以看出,1)如果是add命令,但是key对应的value已经存在,那么只是更新key的最近使用时间,value没有被新的value覆盖,返回NOT_STORED的结果;2)如果是add命令,第一次存储,那么value就会添加到HashTable中,返回STORED结果;3)如果是set命令,无论key对应的value是否已经存在,最后新的value会插入到HashTable中,返回STORED结果

最后,感谢各位在大冷天的看完小弟拙文,谢谢!

(完)

更多文章请移步:www.hcoding.com

时间: 2024-10-26 13:39:16

memcached学习笔记——存储命令源码分析下篇的相关文章

memcached学习笔记——存储命令源码分析上

原创文章,转载请标明,谢谢. 上一篇分析过memcached的连接模型,了解memcached是如何高效处理客户端连接,这一篇分析memcached源码中的process_update_command函数,探究memcached客户端的set命令,解读memcached是如何解析客户端文本命令,剖析memcached的内存管理,LRU算法是如何工作等等. 解析客户端文本命令 客户端向memcached server发出set操作,memcached server读取客户端的命令,客户端的连接状态

Hadoop学习笔记(10) ——搭建源码学习环境

Hadoop学习笔记(10) ——搭建源码学习环境 上一章中,我们对整个hadoop的目录及源码目录有了一个初步的了解,接下来计划深入学习一下这头神象作品了.但是看代码用什么,难不成gedit?,单步调试呢? 看程序不能调那多痛苦啊,想看跟踪一下变量,想看一下执行路径都难. 所以这里,我们得把这个调试环境搭建起来.Hadoop的主要代码是用java编写的,所以这里就选用eclipse作为环境. Hadoop目录下,本身就可以为作eclipse的一个工程来操作,但这里我不想,我想自己来建一个工程,

u-boot学习(三):u-boot源码分析

建立域模型和关系数据模型有着不同的出发点: 域模型: 由程序代码组成, 通过细化持久化类的的粒度可提高代码的可重用性, 简化编程 在没有数据冗余的情况下, 应该尽可能减少表的数目, 简化表之间的参照关系, 以便提高数据的访问速度 Hibernate 把持久化类的属性分为两种: 值(value)类型: 没有 OID, 不能被单独持久化, 生命周期依赖于所属的持久化类的对象的生命周期 实体(entity)类型: 有 OID, 可以被单独持久化, 有独立的生命周期(如果实体类型包含值类型,这个值类型就

EasyUI学习总结(三)——easyloader源码分析

EasyUI学习总结(三)--easyloader源码分析 easyloader模块是用来加载jquery easyui的js和css文件的,而且它可以分析模块的依赖关系,先加载依赖项.模块加载好了会调用parse模块来解析页面.把class是easyui开头的标签都转化成easyui的控件. 先看Demo1例子,再分析源代码. 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>easyloader范例</tit

EasyUI学习总结(四)——parser源码分析

EasyUI学习总结(四)--parser源码分析 parser模块是easyloader第一个加载的模块,它的主要作用,就是扫描页面上easyui开头的class标签,然后初始化成easyui控件. 1 /** 2 * parser模块主要是解析页面中easyui的控件 3 */ 4 $.parser = { 5 // 是否自动解析 6 auto: true, 7 8 // 可以被解析的控件 9 plugins:['linkbutton','menu','menubutton','splitb

Hadoop-2.4.1学习之NameNode -format源码分析

在Hadoop-2.X中,使用hdfs namenode –format对文件系统进行格式化.虽然无论是在生产环境还是测试环境中,已经使用了该命令若干次了,也大体了解该命令就是在本地文件系统中创建几个文件夹,但具体是如何执行的则需要通过阅读源代码来了解了. 要想看到该命令的源代码,需要看看hdfs脚本中是如何执行相应的java类的.在hdfs脚本中,下面的语句说明了实际执行的java类和参数,其中类为org.apache.hadoop.hdfs.server.namenode.NameNode,

Hadoop-2.4.1学习之Map任务源码分析(下)

在Map任务源码分析(上)中,对MAP阶段的代码进行了学习,这篇文章文章将学习Map任务的SORT阶段.如果Reducer的数量不为0,则还需要进行SORT阶段,但从上面的学习中并未发现与MAP阶段执行完毕调用mapPhase.complete()类似的在SORT阶段执行完毕调用sortPhase.complete()的源码,那SORT阶段是在什么时候启动的?对于Map任务来说,有输入就有输出,输入由RecordReader负责,输出则由RecordWriter负责,当Reducer的数量不为0

docker stats 命令源码分析

本文是基于docker 1.10.3版本的源码,对docker stats命令进行源码分析,看看docker stats命令输出的数据是从cgroups fs中怎么怎么计算出来的. docker client相关代码入口可参考:/docker/docker/api/client/stats.go#141 docker daemon相关代码入口可参考:/docker/docker/daemon/daemon.go#1474 源码分析结果 Cpu数据: docker daemon会记录这次读取/sy

EasyUI学习总结(三)——easyloader源码分析(转载)

声明:这一篇文章是转载过来的,转载地址忘记了,原作者如果看到了,希望能够告知一声,我好加上去! easyloader模块是用来加载jquery easyui的js和css文件的,而且它可以分析模块的依赖关系,先加载依赖项.模块加载好了会调用parse模块来解析页面.把class是easyui开头的标签都转化成easyui的控件. 先看Demo1例子,再分析源代码. 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>ea