温斯顿吴 赌场式交易员

编程问题总结-Redis

2017-04-05

Redis中几种实现锁的方式的比较

  • 使用INCRE
    <?php
    $value = $redis->get($lock); 
    if($value < 1 ){
    $redis->incr($lock,1);
    // ...
    $redis->decr($lock,1);
    }
    
  • 使用WATCH
    <?php
    // 被WATCH的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在EXEC执行之前被修改了,那么整个事务都会被取消
    WATCH mykey
    $val = GET mykey   // 乐观锁
    $val = $val + 1
    MULTI
    SET mykey $val
    EXEC
    
  • 使用SETNX 是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置。
<?php
// 缓存过期时通过SetNX获取锁,如果成功了就更新缓存,然后删除锁
$ok = $redis->setNX($key, $value);
if ($ok) {
  $cache->update();
  $redis->del($key);
}

存在问题:如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。

因此需要给锁加一个过期时间以防不测。

<?php
// 加锁
$redis->multi();
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
$redis->exec();

存在问题:当多个请求到达时,虽然只有一个请求的SetNX可以成功,但是任何一个请求的Expire却都可以成功,如此就意味着即便获取不到锁,也可以刷新过期时间,如果请求比较密集的话,那么过期时间会一直被刷新,导致锁一直有效。

从 2.6.12 起,SET涵盖了SETEX的功能,并且SET本身已经包含了设置过期时间的功能:

<?php
$ok = $redis->set($key, $value, array('nx', 'ex' => $ttl));
if ($ok) {
  $cache->update();
  $redis->del($key);
}

HyperLogLog理解

基数 如数据集 {1, 3, 5, 7, 5, 7, 8},那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数(不重复元素个数)为5。

基数估计:在误差可接受的范围内,快速计算基数。 Redis HyperLogLog是用来做基数统计的算法,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的

因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。

例:

redis 127.0.0.1:6379> PFADD w3ckey "redis"
1) (integer) 1
redis 127.0.0.1:6379> PFADD w3ckey "mongodb"
1) (integer) 1
redis 127.0.0.1:6379> PFADD w3ckey "mysql"
1) (integer) 1
redis 127.0.0.1:6379> PFCOUNT w3ckey
(integer) 3

基本命令

  • PFADD key element [element …] # 添加指定元素到HyperLogLog中
  • PFCOUNT key [key …] # 返回给定HyperLogLog的基数估算值
  • PFMERGE destkey sourcekey [sourcekey …] # 将多个HyperLogLog合并为一个HyperLogLog

Redis常见配置选项

使用指定的配置文件启动Redis

./redis-server redis.conf

通过Redis命令查看、设置配置项

# 查看loglevel配置项
redis 127.0.0.1:6379> CONFIG GET loglevel
1) "loglevel"
2) "notice"

# 查看所有配置项
redis 127.0.0.1:6379> CONFIG GET *
1) "dbfilename"
2) "dump.rdb"
3) "requirepass"
4) ""
5) "masterauth"
6) ""
7) "unixsocket"
8) ""

# 设置loglevel项
redis 127.0.0.1:6379> CONFIG SET loglevel "notice"
OK

Redis通信协议理解

发送格式

*<参数的个数>CR LF
$<参数1字节数>CR LF
<参数1>CR LF
...
$<参数n字节数>CR LF
<参数n>CR LF

例如set mykey myvalue命令,相应的字符串为:

*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n

