压缩列表(ziplist)是列表键和哈希键的底层实现之一。当列表键只包含少量列表项,并且每个列表项要么是小整数值,要么是长度较短的字符串时;或者当哈希键只包含少量键值对,并且每个键值对的键和值要么是小整数值,要么是长度较短的字符串时,那么Redis就会使用压缩列表来做为列表键或哈希键的底层实现。

压缩列表是Redis为了节约内存而开发的,可用于存储字符串和整数值。它是一个顺序型数据结构,由一系列特殊编码的连续内存块组成。一个压缩列表可以包含任意多个结点(entry),每个entry的大小不定,每个entry可保存一个字符串或一个整数值。

ziplist的相关实现在都在ziplist.c中。

一:ziplist结构

ziplist的结构如下:

<zlbytes><zltail><zllen><entry1><entry2>...<entryN><zlend>

zlbytes是一个uint32_t类型的整数,表示整个ziplist占用的内存字节数;

zltail是一个uint32_t类型的整数,表示ziplist中最后一个entry的偏移量,通过该偏移量,无需遍历整个ziplist就可以确定尾结点的地址;

zllen是一个uint16_t类型的整数,表示ziplist中的entry数目。如果该值小于UINT16_MAX(65535),则该属性值就是ziplist的entry数目,若该值等于UINT16_MAX(65535),则还需要遍历整个ziplist才能得到真正的entry数目;

entryX表示ziplist的结点,每个entry的长度不定;

zlend是一个uint8_t类型的整数,其值为0xFF(255),表示ziplist的结尾。

在ziplist.c中,定义了一系列的宏,可以分别获取ziplist中存储的各个属性,比如:

#define ZIPLIST_BYTES(zl)       (*((uint32_t*)(zl)))
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t*)((zl)+sizeof(uint32_t))))
#define ZIPLIST_LENGTH(zl) (*((uint16_t*)((zl)+sizeof(uint32_t)*2)))
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t)*2+sizeof(uint16_t))
#define ZIPLIST_ENTRY_HEAD(zl) ((zl)+ZIPLIST_HEADER_SIZE)
#define ZIPLIST_ENTRY_TAIL(zl) ((zl)+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
#define ZIPLIST_ENTRY_END(zl) ((zl)+intrev32ifbe(ZIPLIST_BYTES(zl))-1)

ZIPLIST_BYTES获取ziplist中的zlbytes属性;ZIPLIST_TAIL_OFFSET获取ziplist的zltail属性;ZIPLIST_LENGTH获取ziplist的zllen属性

ZIPLIST_ENTRY_HEAD得到ziplist头结点的地址;ZIPLIST_ENTRY_TAIL得到ziplist中尾节点的首地址,ZIPLIST_ENTRY_END得到ziplist结尾字节zlend的地址。

注意,ziplist中所有的属性值都是以小端的格式存储的。因此取得ziplist中保存的属性值后,还需要对内存做字节翻转才能得到真正的值。intrev32ifbe就是在大端系统下对内存进行字节翻转的函数。

二:entry结构

每个压缩列表节点都由previous_entry_length、encoding和content三部分组成。

previous_entry_length表示前一个entry的字节长度,根据该字段值就可以从后向前遍历ziplist。

previous_entry_length字段长度可以是1字节,也可以是5字节。如果前一个entry长度小于254字节,则该字段只占用一个字节;如果前一个entry长度大于等于254字节,则该字段占用5个字节:第一个字节置为0xFE(254),之后的4个字节保存entry的实际长度(小端格式存储)。

之所以用0xFE(254)这个值作为分界点,是因为0xFF(255)被用作ziplist的结束标志,一旦扫描到0xFF,就认为ziplist结束了。

entry的content字段保存节点的实际内容,它可以是一个字符串或者整数,值的类型和长度由encoding属性决定。

encoding字段记录了节点的content所保存的数据类型及长度。如果entry中保存的是字符串,则encoding字段的前2个二进制位可以是00、01和10,分别表示不同长度类型的字符串,剩下的二进制位就表示字符串的实际长度;如果entry中的内容为整数,则encoding字段的前2个二进制位都为11,剩下的2个二进制位表示整数的类型。encoding的形式如下:

00pppppp,1字节,表示长度小于等于63(2^6- 1) 字节的字符串;

