设为首页 收藏本站
查看: 669|回复: 0

[经验分享] redis 分析 简介

[复制链接]

尚未签到

发表于 2016-12-18 10:29:08 | 显示全部楼层 |阅读模式
1           简介
redis是一个类似memcachedkey/value存储系统,它支持存储的value类型相对较多,包括string(字符串)list(链表)set(集合)zset(有序集合)。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
2           分析
2.1          协议
本文是基于当前最新版本redis 1.2.6版本进行阅读分析的,支持的主要功能协议列表如下:
  string
  get/set/setnx/del/exists/incr/decr/mget
  list
  rpush/lpush/rpop/lpop/llen/lindex/lset/lrange/ltrim/lrem/rpoplpush
  set
  sadd/srem/smove/sismember/scard/spop/srandmember/
  sinter/sinterstore/sunion/sunionstore/sdiff/sdiffstore/smembers
  zset
  zadd/zincrby/zrem/zremrangebyscore/zrange/zrangebyscore/
  zcount/zrevrange/zcard/zscore/incrby/descrby/
select/move/rename/renamenx/expire/expireat/sort/sync

 
  请求采用文本协议,整体分为两种:non-bulk(不带二进制数据)multi-bulk(带二进制数据的)协议,具体协议格式分别如下:
  non-bulk
  command argv … argv  bulk_len\r\n
  bulk_data\r\n

2.2          总体结构
程序运行的主流程如下图所示:
DSC0000.png

2.3          事件循环
redis网络实现没有采用开源的网络框架,具体的源文件为ae.hae.c。用aeEventLoop保存基于事
  件系统的事件主循环,把event分为句柄事件(FileEvent)和超时事件(TimeEvent),用aeFiredEvent保存激活后的fdevent。相关的数据结构如下:
          /* File event structure */
  typedef  struct aeFileEvent {
      int mask; /* one of  AE_(READABLE|WRITABLE|EXCEPTION) */
      aeFileProc  *rfileProc;
      aeFileProc  *wfileProc;
      aeFileProc  *efileProc;
      void *clientData;
  }  aeFileEvent;
  /* Time event structure */
  typedef struct aeTimeEvent {
       long long id; /* time event identifier. */
       long when_sec; /* seconds */
       long when_ms; /* milliseconds */
       aeTimeProc *timeProc;
       aeEventFinalizerProc *finalizerProc;
       void *clientData;
       struct aeTimeEvent *next;
  } aeTimeEvent;
  /* A fired event */
  typedef struct aeFiredEvent {
       int fd;
       int mask;
  } aeFiredEvent;
  /* 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  */
  } aeEventLoop;
  回调接口的定义如下:
  // 句柄事件回调函数
  typedef void aeFileProc(struct aeEventLoop  *eventLoop, int fd, void *clientData, int mask);
  // 超时回调函数
  typedef int aeTimeProc(struct aeEventLoop  *eventLoop, long long id, void *clientData);
  // 超时event删除回调函数
  typedef void aeEventFinalizerProc(struct  aeEventLoop *eventLoop, void *clientData);
  在网络相关操作中,定义了一组公共操作接口:aeApiCreate/  aeApiFree/aeApiAddEvent/aeApiDelEvent/aeApiPoll/aeApiName。在ae_epoll.cae_kqueue.cae_select.c中,分别实现了基于epoll/kqueueselect系统调用的接口。在ae.c中会根据宏来include对应的文件,系统调用的选择顺序依次为epoll -> kqueue -> select
  事件主循环实现在aeMain中,内部会调用aeProcessEvents函数。函数内部会先调用aeApiPoll处理fd上的注册事件,若fd上有对应事件激活,调用对应的aeFileProc进行处理;然后调用processTimeEvents函数扫描超时链表处理已超时的超时事件,调用对应的回调aeTimeProc处理。需要注意的是,aeEventLoop中采用单向链表实现了超时event链表,并且插入timeEvent时没有保证按超时时刻有序,导致每次查找(aeSearchNearestTimer)离当前时间最近的timeEvent的复杂度为O(N)aeDeleteTimeEvent删除timeEvent时的复杂度也为O(N), 删除timeEvent后需要从链表开始处重新查找已超时的timeEvent。在timeEvent链表长度较长时,操作效率会相对较低。
2.4          数据结构
2.4.1         sds (字符串)
redis采用结构sdshdrsds封装了字符串,字符串相关的操作实现在源文件sds.h/sds.c中。sdshdr
  数据结构定义如下:
typedef char  *sds;
struct sdshdr  {
    long len;
    long free;
    char buf[];
};
2.4.2         list(双向链表)
list的定义和实现在源文件adlist.h/adlist.c,相关的数据结构定义如下:
//  list迭代器
typedef struct  listIter {
    listNode *next;
    int direction;
}  listIter;
 
//  list数据结构
typedef struct  list {
    listNode *head;
    listNode *tail;
    void *(*dup)(void *ptr);
    void (*free)(void *ptr);
    int (*match)(void *ptr, void  *key);
    unsigned int len;
    listIter iter;
}  list;
2.4.3         dict(hash)
在源文件dict.h/dict.c中实现了hashtable的操作,数据结构的定义如下:
//  dict中的元素项
typedef struct  dictEntry {
    void *key;
    void *val;
    struct dictEntry *next;
}  dictEntry;
 