响应格式 响应的类型都是由返回数据的第一个字节决定的,有如下几种类型:

  • ”+” 代表一个状态信息,如 +ok
  • ”-“ 代表发生了错误,如操作运算操作了错误的类型
  • ”:” 返回的是一个整数,如:”:11\r\n。 一些命令返回一些没有任何意义的整数,如LastSave返回一个时间戳的整数值, INCR返回一个加1后的数值;一些命令如exists将返回0或者1代表是否true or false;其他一些命令如SADD, SREM 在确实执行了操作时返回1 ,否则返回0
  • ”$” 返回一个块数据,被用来返回一个二进制安全的字符串
  • ”*” 返回多个块数据(用来返回多个值, 总是第一个字节为”*“, 后面写着包含多少个相应值,如:
    C:LRANGE mylist 0 3
    S:*4
    S:$3
    S:foo
    S:$3
    S:bar
    S:$5
    $:world
    

    如果指定的值不存在,那么返回*0

Redis的管道技术理解

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤:

  • (1)客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应。
  • (2)服务端处理命令,并将结果返回给客户端。

Redis管道技术可以在服务端未响应时,客户端能够继续向服务端发送请求,并最终一次性读取所有服务端的响应。 如:

$(echo -en "PING\r\n SET w3ckey redis\r\nGET w3ckey\r\nINCR visitor\r\nINCR visitor\r\nINCR visitor\r\n"; sleep 10) | nc localhost 6379

+PONG
+OK
redis
:1
:2
:3

以上实例中通过使用PING命令查看redis服务是否可用,之后设置了w3ckey的值为redis,然后获取w3ckey的值并使得visitor自增3次。在返回的结果中可以看到这些命令一次性向redis服务提交,并最终一次性读取所有服务端的响应。管道技术最显著的优势是提高了redis服务的性能(批量提交命令)。

SDS相比较C字符串的优势

SDS,simple dynamic string,简单动态字符串 在Redis的数据库里面,包含字符串值的键值对(注意,键也是)在底层都是由SDS实现的。(在Redis源码里,C字符串只会作为字符串字面量用在一些无须对字符串值进行修改的地方,比如打印日志)

如:

redis> SET msg "hello world"
OK

Redis将在数据库中创建一个新的键值对,其中键是一个字符串对象,对象的底层实现是一个保存着字符串“msg”的SDS。值也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world”的SDS。

再如:

redis> RPUSH fruits "apple" "banana" "cherry"
(integer) 3

键是一个字符串对象,对象的底层实现是一个保存了字符串“fruits”的SDS。值是一个列表对象,列表对象包含了三个字符串对象,这三个字符串对象分别由三个SDS实现:第一个SDS保存着字符串“apple”,第二个SDS保存着字符串“ba-nana”,第三个SDS保存着字符串“cherry”。

除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的。

SDS的定义

struct sdshdr {    
  int len;     // 记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
  int free;    // 记录buf数组中未使用字节的数量
  char buf[];  // 字节数组,用于保存字符串(最后一个字节为`\0`,是额外分配的,不算在len中,即当len为5、free为5时,buf的实际长度为11。遵循空字符结尾这一惯例的好处是,SDS可以直接重用一部分C字符串函数库里面的函数)
};

SDS相比较C字符串的优势

    1. 常数复杂度获取字符串长度; # STRLEN
    1. 减少内存分配系统调用,并杜绝缓冲区溢出和内存泄露; 通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略; 空间预分配策略: (1)如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。 (2)如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。 惰性空间释放策略:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。(SDS也提供了专门的API用来真正释放空间)
    1. 二进制安全; 所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样,所以SDS不仅仅可以保存文本数据。(因为SDS使用len属性的值而不是空字符来判断字符串是否结束)
    1. 兼容部分C字符串函数;

Redis内部链表

Redis中除了链表键之外,发布与订阅、慢查询、监视器等功能也用到了链表,Redis服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区。

Redis链表的特性

    1. 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。
    1. 无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。
    1. 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。
    1. 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。
    1. 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

Redis链表(双端链表)的定义

// 链表节点定义
typedef struct listNode {    
  struct listNode * prev; // 前置节点 
  struct listNode * next; // 后置节点
  void * value;           // 节点的值
}listNode;

// 链表类型定义
typedef struct list {
  listNode * head;   // 表头节点  
  listNode * tail;   // 表尾节点        
  unsigned long len; // 链表所包含的节点数量       
  void *(*dup)(void *ptr);  // 节点值复制函数         
  void (*free)(void *ptr);  // 节点值释放函数        
  int (*match)(void *ptr,void *key); // 节点值对比函数
} list;

Redis内部字典

Redis的数据库就是使用字典来作为底层实现的,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。 Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

哈希算法 当要将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。

Redis使用MurmurHash算法来计算键的哈希值,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。

解决键冲突 当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时,称这些键发生了冲突。Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,以此解决键冲突的问题。

因为dictEntry节点组成的链表没有指向链表表尾的指针,所以为了速度考虑,程序总是将新节点添加到链表的表头位置(复杂度为O(1)),排在其他已有节点的前面。

rehash 当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。这可以通过执行rehash(重新散列)操作来完成,Redis对字典的哈希表执行rehash的步骤如下:

    1. 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht[0].used属性的值)。
    1. 将保存在ht[0]中的所有键值对rehash到ht[1]上面:re-hash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
    1. 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。

当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:

    1. 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。
    1. 服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。 负载因子= 哈希表已保存节点数量/ 哈希表大小 另一方面,当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。

在执行BGSAVE命令或BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写入操作,最大限度地节约内存。

为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。在渐进式rehash进行期间,字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行。(例如,要在字典里面查找一个键的话,程序会先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找。在渐进式rehash执行期间,新添加到字典的键值对一律会被保存到ht[1]里面,而ht[0]则不再进行任何添加操作)

跳跃表

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。 Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。

跳跃表节点

