范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

redis源码阅读ziplist

  ziplist1. 介绍
  首先是官方介绍:
  The ziplist is a specially encoded dually linked list that is designed to be very memory efficient. It stores both strings and integer values, where integers are encoded as actual integers instead of a series of characters. It allows push and pop operations on either side of the list in O(1) time. However, because every operation requires a reallocation of the memory used by the ziplist, the actual complexity is related to the amount of memory used by the ziplist.
  ziplist是一个经过特殊编码的双向链表,旨在提高内存效率。它可以储存字符串和整型值,其中,整型值被编码为实际整数而不是作为字符储存。ziplist可以在O(1)的时间内,在其左侧和右侧进行pop和push操作。但是,实际每个操作都需要重新分配ziplist使用的内存,因此实际的复杂性与ziplist使用的内存量有关。 1.1. 内存分布
  每个ziplist占用的内存布局如下:
  其中, zlbytes是一个无符号整数,表示当前ziplist占用的总字节数。 zltail是ziplist最后一个entry的指针相对于ziplist最开始的偏移量。通过它,不需要完全遍历ziplist就可以对最后的entry进行操作。 zllen是ziplist的entry数量。当zllen比2**16-2大时,需要完全遍历entry列表来获取entry的总数目。 zlend是一个单字节的特殊值,等于255,标识着ziplist的内存结束点。 1.2. entry
  ziplist每个entry都有一个header,header中有两部分内容。第一部分内容是前一个entry的字节数prevrawlen。第二部分内容是该entry的编码encoding,如果是字符串编码,会附带储存其长度len。每一个entry中的结构如下,content为entry实际储存的数据,包括整型和字符串。
  第一部分中,上一个entry的长度prevrawlen使用如下方式编码: 如果其长度小于254字节,entry使用一个字节储存 如果其长度大于等于254字节,当前entry使用5个字节。第一个字节设置为254,表示后面的四个字节储存的是上一个entry的长度
  第二部分信息编码形式encoding取决于当前entry数据类型: 如果需要储存的数据是字符串,这部分信息的前两位设置为对应的编码,后续内容储存字符串的长度 如果需要储存的数据是整数,这部分信息的前两位设置为11,之后的两位用来确定整数类型。例如: |00pppppp| - 1 byte
  长度小于等于63字节的string,后6位储存其长度
  |01pppppp|qqqqqqqq| - 2 bytes
  长度小于等于16383字节的string,后14位储存其长度
  |10 __ |qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
  长度大于等于16384字节的string,后32位储存其长度
  |11000000| - 1 byte
  Integer encoded as int16_t (2 bytes).
  |11010000| - 1 byte
  Integer encoded as int32_t (4 bytes).
  |11100000| - 1 byte
  Integer encoded as int64_t (8 bytes).
  |11110000| - 1 byte
  Integer encoded as 24 bit signed (3 bytes).
  |11111110| - 1 byte
  Integer encoded as 8 bit signed (1 byte).
  |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
  xxxx这四位中,0000、1111、1110都不能使用,因此其编码后的值范围为1-13。
  |11111111| - End of ziplist. 2. 函数和宏实现2.1. 宏定义
  redis就是通过一堆宏定义来加速程序执行的。 2.1.1. ZIP_IS_STR(enc)
  通过entry中的编码enc来判断当前entry中持有的数据类型是不是string  #define ZIP_IS_STR(enc) (((enc) & ZIP_STR_MASK) < ZIP_STR_MASK)
  ZIP_STR_MASK是0b0011_0000,将它与enc进行AND操作,如果enc是string类型,那么其前两位应该是00、01或10,因此计算之后的数值应该小于ZIP_STR_MASK的。 2.1.2. ZIPLIST_BYTES(zl)
  这个宏就是把ziplist最开头的zlbytes提取出来,代码也很简单,通过指针取zl对应的值。  #define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))2.1.3. ZIPLIST_TAIL_OFFSET(zl)
  这个宏是提取ziplist的zltail内容,它的指针则是对zl偏移一个uint32_t大小(zlbytes的长度)获得的。  #define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))2.1.4. ZIPLIST_LENGTH(zl)
  获取zllen的内容,其指针地获取同上,只不过需要偏移两个uint32_t的内存大小。  #define ZIPLIST_LENGTH(zl)      (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))2.1.5. ZIPLIST_ENTRY相关
  redis使用三个宏来定位ziplist中entry的首尾位置。
  首先计算了ziplist中第一个entry到ziplist开头的偏移地址,其实就是zlbytes、zltail和zllen占用的内存大小。  #define ZIPLIST_HEADER_SIZE     (sizeof(uint32_t)*2+sizeof(uint16_t))
  接下来通过宏获得ziplist第一个entry的指针地址。  #define ZIPLIST_ENTRY_HEAD(zl)  ((zl)+ZIPLIST_HEADER_SIZE)
  那么ziplist的最后一个entry的指针地址也可以通过2.1.3的宏获得。  #define ZIPLIST_ENTRY_TAIL(zl)  ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
  entry列表的地址边界也可以获得,ziplist最后是一个字节的zlend,因此,zl偏移zlbytes-1就可以获得其指针了。  #define ZIPLIST_ENTRY_END(zl)   ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)2.1.6. ZIPLIST_INCR_LENGTH(zl,incr)
  这个宏用来调整ziplist的entry数量,即zllen。因为ziplist每次只pop或push一个数据,因此这个宏的incr一般为1或-1。
  代码如下,当ZIPLIST_LENGTH(zl)大于UINT16_MAX时,就已经不再更新zllen了,之后统计ziplist长度就需要进行遍历。  #define ZIPLIST_INCR_LENGTH(zl,incr) {       if (ZIPLIST_LENGTH(zl) < UINT16_MAX)           ZIPLIST_LENGTH(zl) = intrev16ifbe(intrev16ifbe(ZIPLIST_LENGTH(zl))+incr);   }
  我当时迷糊了好久,为啥第三行代码ZIPLIST_LENGTH(zl)的结果可以直接赋值呢?我翻了好久也没翻出来,突然发现,ZIPLIST_LENGTH(zl)是宏定义。它在编译时是将定义的代码直接插入到第三行代码中的,这样就可以进行指针赋值了。 2.1.7. ZIP_ENTRY_ENCODING(ptr, encoding)
  这个宏是用来设置entry的encoding字段,具体entry的结构见后续章节。
  代码如下:  #define ZIP_ENTRY_ENCODING(ptr, encoding) do {        (encoding) = (ptr[0]);       if ((encoding) < ZIP_STR_MASK) (encoding) &= ZIP_STR_MASK;   } while(0)
  代码中第三行,如果encoding小于ZIP_STR_MASK(0b1100_0000),就通过AND操作将encoding后6位设置为0。 2.1.8. ZIP_DECODE_LENGTH(ptr, encoding, lensize, len)
  这个宏的实现目标为:根据encoding设置lensize,即len占用的字节数;根据ptr指向的数据设置len。
  接下来介绍lensize和len的设置,具体编码参考1.2章的encoding格式: 如果encoding==ZIP_STR_06B(0b0000_0000),它的lensize为1,len即为ptr[0]的后六位。 如果encoding==ZIP_STR_14B(0b0100_0000),lensize为2,len为((ptr[0]&0x3f)<<8)|ptr[1]。 如果encoding==ZIP_STR_32B(0b1100_0000),lensize为5,len为ptr[1]-ptr[4]组成的uint64类型整数。 否则的话encoding>0b1100_0000,说明其是数字,则调用zipIntSize(encoding)设置它的len,lensize为1.  static unsigned int zipIntSize(unsigned char encoding) {      switch(encoding) {      case ZIP_INT_8B:  return 1;      case ZIP_INT_16B: return 2;      case ZIP_INT_24B: return 3;      case ZIP_INT_32B: return 4;      case ZIP_INT_64B: return 8;      default: return 0; /* 4 bit immediate */      }      assert(NULL);      return 0;  }2.1.9. ZIP_DECODE_PREVLENSIZE(ptr, prevlensize)和ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen)
  在1.2章节中,entry会记录上一个entry的大小,如果超过254则使用5个字节来储存其长度,否则只使用1个字节就够了。因此,ZIP_DECODE_PREVLENSIZE(ptr,prevlensize)就是用来设置entry中的prevlensize字段。  #define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize) do {                                if ((ptr)[0] < ZIP_BIGLEN) {                                                         (prevlensize) = 1;                                                           } else {                                                                             (prevlensize) = 5;                                                           }                                                                            } while(0);
  那ZIP_DECODE_PREVLEN( ptr ,  prevlensize ,  prevlen )就很简单了,就是把ptr[1]-ptr[4]的数据复制到prevlen。  #define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen) do {                           ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);                                        if ((prevlensize) == 1) {                                                            (prevlen) = (ptr)[0];                                                        } else if ((prevlensize) == 5) {                                                     assert(sizeof((prevlensize)) == 4);                                              memcpy(&(prevlen), ((char*)(ptr)) + 1, 4);                                       memrev32ifbe(&prevlen);                                                      }                                                                            } while(0);2.2. 函数
  ziplist的函数实现,从ziplist的创建、插入等操作开始叙述。 2.2.1. char *ziplistNew(void)
  顾名思义,这个函数就是产生一个新的ziplist。
  代码:  unsigned char *ziplistNew(void) {      unsigned int bytes = ZIPLIST_HEADER_SIZE+1;      unsigned char *zl = zmalloc(bytes);      ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);      ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);      ZIPLIST_LENGTH(zl) = 0;      zl[bytes-1] = ZIP_END;      return zl;  }
  见1.1章节ziplist的内存结构,ziplist初始时没有entry,因此其结构中只存在zlbytes、zltail、zllen和zlend四个,其中,zlbytes和zltail分别占用4个字节,zllen占用2个字节,zlend占用一个字节。因此代码中的bytes计算得11个字节。
  分配完对应的内存后,使用上一章的宏定义设置ziplist的相关参数。
  最后,把zl[bytes-1]即最后一个字节设置为0xff,即ZIP_END。 2.2.2. zlentry zipEntry(unsigned char *p)
  这个函数用来生成一个新的entry,entry的结构如下:  typedef struct zlentry {      // prevrawlen 表示上一个entry的大小、      // prevrawlensize 表示prevrawlen占用的字节数      unsigned int prevrawlensize, prevrawlen;      // len 表示当前entry的长度      // lensize 表示len占用的字节数      unsigned int lensize, len;      // headersize 表示entry的头部占用字节数      // headersize = prevrawlensize + lensize      unsigned int headersize;      // encoding 表示当前entry使用的编码      unsigned char encoding;      // p 指向的数据是当前entry的实际内容,包括prevrawlen、encoding和content      unsigned char *p;  } zlentry;
  因此,zipEntry的实现也很简单,entry的prevrawlen和len都储存在p指向的地址中,使用2.1.9的相关宏进行设置。
  上面的entry结构是便于同前端交互而实现的结构,实际ziplist中的entry中只有prevrawlen、len/encoding以及数据content组成。 2.2.3. 数据插入
  ziplist支持从头部和尾部进行数据的插入,这两种方法都需要使用ziplistInsert实现。函数签名如下:  unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen)
  函数中zl就是实际的ziplist实体,p为需要插入的entry指针位置,s为需要插入的字符串,长度为slen。
  实现逻辑如下: 计算prevlensize和prevlen,如果p在zl的最后位置,则通过ptail进行计算,否则直接通过p就可以计算得到。      if (p[0] != ZIP_END) {          // 如果 p 不是 zl 的最后位置,          // 新数据是插在 p 后面的,因此通过 p 可以获得          // prevlensize 和 prevlen          ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);      } else {          // p 是 zl 的最后位置          // 新数据插入在ptail后面          // 那么需要获取ptail的长度给prevlen          unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);          if (ptail[0] != ZIP_END) {              prevlen = zipRawEntryLength(ptail);          }      }使用zipTryEncoding(s,slen,&value,&encoding)判断能否将s字符串转换为数字,并设置相应的encoding;如果无法转换,ziplist将使用字符串编码方式进行储存。 使用ziplistResize(zl,curlen+reqlen+nextdiff)增加zl的内存,并进行内存移动操作,留出可以放置新entry的空间。 新entry下一跳entry的prevlen需要进行调整,在1.2章节中,prevrawlen可能为1或5个字节。在本节中,如果新entry的长度len一个字节就够编码,而下一跳entry的prerawlen之前使用5个字节编码,那么就需要进行内存的调整。具体见__ziplistCascadeUpdate(zl,p+reqlen)的代码实现。 根据需要插入的数据类型(string或整数)添加到ziplist中,并增加zllen。 2.2.4. 查找
  函数签名如下,p为需要查找的entry列表起点,vstr为需要查找的数据,长度为vlen,每次查找时跳过skip个entry进行匹配。例如,skip=2,第一次比较entry0,那么entry1、entry2都会跳过不进行比较,下一次直接比较entry3。  unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr, unsigned int vlen, unsigned int skip)
  这个函数的实现思路为,从第一个entry依次遍历到最后一个entry,即p[0]==ZIP_END。 通过宏定义获取p指向entry的prevlensize、lensize和len。 q=p+prevlensize+lensize,得到当前entry的数据所在地址。 根据encoding,进行分别比较。如果为string,则使用memcmp比较,否则使用zipTryEncoding将vstr转为整数进行比较。 在循环中,使用skipcnt进行跳过。 将p=q+len,指向下一个entry。 2.2.5. 遍历
  在对ziplist进行遍历时需要获取下一个entry的地址,遍历分前向和后向遍历,因此,ziplist也有两种实现方式。  unsigned char *ziplistNext(unsigned char *zl, unsigned char *p)  unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p)
  在ziplistNext中,如果p已经遍历到zl的最后位置ZIP_END,下一个entry就是NULL;否则的话使用zipRawEntryLength获取p的entry占用字节数,然后将p向后移动相应的字节。如果移动后p指向ZIP_END,则返回NULL。
  在ziplistPrev中,如果p指向ZIP_END,那么p的上一个entry就是zltail指向的entry,将其返回即可;如果p是zl的第一个entry,那么返回NULL;否则的话使用ZIP_DECODE_PREVLEN计算当前entry中的prevlen,将p-prevlen并返回。 2.2.6. 删除
  函数签名如下,删除从p开始的num个entry,并返回删除后的ziplist。  unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num)
  实现思路如下: 计算需要删除的entry占用的内存字节数totlen。 如果totlen为0,直接返回zl 如果totlen>0,说明需要进行内存的移动。 删除entry时,第num+1个entry的prevrawlen就需要重新调整,可能从1字节变为5字节,因此需要使用zipPrevLenByteDiff函数判断需要调整的字节数nextdiff,然后p向前移动nextdiff个字节数。 重新设置zlbytes、zllen、zltail等参数,返回zl 2.2.7. entry数量
  获取zl中entry的数量就比较简单了。如果zllen小于UINT16_MAX,直接就可以返回zllen;否则的话需要从第一个entry遍历到最后一个entry进行统计。代码就不贴了,可以看源代码中的ziplistLen(unsigned char *zl)函数。 总结
  ziplist当数据量比较大的时候,其查找还是很消耗资源的,需要进行完全遍历,时间复杂度最坏情况为O(n);它的插入和删除的时间复杂度也并不一致是O(1)。因此在使用时也需要配合其他数据结构使用。
  刚开始接触ziplist的时候不知道它的具体原理是什么,但是只要了解ziplist的内存分布以及entry的内存分布,它的相关函数实现逻辑基本就了然于胸了,不过具体的细节还是需要阅读源码来提高理解的。

