- scan
redis对于命令的处理,即网络io, 是单线程的,如果有上百万个key,使用keys这样的命令,会进行遍历,时间复杂度是O(N), redis server处理的时间就会很长,会导致暂时无法处理其他命令。生产环境,是坚决不能使用keys命令的。
所以,在redis 2.8版本,redis提供了scan命令, 通过cursor的方式进行遍历,对于每一次遍历做了count的限制,不会造成redis server长时间的阻塞。
scan命令的返回值是一个包含两个元素的数组,第一个元素是用于下一次迭代的新cursor,第二个元素则为一个数组,里面包含已经被遍历过的key。
以0作为cursor开始一次新的迭代,默认的count是10,一直调用scan命令,知道命令返回cursor 0,表示一次遍历完成。
scan遍历是顺序是什么样的?redis 的dict中使用的hash table,初始size是4,其实就是遍历这个hashtable里面存储的key。以size是4为例,遍历是顺序为0->2->1->3; size为8时,遍历顺序则为0->4->2->6->1->5->3->7。为什么顺序不是0->1->2->3...这样的呢?主要是考虑rehash情况下,遍历不重复不遗漏。
遍历的算法使用的是高位加1,向低位进位。
/* Increment the reverse cursor */
v = rev(v);
v++;
v = rev(v);
-
rehash
redis 的dict中使用的hash table,初始size是4,后面随着key增多,会进行扩容或者缩容,对于已经存在的key,就需要相应的进行rehash操作了, 就是重新把key分配到扩容/缩容后的hash table里面。
redis是怎么计算一个key应该分配到hash table的哪个index上呢?首先对key进行一次hash操作(crc16),结果再对hash table的大小size取模(即对sizemask = size - 1进行&运算),得到的结果就是key的位置。
如果size从4变为8,原来位于0位置的key,在新的hash table中,就可能被分配到0或者4位置。其他key的位置对应关系如下图。使用scan方法,就可以保证扩容/缩容情况下,遍历过程大概率的不重复不遗漏。对于连续两次缩容的情况,有可能会出现重复。
- skiplist
redis中对于一些数据类型,比如list, set, zset, 在数据量小的时候,是一种存储类型;在数据量大的时候,会变成另一种类型存储。对于zset,在集合中元素小于128时,使用的是ziplist,大于128使用的是skiplist。也就是说,对于大量数据,skiplist的表现要更好,增删改查的时间复杂度大概率为O(logN)。
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
skiplist本质是一个多层有序双向链表,加上了level的概念,构建了多层级的链表。理想skiplist,第一层是一个完整的链表;第二层有一半的节点,对于第一层来说,有一半的节点有第二层,每隔一个节点有第二层;依次构建第三层、第四层...。对于理想skiplist,每一次增加或者删除节点,都需要去维护理想跳表结构。redis中为每个节点随机出一个层数,有25%的概率有第二层,有25%*25%的概率有第三层..., 这样在数据量很大的情况下,就趋近于理想跳表,在增删的时候,不需要去维护理想跳表结构。
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
/* Returns a random level for the new skiplist node we are going to create.
* The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
* (both inclusive), with a powerlaw-alike distribution where higher
* levels are less likely to be returned. */
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
skiplist与rbtree对比
结构的区别:skiplist是多层有序链表;rbtree是二叉树
增删改查时间复杂度:skiplist大概率O(logN);rbtree O(logN)
范围查找zrange:skiplist O(logN); rbtree log(N)*O(logN)
实现上的区别:skiplist 实现简单;rbtree实现复杂,增加、删除比较复杂。
主从复制
redis主从复制是高可用的基础。
2.8版本以前的方案:全量同步
1)slave连接到master,向master发送sync命令;
2)master做rdb持久化(即使不配置rdb,主从复制也会使用rdb进行持久化), 之后记录写命令到缓冲区,发送rdb;
3)slave接收rdb,加载至内存;
4)master发送缓冲区的命令,slave依次处理。
2.8版本以后的方案:增量同步
1)slave连接到master,向master发送psync命令(包含之前连接的master的run_id和offset);
2)master对run_id和offset进行check,如果run_id跟当前的一致,并且offset在缓冲区中,则进行增量同步,否则进行全量同步。其他一些常见的问题
经常听到有人说redis是单线程的,它真的是单线程吗?还有人说如果server的内存是8G,那么maxmemory不能超过4G,因为进行持久化的时候,会fork出一个进程,所占内存就会翻倍,真的是这样吗?
redis是单线程的吗?当然不是,redis只是处理命令是单线程的,他利用了reactor模式,将对于命令,也就是网络io的处理,转变成对于event的处理。
对于maxmemory的设置,真的只能不超过机器内存的1/2?当然不是。在进行rdb/aof持久化的时候,redis会fork出一个子进程做持久化的操作,父进程和子进程共用同一块内存空间,内存会被设置成只读,父进程继续处理命令,子进程做持久化。这时候如果有写命令,那么会产生一个页错误(page-fault中断),会对所在页进行copy,父子进程各一份,这时候父进程就可以进行写操作了。这个就叫做copy on write机制。