typedef struct zskiplistNode {  

  struct zskiplistLevel {           // 层      
    struct zskiplistNode *forward;  // 前进指针    
    unsigned int span;              // 跨度 (记录两个节点之间的距离)
  } level[];    

  struct zskiplistNode *backward;  // 后退指针(每次只能后退至前一个节点)
  double score;                    // 分值(double类型的浮点数,排序的依据)
  robj *obj;                       // 成员对象(唯一性)
} zskiplistNode;

跳跃表

typedef struct zskiplist {    
  structz skiplistNode *header, *tail;  // 表头节点和表尾节点
  unsigned long length;   // 表中节点的数量
  int level;              // 表中层数最大的节点的层数(1至32之间的随机数)
} zskiplist;

Redis内部整数集合

当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为底层实现。

redis> SADD numbers 1 3 5 7 9
(integer) 5
redis> OBJECT ENCODING numbers
"intset"

Redis内部压缩列表

压缩列表(ziplist)是列表和哈希的底层实现之一。当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表的底层实现。(哈希的键值对的键和值具有这样的特征时,也会使用ziplist来实现)。 压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作。 连锁更新在最坏情况下需要对压缩列表执行N次空间重分配操作,而每次空间重分配的最坏复杂度为O(N),所以连锁更新的最坏复杂度为O(N2)。 但这种操作出现的几率并不高,需要当前的内存分布情况恰好满足一定的条件时才可能触发。

Redis内部对象

Redis并没有直接使用简单动态字符串(SDS)、双端链表、字典、压缩列表、整数集合这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种如上的数据结构。 Redis使用对象来表示数据库中的键和值,每当在Redis的数据库中新创建一个键值对时,至少会创建两个对象,一个对象用作键值对的键(键对象),另一个对象用作键值对的值(值对象)。

typedef struct redisObject {    
  unsigned type:4;     // 类型(字符串、列表、哈希、集合、有序集合)
  unsigned encoding:4; // 编码(即底层数据结构的实际类型)
  void *ptr;           // 指向底层实现数据结构的指针
  // ...
} robj;

键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种。

Redis内存回收与对象共享

内存回收 Redis在自己的对象系统中构建了一个引用计数(redisObject结构的refcount属性)技术实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收。

对象共享 对象的引用计数属性还带有对象共享的作用。在Redis中,让多个键共享同一个值对象需要执行以下两个步骤:

  1. 将数据库键的值指针指向一个现有的值对象;
  2. 将被共享的值对象的引用计数增一。

目前,Redis会在初始化服务器时创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器需要用到值为0到9999的字符串对象时(包括嵌套对象的引用),服务器就会使用这些共享对象,而不是新创建对象。

查看引用计数:

redis> SET A 100
OK

redis> OBJECT REFCOUNT A
(integer) 2

redis> SET B 100
OK

redis> OBJECT REFCOUNT A
(integer) 3

redis> OBJECT REFCOUNT B
(integer) 3

尽管共享更复杂的对象可以节约更多的内存,但受到CPU时间的限制,Redis只对包含整数值的字符串对象进行共享。

过期键删除策略

三种不同的删除策略:

    1. 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。定时删除占用太多CPU时间,影响服务器的响应时间和吞吐量。
    1. 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。惰性删除浪费太多内存,有内存泄漏的危险。
    1. 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。服务器必须根据情况,合理地设置删除操作的执行时长和执行频率。

Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡。所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查,它可以在命令真正执行之前,过滤掉过期的输入键,从而避免命令接触到过期键。每当Redis的服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。

AOF、RDB和复制功能对过期键的处理

在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。 在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器将对RDB文件进行载入

    1. 如果服务器以主服务器模式运行,那么在载入RDB文件时,程序会对文件中保存的键进行检查,未过期的键会被载入到数据库中,而过期键则会被忽略,所以过期键对载入RDB文件的主服务器不会造成影响。
    1. 如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,不论是否过期,都会被载入到数据库中。不过,因为主从服务器在进行数据同步的时候,从服务器的数据库就会被清空,所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。

当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加(append)一条DEL命令,来显式地记录该键已被删除。 在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中。

主从复制模式下对过期键的处理

当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制:

    1. 主服务器在删除一个过期键之后,会显式地向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键。
    1. 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会将过期键删除,而是继续像处理未过期的键一样来处理过期键。
    1. 从服务器只有在接到主服务器发来的DEL命令之后,才会删除过期键。通过由主服务器来控制从服务器统一地删除过期键,有利于保证主从服务器数据的一致性。

自动间隔性保存

Redis允许用户通过设置服务器配置的save选项,让服务器每隔一段时间自动执行一次BGSAVE命令。用户可以通过save选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行BGSAVE命令。默认配置如下:

save 900 1     # 服务器在900秒之内,对数据库进行了至少1次修改
save 300 10    # 服务器在300秒之内,对数据库进行了至少10次修改
save 60 10000  # 服务器在60秒之内,对数据库进行了至少10000次修改