01pppppp|qqqqqqqq,2 字节,表示长度小于等于16383(2^14 - 1) 字节的字符串;

10______|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt,5字节,表示长度小于等于4294967295(2^32- 1) 字节的字符串

11000000,1字节,表示int16_t类型的整数;

11010000,1字节,表示int32_t类型的整数;

11100000,1字节,表示int64_t类型的整数;

11110000,1字节,表示24位有符号整数;

11111110,1字节,表示8位有符号整数;

1111xxxx,1字节,表示0到12之间的值。使用这一编码的节点没有相应的oontont属性,因为xxxx就可以表示0到12之间的值。因为0000和1110不可用,所以xxxx的取值从0001到1101,也就是1到13之间的值。因此,需要从xxxx减去1,才能得到实际的值。

注意,所有的整数都是以小端的格式存储的。

三:连锁更新

每个节点的previous_entry_length字段记录了前一个节点的长度。如果前一节点的长度小于254字节,那么previous_entry_length字段占用1个字节;如果前一节点的长度大于等于254字节,那么previous_entry_length字段占用5个字节。

考虑这样一种情况:在一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点e1至eN。因所有节点的长度都小于254字节,所以e1至eN节点的previous_entry_length字段都是1字节长。

这时,如果将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,那么new将成为e1的前置节点,但是因为e1的previous_entry_length字段仅长1字节,没办法保存新节点new的长度,所以需要对压缩列表执行空间重分配操作,将e1节点的previous_entry_length字段从原来的1字节扩展为5字节。

现在,麻烦的事情来了,原e1的长度介于250字节至253字节之间,e1的previous_entry_length字段变成5个字节后,e1的长度就大于等于254了。从而e2的previous_entry_length字段,也需要从原来的1字节长扩展为5字节长。

因此,需要再次对压缩列表执行空间重分配操作,并将e2节点的previous_entry_length属性从原来的1字节长扩展为5字节长。

以此类推,为了让每个节点的previous_entry_length属性都符合压缩列表对节点的要求,程序需要不断地对压缩列表执行空间重分配操作,直到eN为止。

Redis将这种在特殊情况下产生的连续多次空间扩展操作称之为“连锁更新”(cascade update)。除了添加新节点可能会引发连锁更新之外,删除节点也可能会引发连锁更新。

因为连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N^2)。

尽管连锁更新的复杂度较高,但它真正造成性能间题的几率是很低的。

首先,压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的节点,连锁更新才有可能触发。在实际中,这种情况并不多见;

其次,即使出现连锁更新,但只要被更新的节点数量不多,就不会对性能造成任何影响,比如对三五个节点进行连锁更新是绝对不会影响性能的;

因此,ziplistPush等函数的平均复杂度仅为O(N)。在实际中,我们可以放心地使用这些函数,而不必担心连锁更新会影响压缩列表的性能。

ziplist变动时,previous_entry_length字段长度可能需要从1字节扩展为5字节,从而会引起连锁更新,也可能需要从5字节收缩为1字节。这就有可能会发生抖动现象,也就是节点的previous_entry_length字段,不断的扩展和收缩的现象。Redis中,为了避免这种现象,允许previous_entry_length字段在需要收缩的情况下,保持5字节不变。

四:代码

Redis中的ziplist实现,并未涉及难以理解的算法。但是因为ziplist本身的编码需求较多,因而代码需要处理各种细节,初看之下比较繁杂,分析之后,其实很容易理解。

1:连锁更新

下面就是处理连锁更新的代码,zl指向一个ziplist,p指向其中第一个不需要更新的节点(第一个已经更新过的节点),其后续的节点可能需要更新:

static unsigned char *__ziplistCascadeUpdate(unsigned char *zl, unsigned char *p) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
size_t offset, noffset, extra;
unsigned char *np;
zlentry cur, next; while (p[0] != ZIP_END) {
cur = zipEntry(p);
rawlen = cur.headersize + cur.len;
rawlensize = zipPrevEncodeLength(NULL,rawlen); /* Abort if there is no next entry. */
if (p[rawlen] == ZIP_END) break;
next = zipEntry(p+rawlen); /* Abort when "prevlen" has not changed. */
if (next.prevrawlen == rawlen) break; if (next.prevrawlensize < rawlensize) {
/* The "prevlen" field of "next" needs more bytes to hold
* the raw length of "cur". */
offset = p-zl;
extra = rawlensize-next.prevrawlensize;
zl = ziplistResize(zl,curlen+extra);
p = zl+offset; /* Current pointer and offset for next element. */
np = p+rawlen;
noffset = np-zl; /* Update tail offset when next element is not the tail element. */
if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);
} /* Move the tail to the back. */
memmove(np+rawlensize,
np+next.prevrawlensize,
curlen-noffset-next.prevrawlensize-1);
zipPrevEncodeLength(np,rawlen); /* Advance the cursor */
p += rawlen;
curlen += extra;
} else {
if (next.prevrawlensize > rawlensize) {
/* This would result in shrinking, which we want to avoid.
* So, set "rawlen" in the available bytes. */
zipPrevEncodeLengthForceLarge(p+rawlen,rawlen);
} else {
zipPrevEncodeLength(p+rawlen,rawlen);
} /* Stop here, as the raw length of "next" has not changed. */
break;
}
}
return zl;
}

首先获取ziplist当前的长度curlen;

然后从p开始轮训,获取p指向节点的总长度rawlen,以及编码该字节长度需要的字节数rawlensize;

如果p指向的结点没有后继结点,直接退出返回;否则,next表示p的后继节点,next节点的previous_entry_length字段值为next.prevrawlen,该字段长度是next.prevrawlensize。如果next.prevrawlen与rawlen相等,则表示next节点的前继节点的长度未发生变化,直接退出返回;

如果next.prevrawlensize小于rawlensize,表示next节点的前继节点的长度,原来小于254字节,现在大于等于254字节了。因此next节点的previous_entry_length字段需要扩展长度,扩展的字节数extra为rawlensize-next.prevrawlensize,利用ziplistResize扩展ziplist的内存空间。注意,扩容前要保存p的偏移量,扩容后利用该偏移量,可以重新得到p的位置。

如下图,分别表示扩容前和扩容后的情况:

如果next节点不是尾节点,则需要更新ziplist的zltail属性;如果next是尾结点,因为next之前的内容没有变化,因此无需更新zltail属性;

然后开始移动内存,移动的内容是,从next节点的previous_entry_length字段之后的内存开始,到ziplist末尾字节zlend之前的内容:memmove(np+rawlensize, np+next.prevrawlensize, curlen-noffset-next.prevrawlensize-1);

然后更新next中的previous_entry_length字段为rawlen;

然后p指向next节点,依次遍历下一个节点;

如果next.prevrawlensize大于rawlensize,表示next节点的前继节点的长度,原来大于等于254字节,现在小于254字节了。为了避免“抖动”,调用zipPrevEncodeLengthForceLarge保持next节点的previous_entry_length字段长度不变,并强制编码为rawlen,然后退出返回;

如果next.prevrawlensize等于rawlensize,表示next节点的前继节点的长度,原来小于254字节,现在还是小于254字节,或者原来大于等于254字节,现在还是大于等于254字节。这种情况下,直接将next节点的previous_entry_length字段编码为rawlen,然后退出返回。

2:删除节点

下面是删除节点的代码,从ziplist中,p指向的节点开始,最多删除num个节点:

static unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p, unsigned int num) {
unsigned int i, totlen, deleted = 0;
size_t offset;
int nextdiff = 0;
zlentry first, tail; first = zipEntry(p);
for (i = 0; p[0] != ZIP_END && i < num; i++) {
p += zipRawEntryLength(p);
deleted++;
} totlen = p-first.p;
if (totlen > 0) {
if (p[0] != ZIP_END) {
/* Storing `prevrawlen` in this entry may increase or decrease the
* number of bytes required compare to the current `prevrawlen`.
* There always is room to store this, because it was previously
* stored by an entry that is now being deleted. */
nextdiff = zipPrevLenByteDiff(p,first.prevrawlen);
p -= nextdiff;
zipPrevEncodeLength(p,first.prevrawlen); /* Update offset for tail */
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))-totlen); /* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset. */
tail = zipEntry(p);
if (p[tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
} /* Move tail to the front of the ziplist */
memmove(first.p,p,
intrev32ifbe(ZIPLIST_BYTES(zl))-(p-zl)-1);
} else {
/* The entire tail was deleted. No need to move memory. */
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe((first.p-zl)-first.prevrawlen);
} /* Resize and update length */
offset = first.p-zl;
zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl))-totlen+nextdiff);
ZIPLIST_INCR_LENGTH(zl,-deleted);
p = zl+offset; /* When nextdiff != 0, the raw length of the next entry has changed, so
* we need to cascade the update throughout the ziplist */
if (nextdiff != 0)
zl = __ziplistCascadeUpdate(zl,p);
}
return zl;
}