//  dict相关配置函数
typedef struct  dictType {
    unsigned int (*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;
 
//  dict定义
typedef struct  dict {
    dictEntry **table;
    dictType *type;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
    void *privdata;
}  dict;
 
//  dict迭代器
typedef struct  dictIterator {
    dict *ht;
    int index;
    dictEntry *entry, *nextEntry;
}  dictIterator;
          dicttabledictEntry指针的数组,数组中每个成员为hash值相同元素的单向链表。set是在dict的基础上实现的,指定了key的比较函数为dictEncObjKeyCompare,若key相等则不再插入。
2.4.4         zset(排序set)
typedef struct  zskiplistNode {
    struct zskiplistNode **forward;
    struct zskiplistNode *backward;
    double score;
    robj *obj;
}  zskiplistNode;
 
typedef struct  zskiplist {
    struct zskiplistNode *header,  *tail;
    unsigned long length;
    int level;
}  zskiplist;
 
typedef struct  zset {
    dict *dict;
    zskiplist *zsl;
}  zset;
zset利用dict维护key -> value的映射关系,用zsl(zskiplist)保存value的有序关系。zsl实际是叉数
  不稳定的多叉树,每条链上的元素从根节点到叶子节点保持升序排序。如果把zsl中出现在不同层的同一个节点当作不同的节点,则整颗树上只有根节点包含多个子节点,其他的子节点都至多有一个子节点。insertNode的指针可能会插入到多条链中,实现时保证插入的每一个元素会出现在下标为0的左子树上,因此可以通过该左子树遍历所有元素。
  zsl的结构图如下所示:
DSC0001.png  

          插入节点时会随机生成levellevel决定了节点会插入到哪几条路径上。由于level>=1,因此每个节点都会在下标为0的路径上存在。可以看到,level越大的路径上节点数目越少。在查找>=score的节点时,利用这一特点,按照level递减的顺序从最大的level对应的路径开始查找。找到该路径上小于score的最大节点x后,对level-1转换路径从x->forward继续开始查找,直到level最后减为0。返回x->forward[0],若不存在>=score的元素,则x->forward[0]NULL
          zsl的插入过程如下图所示:
  
DSC0002.png  

2.5          主从(master-slave)同步
启动时slave会向master发出指令SYNC进行全数据同步,slave会阻塞式等待数据同步完成加载到
  内存中进行初始化后,才能开始请求处理。运行时数据同步是通过master转发请求给slave列表实现的,仅转发对数据有修改操作的请求。同步的状态机如下:
DSC0003.png  

2.6          存储文件格式
redis使用了两种文件格式:全量数据和增量请求。全量数据格式是把内存中的数据写入磁盘,
  便于下次读取文件进行加载;增量请求文件则是把内存中的数据序列化为操作请求,用于读取文件进行replay得到数据,序列化的操作包括SETRPUSHSADDZADD
redisinterger根据值范围采用不同的编码存储,具体如下:
  值范围
  字节数(byte)
  编码格式
  [0, 1 << 6 –  1]
  1
  00 | value
  [1 << 6, 1 << 14 –  1]
  2
  01 | (value >> 8) | value &  oxFF
  [1 << 14, 1 << 31 –  1]
  5
  10 000000 |  htonl(value)

 
 
 
redis对数值类的string object编码存储格式如下:
  值范围
  字节数(byte)
  编码格式
  [-(1 << 7), 1 << 7 –  1]
  2
  1100 0000 | value &  0xFF
  [-(1 << 15), 1 << 15 –  1]
  3
  1100 0001 |
  (value >> 8) &  0xFF
  | value &  oxFF
  [-(1 << 31)], 1 << 31 –  1]
  5
  1100 0010 |  
  value & oxFF |
  (value >> 8) &  0xFF
  | (value >> 16) & 0xFF  
  | (value >> 24) &  0xFF

 
redis支持字符串压缩存储,压缩的编码格式如下:
  1100  0011
  compl_len
  (压缩后的长度)
  orig_len
  (压缩前的长度)
  comp_value
  (压缩后的内容)

 
2.6.1         data文件格式
 

2.6.2         append文件格式
 
  RedisDb
  *2
  $6
  SELECT
  $length(index)
  index:long
  entry
  *3
  $3
  SET
  $length(key)
  key
  $length(value)
  value
  entry
  *3
  $5
  RPUSH
  $length(key)
  key
  $length(value)
  value
  entry
  *3
  $4
  SADD
  $length(key)
  key
  $length(value)
  value
  entry
  *3
  $4
  ZADD
  $length(key)
  key
  $length(score)
  score:double
  $length(value)
  value
entry
  …
  RedisDb
  …
  
  
  
  RedisDb
  …
  entry
  entry
  entry
 
3           问题
3.1          内存泄露
在源文件hiredis.c中函数redisReadIntergerReply内部,会出现没有释放分配的内存块。
static redisReply *redisReadIntegerReply(int  fd) {
    //  在函数redisReadLine内部分配了sds需要的内存
     sds buf = redisReadLine(fd);
     redisReply *r = zmalloc(sizeof(*r));
     
    //  PROBLEM: bufNULL, 没有释放上面分配的redisRely指向的内存块
if (buf == NULL) return  redisIOError();
     r->type = REDIS_REPLY_INTEGER;
    //  PROBLEM: buf不为NULL, 只是把buf的内容转化为interger, 没有释放buf指向的内存块
r->integer =  strtoll(buf,NULL,10);
// FIX: 需要在这里手动调用sdsfree(buf)
     return r;
}
3.2          同步机制
2.5节所分析,redis实现的同步机制相对简单,缺少同步机制常见的check point和校验机制。
  在运行时,如果master -> slave同步请求转发被丢弃, slave将无法恢复该请求的相关信息,直到slave重启时从master全量加载数据时才能修复。因此,建议使用redis尽量利用其key/valuevalue支持多种类型的特性,存储一些相对不重要的数据。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-315909-1-1.html 上篇帖子: Redis源碼概览 下篇帖子: spring-data-redis配置事务
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表