Redis的服务器周期性操作函数serverCron默认每隔100毫秒就会执行一次,该函数用于对正在运行的服务器进行维护,它的其中一项工作就是检查save选项所设置的保存条件是否已经满足,如果满足的话,就执行BGSAVE命令。

RDB持久化

RDB持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中。Redis服务器就可以用它来还原数据库状态。

SAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。客户端发送的所有命令请求都会被拒绝

BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。

RDB文件的载入工作是在服务器启动时自动执行的,所以Redis并没有专门用于载入RDB文件的命令,只要Redis服务器在启动时检测到RDB文件存在,它就会自动载入RDB文件。

因为AOF文件的更新频率通常比RDB文件的更新频率高,所以如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据库状态。只有在AOF持久化功能处于关闭状态时,服务器才会使用RDB文件来还原数据库状态。

在BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF三个命令的方式会和平时有所不同:

    1. 在BGSAVE命令执行期间,客户端发送的SAVE命令会被服务器拒绝,服务器禁止SAVE命令和BGSAVE命令同时执行。(防止竞争)
    1. 在BGSAVE命令执行期间,客户端发送的BGSAVE命令会被服务器拒绝。(防止竞争)
    1. BGREWRITEAOF和BGSAVE两个命令不能同时执行。(性能原因)

RDB文件载入时的服务器状态 服务器在载入RDB文件期间,会一直处于阻塞状态,直到载入工作完成为止。

RDB文件结构

REDIS db_version database EOF check_sum

详略。

分析RDB文件

打印RDB文件:

redis> FLUSHALL
OK
redis> SET MSG "HELLO"
OK
redis> SAVE
OK

$ od -c dump.rdb
0000000   R   E  D  I  S  0   0   0  6 376  \0 \0 003  M   S  G
0000020 005   H  E  L  L  O 377 207  z  =  304  f   T  L 343
0000037

Redis本身带有RDB文件检查工具redis-check-dump。

AOF持久化

与RDB持久化通过保存数据库中的键值对来记录数据库状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。被写入AOF文件的所有命令都是以Redis的命令请求协议格式保存的。

服务器在启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。

AOF持久化的实现

    1. 当AOF持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区的末尾。
    1. Redis的服务器进程就是一个事件循环(loop),这个循环中的文件事件负责接收客户端的命令请求,以及向客户端发送命令回复,而时间事件则负责执行像serverCron函数这样需要定时运行的函数。在服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到AOF文件里面。

AOF文件的载入与数据还原 Redis读取AOF文件并还原数据库状态的详细步骤如下:

    1. 创建一个不带网络连接的伪客户端(fake client)(因为Redis的命令只能在客户端上下文中执行);
    1. 从AOF文件中分析并读取出一条写命令。
    1. 从AOF文件中分析并读取出一条写命令。
    1. 一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止。

AOF重写 体积过大的AOF文件很可能对Redis服务器、甚至整个宿主计算机造成影响,并且AOF文件的体积越大,使用AOF文件来进行数据还原所需的时间就越多。

为了解决AOF文件体积膨胀的问题,Redis提供了AOF文件重写(rewrite)功能。通过该功能,Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF文件的体积通常会比旧AOF文件的体积要小得多。

从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是AOF重写功能的实现原理。(比如连续6条RPUSH命令会被整合成1条)

在实际中,为了避免在执行命令时造成客户端输入缓冲区溢出,重写程序在处理列表、哈希表、集合、有序集合这四种可能会带有多个元素的键时,会先检查键所包含的元素数量,如果元素的数量超过了redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD常量的值,那么重写程序将使用多条命令来记录键的值,而不单单使用一条命令。

BGREWRITEAOF命令的实现原理

使用子进程进行AOF重写,在这同时主进程仍然在接受并处理客户端的请求。为了解决数据不一致问题,Redis服务器设置了一个AOF重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当Redis服务器执行完一个写命令之后,它会同时将这个写命令发送给AOF缓冲区和AOF重写缓冲区。 在子进程执行AOF重写期间,服务器进程需要执行以下三个工作:

    1. 执行客户端发来的命令。
    1. 将执行后的写命令追加到AOF缓冲区。
    1. 将执行后的写命令追加到AOF重写缓冲区。 这样一来可以保证:
  • 1.AOF缓冲区的内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作会如常进行。
  • 2.从创建子进程开始,服务器执行的所有写命令都会被记录到AOF重写缓冲区里面。 当子进程完成AOF重写工作之后,它会向父进程发送一个信号,父进程在接到该信号之后,会调用一个信号处理函数,并执行以下工作:
    1. 将AOF重写缓冲区中的所有内容写入到新AOF文件中,这时新AOF文件所保存的数据库状态将和服务器当前的数据库状态一致。
    1. 对新的AOF文件进行改名,原子地(atomic)覆盖现有的AOF文件,完成新旧两个AOF文件的替换。 在整个AOF后台重写过程中,只有信号处理函数执行时会对服务器进程(父进程)造成阻塞,在其他时候,AOF后台重写都不会阻塞父进程,这将AOF重写对服务器性能造成的影响降到了最低。