首先将p转化为zlentry结构的first,然后从p开始向后遍历num个节点,这样,就得到了需要删除的节点数deleted,以及要删除的总字节数totlen。只有当totlen大于0时,才进行删除动作,否则直接返回zl;此时p指向最后一个删除节点的后继节点;

若此时p不是ziplist的结尾字节,则其指向节点的previous_entry_length字段,需要设置为新值first.prevrawlen。首先计算p指向节点,其新旧previous_entry_length字段长度的差值nextdiff,nextdiff为正,表示p节点的previous_entry_length字段需要扩容,从要删除的内存中选择扩容的部分,因此p-=nextdiff,p向前走nextdiff个字节,表示少删除nextdiff个字节。如果nextdiff为负,表示p节点的previous_entry_length字段需要收缩,因此p-=nextdiff,表示p向后走nextdiff个字节,表示多删除nextdiff个字节;

然后重新设置p指向节点的previous_entry_length字段值为first.prevrawlen;

接下来就是调整zltail属性,分情况讨论:如果遍历num个节点之后,未做nextdiff调整之前,p指向的节点就是尾结点,如下图所示(仅以nextdiff为正为例):

上图中,p'表示未做nextdiff调整之前的指针,也就是指向原尾结点的指针,p表示做出nextdiff调整之后的指针。删除num个节点之后,zltail的值,也就是first.p-zl,因此有:new_zltail = old_zltail - totlen; 删除后如下图所示:

上面的情况,对于nextdiff为负也是成立的,不再赘述。

如果遍历num个节点之后,未做nextdiff调整之前,p指向的节点不是尾结点,则新的zltail,等于旧的zltail,减去删除的总字节数,删除的总字节数为totlen-nextdiff,因此有:new_zltail = old_zltail - totlen + nextdiff;

更新完zltail属性后,接下来就是移动内存了,将当前p指向的内存,移动到first.p指向的位置上,移动的内存总数就是,从当前p指向的内存开始,到ziplist结尾字节之前的内容;

如果在遍历num个节点之后,p指向的字节就是ziplist的结尾字节,则无需移动内存,仅需要重新设置zltail属性即可,此时的尾结点,是first.p的前继节点,因此有:new_zltail = first.p - zl -first.prevrawlen;

移动内存,以及设置新的zltail之后,剩下的就是重新为ziplist分配空间,并且设置新的zllen属性。注意,重新分配ziplist内存之前,保存p的偏移量offset,这样,在分配空间之后,就可以利用offset重新得到p的位置了;

如果nextdiff不为0,表示p指向的节点的大小发生了变化,调用__ziplistCascadeUpdate处理后续的节点。

3:插入节点

下面是插入节点的代码,根据原始数据s,及其长度slen,新建一个ziplist节点,将该节点插入到p之前的位置,也就是,p指向的节点,成为新结点的后继节点。