插电式混动SUV2019款现代VisionT概念车官图发布近日,现代汽车发布了一组现代VisionT插电式混合动力SUV概念车。新概念车是现代设计中心所设计系列概念车中的第七款,新概念车采用磨砂绿色外观,并由插电式混合动力总成提供动力,表换装大格栅和软顶敞篷宝马4系敞篷车谍照曝光近日,编辑从外媒处获得一组新款宝马4系敞篷车的谍照,通过这些照片可以看出这是一款软顶敞篷轿跑车,但车身上仍然有很多伪装,细节方面透露不多。新款4系列车型可能会在2020年上半年进行外观更加动感标致ExpertSportEdition官图发布近日,标致汽车发布了一组Expert车系旗舰车型SportEdition的官方图片,新车以车体形式分为PanelVan厢式货车及CrewVan商旅车两款,新车与标准车型相比,具有额搭载插电式混合动力斯柯达明锐RS旅行版谍照曝光近日,外媒发布了一组斯柯达明锐RS旅行版的谍照,新车属于OctaviaRS的第四次升级换代车型,搭载了汽油和柴油发动机以及插电式混合动力,并配备了手动和自动变速器及两驱和四驱版本等为驾驶乐趣而生兰博基尼HuracanEvoRWD官图发布近日,兰博基尼汽车发布了一组2021款兰博基尼HuracanEvoRWD的官方图片,新车的性能依赖于人与机器之间的和谐,驾驶技能和机械装置提供了完美平衡的动力,既增强了产品阵容,又配备4级自动驾驶系统全新奥迪AIME概念车官图发布近日,奥迪汽车发布了一组全新奥迪AIME概念车的官方图片,新车具有紧凑的尺寸,宽敞的未来派内饰以及能够在4级自动驾驶的能力奥迪AIME概念车还提供了高科技功能用于通讯娱乐和休闲。新首次匹配轻混系统2020款马自达2英国版官图发布近日,马自达汽车发布了一组2020款马自达2英国版的官方图片,新车凭借更新更先进的外观设计新技术改进的精致感和升级的内饰,并且首次搭载了马自达的MHybrid轻混系统。2020款马海信TOUCH音乐阅读器口袋中的音乐库与藏书阁经常听到这么一句话要么去旅行,要么去读书,身体和灵魂必须有一个在路上。最近小编发现了一款特别适合在路上阅读的神器海信TOUCH音乐阅读器。带着这款阅读音乐器出门,完全无需在背囊中开探店卡萨帝银河中央空调值得推荐吗最近朋友准备装修新房,就跟她一起逛家电,居然被我们找到一款颜值报表的高端中央空调,完全颠覆我对以往中央空调的认知,它坐落在卡萨帝展厅里面,一进门,我就被其吸引住了!了解了一下,是新最高直降450元海信阅读手机618京东Hi读狂欢来袭在短视频泛滥的时代里,想必很多人都希望找回许久未有的沉浸式阅读体验。因此,选购一款护眼阅读手机就非常合适了。而海信手机正是阅读手机领域的领头羊,时至618,在京东平台,海信多款阅读海信阅读手机618年中盛典Hi读狂欢全场至高立省450元618年中狂欢迎来高潮,海信手机618年中盛典Hi读狂欢火热开启,超级新品海信TOUCH火热抢购,全品类阅读产品超值大放价,全场至高立省450元,还有新品开售限时专享福利,海信评价
购买了新能源车,物业不同意安装充电桩?业主起诉至法院,判决来了!来源南京中院江苏公共新闻频道转自湖南高院特别提示凡本号注明来源或转自的作品均转载自媒体,版权归原作者及原出处所有。所分享内容为作者个人观点,仅供读者学习参考,不代表本号观点卖掉燃油荣耀X20Max入网,7。2英寸大屏6000mAh,6nm旗舰芯坐镇荣耀自脱离华为后,便走上了一条自救之路,在不到一年的时间里,它到处寻找供应商,合作伙伴,很快它的供应链就得到了恢复甚至完善,并接连推出了很多新机,比如荣耀V40系列荣耀50系列等,手机4G网速变慢了,学会添加一个新的接入点,网速快了哈喽,大家好,这里是村夫的小生活,欢迎大家收看我的频道!5G已经慢慢地在普及了,但是咱们大部分的手机都是用的4g网络,很多人都觉得手机的网速不如以前那么快了,又不想去办5g网络,毕定档十月份?华为Mate50Pro曝光,外观意料之中iPhone13的发布,不仅使得苹果在售的老款iPhone有了不同幅度的降价,还使得安卓阵营中的各个厂商显得有些尴尬,毕竟此次新款iPhone在全球缺芯和疫情的影响下,并没有外界传升级iOS14。8后耗电严重?分享10个iPhone省电设置,适用所有机型又见面了,我是大尾,除了每天为你分享一线数码资讯,偶尔还为你带来一些iPhone使用经验干货。相信不少人已经升级了iOS14。8系统了,大多数反馈都是续航变差了,信号变好了。今天跟什么是手机上的NFC?NFC用途介绍现在越来越多的手机支持NFC功能。当然,很多人不知道NFC到底是什么。近场通信(NFC)是一种短距离无线技术。NFC具有移动支付数据传输信息浏览和访问控制等功能。它提供了一种简2000元左右连用4年不卡顿?今年这三款手机,何不了解下很多人买中端机都会有一定忧虑,担心手机的使用寿命不够长久,但其实现在千元机早就发展得超乎我们想象,今天就来分享不同厂家的中端机得意之作,保证你用四年也爱不释手。RedmiK40售价为省钱追求性价比不丢人,四款低价手机推荐,体验甚至不输旗舰机八九千甚至上万元的高端手机,虽然很贵,但其实这些手机并不能彰显一个人的能力,因为现在各大手机厂商都推出了免息政策,有些人虽然用着上万元的手机,但他可能是缩衣节食换来的,因此用手机来1000元左右的手机推荐第一款这是千元机的爆款,红米一直作为性价比的代表。这款红米note10更是如此。1处理器天玑700八核高性能芯片,7nm的制程,流畅不卡顿。2网络双卡双5G,优秀的制作工艺让信号更经典手机三星伸缩式CCD摄像头2005年初,三星拍照手机M339。金属喷漆质感独特下滑盖光学变焦发布就是炫技。纵观同年诺基亚摩托之30万百万像素数码变焦,M339镜头伸出那一刻,即是赶尽sha绝!外观银色机身金苹果手机和小鹏汽车的异床同梦文W探员图网络编辑田田这周科技圈最热的事情应该是苹果发布会的举行。TimCook一举公布了全新的iPhone,iPad,iWatch,AppleTV等产品。苹果发布会全家桶这周汽车