Redis内部事件

Redis服务器是一个事件驱动程序,服务器需要处理以下两类事件:

    1. 文件事件(file event):Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象。服务器与客户端(或者其他服务器)的通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。
    1. 时间事件(time event):Redis服务器中的一些操作(比如serverCron函数)需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。

文件事件

    1. 文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
    1. 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

时间事件 Redis的时间事件分为以下两类:

    1. 定时事件:让一段程序在指定的时间之后执行一次。比如说,让程序X在当前时间的30毫秒之后执行一次。
    1. 周期性事件:让一段程序每隔指定时间就执行一次。比如说,让程序Y每隔30毫秒就执行一次。

服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

serverCron函数的主要工作

    1. 更新服务器的各类统计信息,比如时间、内存占用、数据库占用情况等。
    1. 清理数据库中的过期键值对。
    1. 关闭和清理连接失效的客户端。
    1. 尝试进行AOF或RDB持久化操作。
    1. 如果服务器是主服务器,那么对从服务器进行定期同步。
    1. 如果处于集群模式,对集群进行定期同步和连接测试。 用户可以通过修改hz选项来调整server-Cron的每秒执行次数。

一个普通客户端可以因为多种原因而被关闭

    1. 客户端进程退出或者被杀死;
    1. 向服务器发送了带有不符合协议格式的命令请求;
    1. 成为了CLIENT KILL命令的目标;
    1. 当客户端的空转时间超过timeout选项设置的值时;(客户端本身是主从服务器、或者正在执行阻塞、订阅命令除外)
    1. 客户端发送的命令请求的大小超过了输入缓冲区的限制大小;
    1. 如果要发送给客户端的命令回复的大小超过了输出缓冲区的限制大小,那么这个客户端会被服务器关闭。

命令请求的执行过程

    1. 当用户在客户端中键入一个命令请求时,客户端会将这个命令请求转换成协议格式,然后通过连接到服务器的套接字,将协议格式的命令请求发送给服务器;
    1. 服务器读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区里面。
    1. 对输入缓冲区中的命令请求进行分析,提取出命令请求中包含的命令参数,以及命令参数的个数,然后分别将参数和参数个数保存到客户端状态的argv属性和argc属性里面。(命令表使用的是大小写无关的查找算法)
    1. 调用命令执行器,执行客户端指定的命令。
    1. 要执行一些后续工作;(记日志、写AOF缓冲区、传播给从服务器等,取决于具体的配置)
    1. 将命令回复发送给客户端,清空客户端状态的输出缓冲区,为处理下一个命令请求做好准备;
    1. 当客户端接收到协议格式的命令回复之后,它会将这些回复转换成人类可读的格式,并打印给用户观看;

服务器初始化的过程

一个Redis服务器从启动到能够接受客户端的命令请求,需要经过一系列的初始化和设置过程:

  1. 初始化服务器状态结构(initServerConfig):创建一个struct redisServer类型的实例变量server作为服务器的状态,并为结构中的各个属性设置默认值。
    • (1)设置服务器的运行ID。
    • (2)设置服务器的默认运行频率。
    • (3)设置服务器的默认配置文件路径。
    • (4)设置服务器的运行架构。
    • (5)设置服务器的默认端口号。
    • (6)设置服务器的默认RDB持久化条件和AOF持久化条件。
    • (7)初始化服务器的LRU时钟。
    • (8)创建命令表。
  2. 载入配置选项:在启动服务器时,用户可以通过给定配置参数或者指定配置文件来修改服务器的默认配置。
  3. 初始化服务器数据结构(initServer):
    • (1)server.clients链表;
    • (2)server.db数组;
    • (3)用于保存频道订阅信息的server.pubsub_channels字典,以及用于保存模式订阅信息的server.pubsub_patterns链表。
    • (4)用于执行Lua脚本的Lua环境server.lua。
    • (5)用于保存慢查询日志的server.slowlog属性。
  4. 为服务器设置进程信号处理器。
  5. 创建共享对象:这些对象包含Redis服务器经常用到的一些值,比如包含”OK”回复的字符串对象,包含”ERR”回复的字符串对象,包含整数1到10000的字符串对象等等;
  6. 打开服务器的监听端口,并为监听套接字关联连接应答事件处理器,等待服务器正式运行时接受客户端的连接。
  7. 为serverCron函数创建时间事件,等待服务器正式运行时执行serverCron函数。
  8. 如果AOF持久化功能已经打开,那么打开现有的AOF文件,如果AOF文件不存在,那么创建并打开一个新的AOF文件,为AOF写入做好准备。
  9. 初始化服务器的后台I/O模块(bio),为将来的I/O操作做好准备。
  10. 服务器将用ASCII字符在日志中打印出Redis的图标,以及Redis的版本号信息;