static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
unsigned int prevlensize, prevlen = 0;
size_t offset;
int nextdiff = 0;
unsigned char encoding = 0;
long long value = 123456789; /* initialized to avoid warning. Using a value
that is easy to see if for some reason
we use it uninitialized. */
zlentry tail; /* Find out prevlen for the entry that is inserted. */
if (p[0] != ZIP_END) {
ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
} else {
unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
if (ptail[0] != ZIP_END) {
prevlen = zipRawEntryLength(ptail);
}
} /* See if the entry can be encoded */
if (zipTryEncoding(s,slen,&value,&encoding)) {
/* 'encoding' is set to the appropriate integer encoding */
reqlen = zipIntSize(encoding);
} else {
/* 'encoding' is untouched, however zipEncodeLength will use the
* string length to figure out how to encode it. */
reqlen = slen;
}
/* We need space for both the length of the previous entry and
* the length of the payload. */
reqlen += zipPrevEncodeLength(NULL,prevlen);
reqlen += zipEncodeLength(NULL,encoding,slen); /* When the insert position is not equal to the tail, we need to
* make sure that the next entry can hold this entry's length in
* its prevlen field. */
nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; /* Store offset because a realloc may change the address of zl. */
offset = p-zl;
zl = ziplistResize(zl,curlen+reqlen+nextdiff);
p = zl+offset; /* Apply memory move when necessary and update tail offset. */
if (p[0] != ZIP_END) {
/* Subtract one because of the ZIP_END bytes */
memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); /* Encode this entry's raw length in the next entry. */
zipPrevEncodeLength(p+reqlen,reqlen); /* Update offset for tail */
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen); /* When the tail contains more than one entry, we need to take
* "nextdiff" in account as well. Otherwise, a change in the
* size of prevlen doesn't have an effect on the *tail* offset. */
tail = zipEntry(p+reqlen);
if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
ZIPLIST_TAIL_OFFSET(zl) =
intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
}
} else {
/* This element will be the new tail. */
ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
} /* When nextdiff != 0, the raw length of the next entry has changed, so
* we need to cascade the update throughout the ziplist */
if (nextdiff != 0) {
offset = p-zl;
zl = __ziplistCascadeUpdate(zl,p+reqlen);
p = zl+offset;
} /* Write the entry */
p += zipPrevEncodeLength(p,prevlen);
p += zipEncodeLength(p,encoding,slen);
if (ZIP_IS_STR(encoding)) {
memcpy(p,s,slen);
} else {
zipSaveInteger(p,value,encoding);
}
ZIPLIST_INCR_LENGTH(zl,1);
return zl;
}

首先获取ziplist当前长度curlen;

然后得到p指向位置的前继节点的长度prevlen;

然后计算新结点中content部分的长度:如果s中的内容能够表示为整数值,则得到该整数值的长度reqlen,否则,reqlen等于slen;

根据prevlen,得到新结点的previous_entry_length字段长度,加到reqlen中;

根据encoding和slen,得到新结点的encoding字段长度,加到reqlen中,至此,reqlen就表示新结点的总长度;

因新结点将成为p的前继节点,因此,只要p并非ziplist的结尾字节,就利用zipPrevLenByteDiff,计算p中新旧previous_entry_length字段的长度之差nextdiff,nextdiff为正,表示因新结点的长度大于等于254,p的previous_entry_length字段需要扩容nextdiff个字节;如果nextdiff为负,表示因新结点的长度小于254,p的previous_entry_length字段需要收缩nextdiff个字节;

接下来,利用ziplistResize对ziplist进行扩容,扩容长度为reqlen+nextdiff,扩容之前,保存p的偏移量offset,这样扩容后根据offset,就可以重新得到p的位置;

如果p并非ziplist的结尾字节,则接下来开始移动内存,从p-nextdiff指向的位置,移动到p+reqlen的位置,移动的字节数为curlen-offset-1+nextdiff;

然后设置新结点的后继节点(也就是插入之前,p指向的节点)的previous_entry_length字段;

然后设置zltail属性,这里根据p是否是尾结点而区分对待,情况类似与__ziplistDelete中的讨论,不再赘述。

如果p就是ziplist的结尾字节,则只需要更新zltail属性即可;

如果nextdiff非0,则需要调用__ziplistCascadeUpdate,处理p+reqlen节点开始的连锁升级或是收缩情况;

最后,设置插入的新结点的各种字段值,并更新zllen属性。

其他关于ziplist的代码,可参阅:

https://github.com/gqtc/redis-3.0.5/blob/master/redis-3.0.5/src/ziplist.c

