Redis代码阅读心得

1. 只在必要的地方打错误日志,无须一层层抛出去,很多错误在当前函数就是明确的。一般的C操作都无须判断返回。c程序,只有两个地方可能有错,1.文件打开读写2.内存申请

例如这样的地方

server.clients = listCreate();

listCreate内部有内存申请,但无须判断client是否为NULL,后面如果用到clients的地方访问到了null,会有内存访问错,触发SIGSEGV信号,详见 http://blog.ddup.us/?p=89

2. 提供宏来访问结构对象,保持执行效率和代码内聚性的平衡

typedef struct list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void *key);
    unsigned int len;
} list;

/* Functions implemented as macros */
#define listLength(l) ((l)->len)
#define listFirst(l) ((l)->head)
#define listLast(l) ((l)->tail)

3.哈希表的实现

http://huangz.iteye.com/blog/1455808

这里有比较深入浅出的代码说明,redis的实现特色在于平摊式的rehash,避免一次操作过多数据。

值得一提的是,这篇文章里面说

唯一的遗憾是,dictht 作为字典的内部实现,本该被隐藏起来的,现在却完全暴露了出来

其实看第2条,暴露内部实现同时通过宏来访问数据,可以提高效率,因为函数调用是有开销的。

“隐藏内部实现“是面向对象的教条,只是怕多人合作的时候有人不知道某些数据不能访问。

只要写代码的人约定通过函数或者宏来访问对象,而非直接操作对象的数据,那么封装原则就是满足的。

这再次证明了java的优点在于“限制不靠谱的人写不靠谱的代码”。

另,关于hash表的大小

typedef struct dictht {
    dictEntry **table;      // 节点指针数组
    unsigned long size;     // 桶的数量
    unsigned long sizemask; // mask 码,用于地址索引计算
    unsigned long used;     // 已有节点数量
} dictht;

其中size是桶的数量,redis的实现把这个数量定为2^n,也就是说每次hash表扩展的时候,桶的数量定为4,8,16,32这样的递增序列。为什么要这么定呢?

继续看代码

n.sizemask = realsize-1;

h = dictHashKey(d, de->key) & d->ht[1].sizemask;

也就是说,sizemask恒为size-1,size为2^n,则sizemask的2进制表示为11111..(n个1)。

一般来说,计算一个节点的hash值后,将这个 (hash值 % 指针数组的长度) 得到应该把这个节点放到数组的第几个元素为头指针的链表上。而redis的聪明在于避免%的操作,用快速的&操作来代替,因为 

X & (2^n -1) == X % 2^n

X/2相当于将这个二进制数的X右移n位,而X%2相当于取X最低的N位的值,例如

9 / 4 = 01001 >> 2 = 010 = 2

9 % 4 = 01001 & 011 = 01 = 1

其中 01000是4可以整除的部分,01是余数部分。这样就能够快速求值,避免%的操作。

继续说,hash表的各种操作都要兼顾内部有两个表,同时还要保持功能的正交,迭代器的实现,只有在单线程的情况下能写出这样的hash表。

4.代码规范

私有函数以下划线开头

变量和函数命名为驼峰法(其实我不喜欢这个)

先include公共头文件,后include私有头文件

5.lzf压缩算法

pass,对算法一窍不通,也许以后能用一下。

6.ae.c这是个专门的话题,看到接口,我就有问题:

/* State of an event based program */
typedef struct aeEventLoop {
    int maxfd;
    long long timeEventNextId;
    aeFileEvent events[AE_SETSIZE]; /* Registered events */
    aeFiredEvent fired[AE_SETSIZE]; /* Fired events */
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* This is used for polling API specific data */
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;


/* Prototypes */
aeEventLoop *aeCreateEventLoop(void);  /* 创建,毫无疑问的 */

void aeDeleteEventLoop(aeEventLoop *eventLoop); /* 销毁,也毫无疑问 */


void aeStop(aeEventLoop *eventLoop); /* 1.停止是什么意思? */


int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData); /* 注册一个文件事件,因该是网络套接字的fd,2.对普通文件效果如何? */


void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask); /* 删除事件,3. mask是什么用的? */


int aeGetFileEvents(aeEventLoop *eventLoop, int fd); /* 4. get并不返回,有什么用? */


long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc); /* 5.final最终是干什么的? */


int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id); /*删掉某个id的时间事件,无问题 */


int aeProcessEvents(aeEventLoop *eventLoop, int flags); /*6.语义不明*/


int aeWait(int fd, int mask, long long milliseconds); /* 7.不明 */



void aeMain(aeEventLoop *eventLoop); /* 8.应该是启动事件主循环*/


char *aeGetApiName(void); /* 9. 不明 */


void aeSetBeforeSleepProc(aeEventLoop *eventLoop, aeBeforeSleepProc *beforesleep); /*设置一个sleep之前的处理函数,10.程序怎么知道自己要sleep? */

在浏览完ae.c后,我来回答这些问题:

6.1 简单的把eventLoop->stop = 1;在aeMain里面做判断,用于终止事件循环

    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }

6.2 eventLoop->events[fd]是用来存储某个事件的,并没有标志位的功能,epoll(假定用epoll),返回一个事件的数组,所以无须担心还需要遍历的问题。普通文件也能用,这样就引申出一个问题,6.11如何区分客户端命令的边界?

6.3 maxfd是用在process里面的,如果maxfd为-1,说明目前没有注册的描述符,就不用处理了

if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {

mask是用来减去的,并不一点是清0,可能是减掉。

6.4 返回的是相应fd的mask

6.5 用于在aeDeleteTimeEvent的时候调用,不过不知道实际例子是什么。

6.6 wait完了之后,处理已经产生的文件事件,或者时间事件。这个函数首先遍历时间事件的链表,找到最近需要处理的时间事件,然后把未来时间-当前时间作为wait的参数(等待n秒),wait完毕后执行读或者写的事件。同时遍历TimeEvent的链表,寻找当前已经过时的TimeEvent来执行。

6.7 select 的简单包装,用于syncio.c,在指定时间内同步读写相关函数。

6.8 启动事件主循环函数,唯一的问题是eventLoop->beforesleep(eventLoop);不知道在实际中怎么用6.12

6.9  返回api的名字,epoll,select或者kqueue,给客户端的INFO命令显示用

6.10 程序用epoll等方法来等待有事件产生。 

6.11

6.12

值得一体的是,ae_epoll.c里面用的是水平触发(LT),不是边缘触发(ET),效率稍低一点。不知道有什么意义?

 

 

你可能感兴趣的:(Redis代码阅读心得)