还原数据库状态

在完成了对服务器状态server变量的初始化之后,服务器需要载入RDB文件或者AOF文件,并根据文件记录的内容来还原服务器的数据库状态。如果服务器启用了AOF持久化功能,那么服务器使用AOF文件来还原数据库状态。如果服务器没有启用AOF持久化功能,那么服务器使用RDB文件来还原数据库状态。

[5244] 21 Nov 22:43:49.084 * DB loaded from disk: 0.068 seconds

复制

可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制(replicate)另一个服务器。如:

127.0.0.1:12345> SLAVEOF 127.0.0.1 6379
OK

进行复制中的主从服务器双方的数据库将保存相同的数据。

新版复制功能的实现

Redis从2.8版本开始,使用PSYNC命令代替SYNC命令来执行复制时的同步操作。PSYNC命令具有完整重同步(full resynchronization)和部分重同步(partial resynchronization)两种模式:

  1. 完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样,它们都是通过让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步。
  2. 部分重同步用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些写命令,就可以将数据库更新至主服务器当前所处的状态。

执行SYNC命令需要生成、传送和载入整个RDB文件,而部分重同步只需要将从服务器缺少的写命令发送给从服务器执行就可以了。

部分重同步的实现

部分重同步功能由以下三个部分构成:

  1. 主服务器的复制偏移量(replication offset)和从服务器的复制偏移量。 主服务器和从服务器会分别维护一个复制偏移量(记录发送、收到的字节数)。通过对比主从服务器的复制偏移量,程序可以很容易地知道主从服务器是否处于一致状态。
  2. 主服务器的复制积压缓冲区(replication backlog)。
    由主服务器维护的一个固定长度先进先出(FIFO)队列,默认大小为1MB。当主服务器进行命令传播时,它不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面。
  3. 服务器的运行ID(run ID)。 每个Redis服务器,不论主服务器还是从服务,都会有自己的运行ID。主服务器会在第一次同步时将自己的ID发送给从服务器,而从服务器则会在断线重连时用其进行判断,如果主服务器ID不一致,则进行全量复制。

复制的实现

具体步骤如下:

  1. 设置主服务器的地址和端口;
  2. 建立套接字连接;
  3. 发送PING命令(检查套接字的读写状态是否正常),主服务器应该返回PONG
  4. 身份验证;
  5. 向主服务器发送从服务器的监听端口号;
  6. 同步(从服务器将向主服务器发送PSYNC命令);
  7. 命令传播;

心跳检测

从服务器默认会以每秒一次的频率,向主服务器发送命令:

REPLCONF ACK [replication_offset]

主要有三个作用:

  1. 检测主从服务器的网络连接状态。
  2. 辅助实现min-slaves选项(Redis的min-slaves-to-write和min-slaves-max-lag两个选项可以防止主服务器在不安全的情况下执行写命令)。
  3. 检测命令丢失。

Sentinel

Sentinel(哨岗、哨兵)是Redis的高可用性解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

启动并初始化Sentinel

$ redis-sentinel /path/to/your/sentinel.conf

或者:

$ redis-server /path/to/your/sentinel.conf --sentinel

Sentinel本质上只是一个运行在特殊模式下的Redis服务器。(默认26379端口)

普通服务器在初始化时会通过载入RDB文件或者AOF文件来还原数据库状态,但是因为Sentinel并不使用数据库,所以初始化Sentinel时就不会载入RDB文件或者AOF文件。

在Sentinel模式下,Redis服务器不能执行诸如SET、DBSIZE、EVAL等等这些命令,因为服务器根本没有在命令表中载入这些命令。PING、SENTINEL、INFO、SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE这七个命令就是客户端可以对Sentinel执行的全部命令。

获取主服务器信息 Sentinel默认会以每十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。

获取从服务器信息 当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构之外,Sentinel还会创建连接到从服务器的命令连接和订阅连接。

向主服务器和从服务器发送信息 在默认情况下,Sentinel会以每两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送以下格式的命令:

PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"

详略。

接收来自主服务器和从服务器的频道信息 对于每个与Sentinel连接的服务器,Sentinel既通过命令连接向服务器的__sentinel__:hello频道发送信息,又通过订阅连接从服务器的__sentinel__:hello频道接收信息。 详略。