Redis源码解析:07压缩列表的更多相关文章

  1. Redis源码解析之跳跃表(三)

    我们再来学习如何从跳跃表中查询数据,跳跃表本质上是一个链表,但它允许我们像数组一样定位某个索引区间内的节点,并且与数组不同的是,跳跃表允许我们将头节点L0层的前驱节点(即跳跃表分值最小的节点)zsl- ...

  2. .Net Core缓存组件(Redis)源码解析

    上一篇文章已经介绍了MemoryCache,MemoryCache存储的数据类型是Object,也说了Redis支持五中数据类型的存储,但是微软的Redis缓存组件只实现了Hash类型的存储.在分析源 ...

  3. Redis源码解析:15Resis主从复制之从节点流程

    Redis的主从复制功能,可以实现Redis实例的高可用,避免单个Redis 服务器的单点故障,并且可以实现负载均衡. 一:主从复制过程 Redis的复制功能分为同步(sync)和命令传播(comma ...

  4. Redis源码解析:13Redis中的事件驱动机制

    Redis中,处理网络IO时,采用的是事件驱动机制.但它没有使用libevent或者libev这样的库,而是自己实现了一个非常简单明了的事件驱动库ae_event,主要代码仅仅400行左右. 没有选择 ...

  5. Redis源码解析

    一.src/server.c 中的redisCommandTable列出的所有redis支持的命令,其中字符串命令包括从get到mget:列表命令从rpush到rpoplpush:集合命令包括从sad ...

  6. Redis源码解析:25集群(一)握手、心跳消息以及下线检测

    Redis集群是Redis提供的分布式数据库方案,通过分片来进行数据共享,并提供复制和故障转移功能. 一:初始化 1:数据结构 在源码中,通过server.cluster记录整个集群当前的状态,比如集 ...

  7. Redis源码解析之跳跃表(一)

    跳跃表(skiplist) 有序集合(sorted set)是Redis中较为重要的一种数据结构,从名字上来看,我们可以知道它相比一般的集合多了一个有序.Redis的有序集合会要求我们给定一个分值(s ...

  8. Redis源码解析:26集群(二)键的分配与迁移

    Redis集群通过分片的方式来保存数据库中的键值对:一个集群中,每个键都通过哈希函数映射到一个槽位,整个集群共分16384个槽位,集群中每个主节点负责其中的一部分槽位. 当数据库中的16384个槽位都 ...

  9. Redis源码解析:10scan类命令的实现

    像keys或者smembers命令,需要遍历数据集合中的所有元素.在一个大的数据库中使用,可能会阻塞服务器较长的一段时间,造成性能问题,因此不适用与生产环境. 在Redis2.8.0中引入了scan类 ...

随机推荐

  1. 20190829 [ Night ] - 玻

    又考试 半集训真累…… 老帅哥露出会心的微笑.jpeg +------------+ | 寸 | | 土 分 | | 不 分 | | 让 必 | | 争 | +------------+ 考试过程: ...

  2. linux 下建立桌面快捷方式

    这段时间从windows转到了Linux,发现桌面上没有快捷方式很不适应,找了好久资料,找到解决方法,记录下来以后备用 1.首先建立一个新文件 ``` vi quick.desktop //后缀为de ...

  3. 使用neo4j-import导入数据及关系

    背景 上节我们了解了什么是图数据库,作为研究对象的neo4j的特点,优缺点以及基本的环境搭建. 现在我们要讲存储在csv中的通话记录数据导入到neo4j中去,并且可以通过cql去查询导入的数据及关系 ...

  4. tp5.1 本地正常, 线上route.php不起作用的问题

    由于本项目 的.htaccess文件是放在根目录的, 上传没有覆盖,重新编辑 <IfModule mod_rewrite.c> Options +FollowSymlinks -Multi ...

  5. JavaScript基本的使用

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. VMware安装Centos后无法上网

    参考于: http://www.xpxt.net/xtjc/win8/04194953.html

  7. 【JZOJ5064】【GDOI2017第二轮模拟day2】友好城市 Kosarajo算法+bitset+ST表+分块

    题面 在Byteland 一共有n 座城市,编号依次为1 到n,这些城市之间通过m 条单向公路连接. 对于两座不同的城市a 和b,如果a 能通过这些单向道路直接或间接到达b,且b 也能如此到达a,那么 ...

  8. 前端框架中 “类mixin” 模式的思考

    "类 mixin" 指的是 Vue 中的 mixin,Regular 中的 implement 使用 Mixin 的目的 首先我们需要知道为什么会有 mixin 的存在? 为了扩展 ...

  9. 获得CSM(Certified Scrum Master)-价值驱动交付。

    2019年越来越多的企业开始实行敏捷转型,紧随时代潮流,学习最先进的科学管理方法,找到正确的人(团队),为企业交付高价值的产品服务. 导师Ethan ,培训的课程让人收益匪浅,活到老学到老,丰富的知识 ...

  10. Mathematica 和 MATLAB、Maple 并称为三大数学软件

    Mathematica是一款科学计算软件,很好地结合了数值和符号计算引擎.图形系统.编程语言.文本系统.和与其他应用程序的高级连接.很多功能在相应领域内处于世界领先地位,它也是使用最广泛的数学软件之一 ...