检测主观下线状态 在默认情况下,Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送PING命令,并通过实例返回的PING命令回复来判断实例是否在线。

Sentinel配置文件中的down-after-milliseconds选项指定了Sentinel判断实例进入主观下线所需的时间长度。

检查客观下线状态 当Sentinel将一个主服务器判断为主观下线之后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。当Sentinel从其他Sentinel那里接收到足够数量的已下线判断之后,Sentinel就会将从服务器判定为客观下线,并对主服务器执行故障转移操作。

选举领头Sentinel 当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器执行故障转移操作。

故障转移 领头Sentinel对已下线的主服务器执行故障转移操作,该操作包含以下三个步骤:

  1. 在已下线主服务器属下的所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器。
  2. 让已下线主服务器属下的所有从服务器改为复制新的主服务器。
  3. 将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。

节点

一个Redis集群通常由多个节点(node)组成,连接各个节点的工作可以使用CLUSTER MEET命令来完成:

CLUSTER MEET [ip] [port]

向一个节点node发送CLUSTER MEET命令,可以让node节点与ip和port所指定的节点进行握手(handshake),当握手成功时,node节点就会将ip和port所指定的节点添加到node节点当前所在的集群中。

一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。

每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的clusterNode结构,以此来记录其他节点的状态。

通过向节点A发送CLUSTER MEET命令,客户端可以让接收命令的节点A将另一个节点B添加到节点A当前所在的集群里面。

之后,节点A会将节点B的信息通过Gossip协议传播给集群中的其他节点,让其他节点也与节点B进行握手,最终,经过一段时间之后,节点B会被集群中的所有节点认识。

槽指派

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以处理0个或最多16384个槽。 当数据库中的16384个槽都有节点在处理时,集群处于上线状态(ok);相反地,如果数据库中有任何一个槽没有得到处理,那么集群处于下线状态(fail)。

通过向节点发送CLUSTER ADDSLOTS命令,可以将一个或多个槽指派(assign)给节点负责。 将槽0至槽5000指派给节点7000负责:

127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000
OK

将槽5001至槽10000指派给节点7001负责:

127.0.0.1:7001> CLUSTER ADDSLOTS 5001 5002 5003 5004 ... 10000
OK

将槽10001至槽16383指派给7002负责:

127.0.0.1:7002> CLUSTER ADDSLOTS 10001 10002 10003 10004 ... 16383
OK

当以上三个CLUSTER ADDSLOTS命令都执行完毕之后,数据库中的16384个槽都已经被指派给了相应的节点,集群进入上线状态:

127.0.0.1:7000> CLUSTER INFO
cluster_state:ok
...

一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性和numslots属性之外,它还会将自己的slots数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。

在集群中执行命令

当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:

  1. 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令。
  2. 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令。
    127.0.0.1:7000> SET msg "happy new year!"
    -> Redirected to slot [6257] located at 127.0.0.1:7001
    OK
    127.0.0.1:7001> GET msg
    "happy new year!"
    

计算键属于哪个槽

节点使用以下算法来计算给定键key属于哪个槽:

def slot_number(key):    
  return CRC16(key) & 16383

当客户端接收到节点返回的MOVED错误时,客户端会根据MOVED错误中提供的IP地址和端口号,转向至负责处理槽slot的节点,并向该节点重新发送之前想要执行的命令。 集群模式的redis-cli客户端在接收到MOVED错误时,并不会打印出MOVED错误,而是根据MOVED错误自动进行节点转向,并打印出转向信息,所以看不见节点返回的MOVED错误。

注意:节点和单机服务器在数据库方面的一个区别是,节点只能使用0号数据库。

Redis集群故障检测与故障转移

故障检测 集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此来检测对方是否在线,如果接收PING消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG消息,那么发送PING消息的节点就会将接收PING消息的节点标记为疑似下线(probable fail,PFAIL)。 如果在一个集群里面,半数以上负责处理槽的主节点都将某个主节点x报告为疑似下线,那么这个主节点x将被标记为已下线(FAIL),将主节点x标记为已下线的节点会向集群广播一条关于主节点x的FAIL消息,所有收到这条FAIL消息的节点都会立即将主节点x标记为已下线。 故障转移 当一个从节点发现自己正在复制的主节点进入了已下线状态时,从节点将开始对下线主节点进行故障转移:

    1. 复制下线主节点的所有从节点里面,会有一个从节点被选中。
    1. 被选中的从节点会执行SLAVEOF no one命令,成为新的主节点。
    1. 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
    1. 新的主节点向集群广播一条PONG消息,这条PONG消息可以让集群中的其他节点立即知道这个节点已经由从节点变成了主节点,并且这个主节点已经接管了原本由已下线节点负责处理的槽。
    1. 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。 新的主节点是通过选举产生的。

Redis集群消息

集群中的各个节点通过发送和接收消息(message)来进行通信。节点发送的消息主要有以下五种:

    1. MEET消息:当发送者接到客户端发送的CLUSTER MEET命令时,发送者会向接收者发送MEET消息,请求接收者加入到发送者当前所处的集群里面。
    1. PING消息:集群里的每个节点默认每隔一秒钟就会从已知节点列表中随机选出五个节点,然后对这五个节点中最长时间没有发送过PING消息的节点发送PING消息,以此来检测被选中的节点是否在线。除此之外,如果节点A最后一次收到节点B发送的PONG消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout选项设置时长的一半,那么节点A也会向节点B发送PING消息。
    1. PONG消息:当接收者收到发送者发来的MEET消息或者PING消息时,为了向发送者确认这条MEET消息或者PING消息已到达,接收者会向发送者返回一条PONG消息。另外,一个节点也可以通过向集群广播自己的PONG消息来让集群中的其他节点立即刷新关于这个节点的认识。
    1. FAIL消息:当一个主节点A判断另一个主节点B已经进入FAIL状态时,节点A会向集群广播一条关于节点B的FAIL消息,所有收到这条消息的节点都会立即将节点B标记为已下线。
    1. PUBLISH消息:当节点接收到一个PUBLISH命令时,节点会执行这个命令,并向集群广播一条PUBLISH消息,所有接收到这条PUBLISH消息的节点都会执行相同的PUB-LISH命令。

发布与订阅

通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道:

SUBSCRIBE "news.it"

另一个客户端执行发布命令:

PUBLISH "news.it" "hello"

还可以通过执行PSUBSCRIBE命令订阅一个或多个模式(一个模式可以匹配多个频道)。

事务

事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。

redis> MULTI
OK

redis> SET "name" "Practical Common Lisp"
QUEUED

redis> GET "name"
QUEUED

redis> SET "author" "Peter Seibel"
QUEUED

redis> GET "author"
QUEUED

redis> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"

WATCH命令的实现

WATCH命令是一个乐观锁(optimistic locking),它可以在EXEC命令执行之前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改过了,如果是的话,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。

redis> WATCH "name"
OK

redis> MULTI
OK

redis> SET "name" "peter"
QUEUED

redis> EXEC
(nil)

每个Redis数据库都保存着一个watched_keys字典,这个字典的键是某个被WATCH命令监视的数据库键,而字典的值则是一个链表,链表中记录了所有监视相应数据库键的客户端。

所有对数据库进行修改的命令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等等,在执行之后都会调用multi.c/touchWatchKey函数对watched_keys字典进行检查,查看是否有客户端正在监视刚刚被命令修改过的数据库键,如果有的话,那么touchWatchKey函数会将监视被修改键的客户端的REDIS_DIRTY_CAS标识打开,表示该客户端的事务安全性已经被破坏。

当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务。

监视器

通过执行MONITOR命令,客户端可以将自己变为一个监视器,实时地接收并打印出服务器当前处理的命令请求的相关信息。

redis> MONITOR
OK

1378822099.421623 [0 127.0.0.1:56604] "PING"
1378822105.089572 [0 127.0.0.1:56604] "SET" "msg" "hello world"

ZREVRANGEBYSCORE

返回有序集 key 中, score 值介于 max 和 min 之间(默认包括等于 max 或 min )的所有的成员。有序集成员按 score 值递减(从大到小)的次序排列。具有相同 score 值的成员按字典序的逆序(reverse lexicographical order )排列。

redis > ZADD salary 10086 jack
(integer) 1
redis > ZADD salary 5000 tom
(integer) 1
redis > ZADD salary 7500 peter
(integer) 1
redis > ZADD salary 3500 joe
(integer) 1

redis > ZREVRANGEBYSCORE salary +inf -inf   # 逆序排列所有成员
1) "jack"
2) "peter"
3) "tom"
4) "joe"

redis > ZREVRANGEBYSCORE salary 10000 2000  # 逆序排列薪水介于 10000 和 2000 之间的成员
1) "peter"
2) "tom"
3) "joe"

批量删除Redis数据库中的Key

Redis中有删除单个Key的指令DEL,但是没有批量删除Key的指令,可以借助Linux的xargs指令来完成这个动作:

redis-cli keys "*" | xargs redis-cli del 

如果要指定 Redis 数据库访问密码,使用下面的命令:

redis-cli -a password keys "*" | xargs redis-cli -a password del  

如果要访问 Redis 中特定的数据库,使用下面的命令:

redis-cli -n 0 keys "*" | xargs redis-cli -n 0 del  

删除所有Key,可以使用Redis的flushdb和flushall命令:

flushdb  
flushall

抖音
文章目录