- [redis](#redis) - [Using Command](#using-command) - [Keys and values](#keys-and-values) - [Content of Keys](#content-of-keys) - [Hashtags](#hashtags) - [altering and querying the key space](#altering-and-querying-the-key-space) - [key expiration](#key-expiration) - [Navigating the keyspace](#navigating-the-keyspace) - [Scan](#scan) - [keys](#keys) - [pipelining](#pipelining) - [Request/Response protocols and round-trip time(RTT)](#requestresponse-protocols-and-round-trip-timertt) - [redis pipelining](#redis-pipelining) - [Performance Imporvement](#performance-imporvement) - [pipelining vs scripting](#pipelining-vs-scripting) - [Transactions](#transactions) - [Usage](#usage) - [Errors inside transaction](#errors-inside-transaction) - [rollback for redis Transaction](#rollback-for-redis-transaction) - [Discard the command queue](#discard-the-command-queue) - [Optimistic locking using check-and-set](#optimistic-locking-using-check-and-set) - [WATCH](#watch) - [UNWATCH](#unwatch) - [Using WATCH to implement ZPOP](#using-watch-to-implement-zpop) - [Data Types](#data-types) - [Redis Strings](#redis-strings) - [SET / GET](#set--get) - [set with additional arguments](#set-with-additional-arguments) - [GETSET](#getset) - [MSET / MGET](#mset--mget) - [strings as counters](#strings-as-counters) - [Limits](#limits) - [JSON](#json) - [`JSON.SET`](#jsonset) - [json数值操作](#json数值操作) - [json数组操作](#json数组操作) - [`JSON.DEL`](#jsondel) - [`JSON.ARRAPPEND`](#jsonarrappend) - [`JSON.ARRTRIM`](#jsonarrtrim) - [json object操作](#json-object操作) - [format output](#format-output) - [Limitation](#limitation) - [Redis lists](#redis-lists) - [Blocking commands](#blocking-commands) - [Queue(first in, first out)](#queuefirst-in-first-out) - [Stack(first in, last out)](#stackfirst-in-last-out) - [check length of list](#check-length-of-list) - [Atomically pop one element from one list and push to another](#atomically-pop-one-element-from-one-list-and-push-to-another) - [trim the list](#trim-the-list) - [Redis List Impl](#redis-list-impl) - [`LPUSH, RPUSH`](#lpush-rpush) - [`LRANGE`](#lrange) - [`LPOP, RPOP`](#lpop-rpop) - [Common use cases for lists](#common-use-cases-for-lists) - [Capped lists - `latest n`](#capped-lists---latest-n) - [Blocking operations on lists](#blocking-operations-on-lists) - [BRPOP](#brpop) - [Automatic Creation and removal of keys](#automatic-creation-and-removal-of-keys) - [redis sets](#redis-sets) - [basic commands](#basic-commands) - [SADD](#sadd) - [SMEMBERS](#smembers) - [SDIFF](#sdiff) - [SINTER](#sinter) - [SREM](#srem) - [SPOP](#spop) - [SRANDMEMBER](#srandmember) - [redis hashes](#redis-hashes) - [对象表示](#对象表示) - [counters](#counters) - [Field Expiration](#field-expiration) - [Common field expiration use cases](#common-field-expiration-use-cases) - [Field Expiration examples](#field-expiration-examples) - [Sorted sets](#sorted-sets) - [ZRANGEBYSCORE](#zrangebyscore) - [ZREMRANGEBYSCORE / ZREM](#zremrangebyscore--zrem) - [Inclusive and exclusive](#inclusive-and-exclusive) - [ZRANK / ZREVRANK](#zrank--zrevrank) - [`Lexicographical scores`](#lexicographical-scores) - [ZRANGEBYLEX](#zrangebylex) - [Updating the score: leaderboards](#updating-the-score-leaderboards) - [Leaderboard Example](#leaderboard-example) - [ZADD](#zadd) - [ZINCRBY](#zincrby) - [redis Streams](#redis-streams) - [Examples](#examples) - [添加stream entry](#添加stream-entry) - [从指定id开始读取stream entries](#从指定id开始读取stream-entries) - [从末尾开始读取](#从末尾开始读取) - [Stream basics](#stream-basics) - [XADD](#xadd) - [XLEN](#xlen) - [Entry IDs](#entry-ids) - [entry ID设计](#entry-id设计) - [Redis Stream consumer query model](#redis-stream-consumer-query-model) - [Querying by range: XRANGE and XREVRANGE](#querying-by-range-xrange-and-xrevrange) - [query by time range](#query-by-time-range) - [count option](#count-option) - [Listening for new items with XREAD](#listening-for-new-items-with-xread) - [XREAD with STREAMS option](#xread-with-streams-option) - [XREAD with BLOCK argument](#xread-with-block-argument) - [Consumer groups](#consumer-groups) - [Creating a consumer group](#creating-a-consumer-group) - [Create the stream automatically](#create-the-stream-automatically) - [XREADGROUP](#xreadgroup) - [Recovering from permanent failures](#recovering-from-permanent-failures) - [XPENDING](#xpending) - [checking the message content](#checking-the-message-content) - [XCLAIM](#xclaim) - [JUSTID option](#justid-option) - [Automatic claiming](#automatic-claiming) - [Claiming and the delivery counter](#claiming-and-the-delivery-counter) - [working with multiple consumer groups](#working-with-multiple-consumer-groups) - [Enhanced deletion control in Redis 8.2](#enhanced-deletion-control-in-redis-82) - [Stream Observability](#stream-observability) - [Difference with kafka partitions](#difference-with-kafka-partitions) - [Capped Streams](#capped-streams) - [XADD with MAXLEN](#xadd-with-maxlen) - [XTRIM](#xtrim) - [Trimming with consumer group Awareness](#trimming-with-consumer-group-awareness) - [Special IDs in streams API](#special-ids-in-streams-api) - [Persistence, replication and message safety](#persistence-replication-and-message-safety) - [removing single items from a stream](#removing-single-items-from-a-stream) - [XDEL](#xdel) - [Enhanced deletion with XDELEX](#enhanced-deletion-with-xdelex) - [zero length streams](#zero-length-streams) - [Total latency of consuming a message](#total-latency-of-consuming-a-message) - [The model redis uses in order to route stream messages](#the-model-redis-uses-in-order-to-route-stream-messages) - [Redis Geospatial](#redis-geospatial) - [Bike Rental stations Example](#bike-rental-stations-example) - [Redis bitmaps](#redis-bitmaps) - [Example](#example) - [Bit Operations](#bit-operations) - [single bit operations](#single-bit-operations) - [operations on group of bits](#operations-on-group-of-bits) - [longest streak of daily visits](#longest-streak-of-daily-visits) - [Probabilistic](#probabilistic) - [HyperLogLog](#hyperloglog) - [Bloom Filter](#bloom-filter) - [Example](#example-1) - [Reserving Bloom filters](#reserving-bloom-filters) - [total size of bloom filter](#total-size-of-bloom-filter) - [Performance](#performance) - [Cuckoo filter](#cuckoo-filter) - [User Cases](#user-cases) - [Example](#example-2) - [Cuckoo vs Bloom Filter](#cuckoo-vs-bloom-filter) - [Sizing Cuckoo filters](#sizing-cuckoo-filters) - [t-digest](#t-digest) - [Use Cases](#use-cases) - [Examples](#examples-1) - [Estimating fractions or ranks by values](#estimating-fractions-or-ranks-by-values) - [Estimating values by fractions or ranks](#estimating-values-by-fractions-or-ranks) - [trimmed mean](#trimmed-mean) - [TDIGEST.MERGE](#tdigestmerge) - [Retrieving sketch information](#retrieving-sketch-information) - [Resetting a sketch](#resetting-a-sketch) - [Redis Programmability](#redis-programmability) - [Introduce](#introduce) - [Backgroud](#backgroud) - [running scripts](#running-scripts) - [Read-only scripts](#read-only-scripts) - [Sandboxed script context](#sandboxed-script-context) - [Maximum execution time](#maximum-execution-time) - [Scripting with Lua](#scripting-with-lua) - [Getting Started](#getting-started) - [Script Parameterization](#script-parameterization) - [Interact with redis from a script](#interact-with-redis-from-a-script) - [Script Cache](#script-cache) - [Cache volatility](#cache-volatility) - [evalsha in context of pipelining](#evalsha-in-context-of-pipelining) - [Script Cache Semantics](#script-cache-semantics) - [Script Command](#script-command) - [Script Replication](#script-replication) - [Replicating commands instead of Scripts](#replicating-commands-instead-of-scripts) - [Scripts with deterministic writes](#scripts-with-deterministic-writes) - [Debugging Eval scripts](#debugging-eval-scripts) - [Execution under low memory conditions](#execution-under-low-memory-conditions) - [Eval flags](#eval-flags) - [Redis Pub/sub](#redis-pubsub) - [Delivery semantics](#delivery-semantics) - [format of pushed messages](#format-of-pushed-messages) - [Database \& Scoping](#database--scoping) - [Example](#example-3) - [Pattern-matching subscriptions](#pattern-matching-subscriptions) - [Messages matching both a pattern and a channel subscription](#messages-matching-both-a-pattern-and-a-channel-subscription) - [the meaning of the subscription count with pattern matching](#the-meaning-of-the-subscription-count-with-pattern-matching) - [Sharded Pub/Sub](#sharded-pubsub) - [Redis Keyspace notifaction](#redis-keyspace-notifaction) - [Type of events](#type-of-events) - [Configuration](#configuration) - [Timing of expired events](#timing-of-expired-events) - [Events in cluster](#events-in-cluster) - [Redis Cluster Speification](#redis-cluster-speification) - [Main properties and rationales of the design](#main-properties-and-rationales-of-the-design) - [Redis Cluster Goals](#redis-cluster-goals) - [Implemented subset](#implemented-subset) - [Client and Server roles in the Redis cluster protocol](#client-and-server-roles-in-the-redis-cluster-protocol) - [write safety](#write-safety) - [connected to the majority of masters](#connected-to-the-majority-of-masters) - [connected to the minority of masters](#connected-to-the-minority-of-masters) - [Availability](#availability) - [Performance](#performance-1) - [Why merge operations are avoided](#why-merge-operations-are-avoided) - [Overview of Redis Cluster main components](#overview-of-redis-cluster-main-components) - [key distribution model](#key-distribution-model) - [hash tags](#hash-tags) - [global-style patterns](#global-style-patterns) - [Cluster node attributes](#cluster-node-attributes) - [node ID](#node-id) - [node Attributes](#node-attributes) - [CLUSTER NODES](#cluster-nodes) - [Cluster bus](#cluster-bus) - [Cluster topology](#cluster-topology) - [node handshake](#node-handshake) - [Redirection and Resharding](#redirection-and-resharding) - [MOVED Redirection](#moved-redirection) - [Live reconfiguration](#live-reconfiguration) - [ADDSLOTS/DELSLOTS](#addslotsdelslots) - [MIGRATING/IMPORTING](#migratingimporting) - [MIGRATE](#migrate) - [`CLUSTER GETKEYSINSLOT`](#cluster-getkeysinslot) - [MIGRATE](#migrate-1) - [ASK redirection](#ask-redirection) - [Client connections and redirection handling](#client-connections-and-redirection-handling) - [CLUSTER SLOTS](#cluster-slots) - [Multi-keys operations](#multi-keys-operations) # redis ## Using Command ### Keys and values #### Content of Keys key为data model中`拥有含义的文本名称`。Redis key在命名格式方面几乎没有限制,故而key name中可以包含空格或标点符号。`redis key并不支持namespace或categories`,故而在命名时应当避免命名冲突。 通常,会使用`:`符号来将redis的命名分割为多个sections,例如`office:London`,可以使用该命名规范来实现类似`categories`的效果。 尽管keys通常是文本的,redis中也实现了`binary-safe` key。可以使用任何`byte sequence`来作为有效的key,并且,在redis中`empty string`也可以作为有效的key。 redis key在命名时存在如下规范: - 不推荐使用长度很长的key,会在存储和key-comparisions方面带来负面影响 - 不推荐使用长度非常短的key,其会减少可读性,通常`user:1000:followers`的可读性相较`u1000flw`来说可读性要更好,并且前者带来的额外存储开销也较小 - 应在命名时使用统一的命名模式,例如`object-type:id`,在section中包含多个单词时,可以使用`.`或`-`符号来进行分隔,例如`comment:4321:reply.to`或`comment:4321:reply-to` - key size的最大允许大小为512MB #### Hashtags redis通过hashing来获取`key`关联的value。 通常,整个key都会被用作hash index的计算,但是,在部分场景下,开发者可能只希望使用key中的一部分来计算hash index。此时,可以通过`{}`包围key中`想要计算hash index的部分`,该部分被称为hash-tag`。 例如,`person:1`和`person:2`这两个key会计算出不同的hash index;但是`{persion}:1`和`{person}:2`这两个key计算出的hash index却是相同的,因为只有`person`会被用于计算hash index。 通常,hashtag的应用场景是`在集群场景下进行multi-key operations`。在集群场景下,除非所有key计算出的hash index相同,否则集群并不允许执行multi-key操作。 例如,`SINTER`命令用于查询两个不同`set values`的交集,可以接收多个key。在集群场景下: ```redis SINTER group:1 group:2 ``` 上述命名并无法成功执行,因为`group:1`和`group:2`两个key的hash index不同。 但是,如下命令在集群环境下则是可以正常执行: ```redis SINTER {group}:1 {group}:2 ``` hashtag让两个key产生相同的hash值。 虽然hashtag在上述场景下有效,但是,不应该过度的使用hashtag。因为hashtag相同的key其hash index都相同,故而会被散列到同一个slot中,当同一slot中元素过多时,会导致redis的性能下降。 #### altering and querying the key space 存在部分命令,其并不针对特定的类型,而是用于和key space进行交互,其可以被用于所有类型的key。 例如,`EXISTS`命令会返回0和1,代表给定key在redis中是否存在;而`DEL`命令则是用于删除key和key关联的value,无论value是什么类型。 示例如下: ```bash > set mykey hello OK > exists mykey (integer) 1 > del mykey (integer) 1 > exists mykey (integer) 0 ``` 在上述示例中,`DEL`命令返回的值为1或0,代表要被删除的值在redis中是否存在。 `TYPE`命令则是可以返回`key所关联value的类型`: ```bash > set mykey x OK > type mykey string > del mykey (integer) 1 > type mykey none ``` #### key expiration 在redis中,不管key对应的value为何种类型,都支持`key expiration`特性。`key exipiration`支持为key设置超时,`key expiration`也被称为`time to live`/`TTL`,当`ttl`指定的时间过去后,key将会被自动移除。 对于key expiration: - 在对key设置key expiration时,可以按照秒或毫秒的精度进行设置 - 但是,`expire time`在解析时单位永远为毫秒 - expire相关的信息会被`replicated`并存储在磁盘中,即使在redis宕机时,`time virtually passes`(即redis key其expire若为1天,宕机4小时后恢复,其expire会变为8小时,宕机并不会导致key expire停止计算) 可以通过`EXPIRE`命令来设置key expiration: ```bash > set key some-value OK > expire key 5 (integer) 1 > get key (immediately) "some-value" > get key (after some time) (nil) ``` 在第二次调用时,delay超过5s,key已经不存在。 上述示例中,`expire key 5`将key的超时时间设置为了5s,`EXPIRE`用于为key指定不同的超时时间。 类似的,可以通过`PERSIST`命令来取消key的超时设置,让key永久被保留。 除了使用`expire`来设置超时外,在创建时也能会key指定expiration: ```bash > set key 100 ex 10 OK > ttl key (integer) 9 ``` 上述示例中,使用`ttl`命令来检查key的超时时间。 如果想要按照毫秒来设置超时,可以使用`PEXPIRE`和`PTTL`命令。 #### Navigating the keyspace ##### Scan `SCAN`命令支持对redis中key的增量迭代,在每次调用时只会返回一小部分数据。该命令可以在生产中使用,并不会像`keys`或`smembers`等命令一样,在处理大量elements或keys时可能产生长时间的阻塞。 scan使用实例如下: ```bash > scan 0 1) "17" 2) 1) "key:12" 2) "key:8" 3) "key:4" 4) "key:14" 5) "key:16" 6) "key:17" 7) "key:15" 8) "key:10" 9) "key:3" 10) "key:7" 11) "key:1" > scan 17 1) "0" 2) 1) "key:5" 2) "key:18" 3) "key:0" 4) "key:2" 5) "key:19" 6) "key:13" 7) "key:6" 8) "key:9" 9) "key:11" ``` scan是一个cursor based iterator,每次在调用scan命令时,都会返回一个`update cursor`,并且在下次调用scan时需要使用上次返回的cursor。 当cursor被设置为0时,iteration将会开始,并且当server返回的cursor为0时,iteration结束。 ##### keys 除了scan外,还可以通过keys命令来迭代redis中所有的key。但是,和`scan`的增量迭代不同的是,keys会一次性返回所有的key,在返回前会阻塞redis-server。 keys命令支持glob-style pattern: - `h?llo`:`?`用于匹配单个字符 - `h*llo`: `*`用于匹配除`/`外的任何内容 - `h[ae]llo`: 匹配`hallo`和`hello` - `h[^e]llo`: [^e]匹配除`e`外的任何字符 - `h[a-b]llo`: 匹配`hallo`和`hbllo` global-style pattern中转义符为`\` ### pipelining redis pipelining支持一次发送多条命令,而非`逐条发送命令,并且发送后一条命令之前必须要等待前一条请求执行完成`。pipelining被绝大多数redis client支持,能够提高性能。 #### Request/Response protocols and round-trip time(RTT) redis是一个使用`client-server model`的tcp server,在请求完成前,会经历如下步骤: - client向server发送query,并且阻塞的从socket中读取server的响应 - server接收到client的请求后,处理命令,并且将处理结果返回给client 例如,包含4条命令的命令序列如下: 1. client: incr x 2. server: 1 3. client: incr x 4. server: 2 5. client: incr x 6. server: 3 7. client: incr x 8. server: 4 client和server之间通过网络进行连接,每次client和server的请求/响应,都需要经历`client发送请求,server发送响应`的过程,该过程会经过网络来传输,带来传输延迟。 该延迟被称为`RTT`(round trip time), 如果在一次请求中能够发送`n`条命令,那么将能够节省`n-1`次网络传输的往返时间。例如,RTT如果为`250ms`,即使server能够以`100K/s`的速度处理请求,对于同一client,其每秒也只能处理4条命令。 #### redis pipelining 在redis server处理命令时,其处理新请求前并不要求client接收到旧请求,并且client在发送多条命令后,会一次性统一读取执行结果。 该技术被称为`Pipelining`,在其他协议中也有广泛使用,例如`POP3`。 pipelining在redis的所有版本中都被支持,示例如下: ```bash $ (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379 +PONG +PONG +PONG ``` 通过pipelining,并不需要对每个命令都花费RTT用于网络传输,而是在一次网络传输时就包含3条命令。 > 当client使用pipelining发送commands时,server会在内存中对replies进行排队。故而,在client需要使用pipeline向server发送大量的请求时,其需要分批发送,每批中包含合适数量的命令。 > pipeline会积累多条命令并一次性发送给server。 #### Performance Imporvement pipeline不仅能够减少RTT的次数,其也能够增加redis server在每秒的执行操作数。 在redis server处理command时,实际的处理逻辑开销很小,但是和socket io的交互开销却很大。在进行socket io时,会进行`write`和`read`的系统调用,其涉及到用户态和内核态的切换,这将带来巨大的开销。 如果使用pipeline,多条指令只需要调用一次`read`系统调用,并且多条执行的执行结果只需要通过一次`write`系统调用即能够执行。通过使用pipeline,能够有效降低redis server的系统调用次数,这将减少socket io带来的开销,故而redis server能够在一秒内执行更多的commands。 #### pipelining vs scripting 相比于pipelining,scripting可以在`read, compute, write`的场景下带来更高的性能。pipelining并无法处理`read, compute, write`的场景,因为在执行write command之前,需要先获取read command的执行结果,故而无法将read和write命令通过pipeline同时发送给server。 ### Transactions redis transaction支持`execute a group of commands in a single step`,其涉及到`multi, exec, discard, watch`命令。 - 在redis事务中,所有命令都会被串行、按顺序执行,在redis transaction执行时,其他client发送的请求永远不会插入到redis transaction中间。在redis transaction中,所有命令都会`executed as a single siolated operation`,事务执行的过程中不会被其他命令打断 - `EXEC`命令会触发事务中所有命令的执行,故而,当client在`事务上下文中exec命令调用之前`,失去了与server的连接,事务中的命令都不会被执行。只有当exec被调用后,事务中的命令才会实际开始执行 #### Usage 可以通过`multi`命令来进入redis事务,该命令总会返回`ok`。在进入事务后,可以向事务中添加多条命令,这些命令并不会被立马执行,而是被排队。只有当发送`EXEC`命令后,之前排队的命令才会被实际执行。 `DISCARD`命令可以清空被排队的命令,并且退出事务的上下文。 如下示例展示了如何通过事务原子的执行一系列命令: ```bash > MULTI OK > INCR foo QUEUED > INCR bar QUEUED > EXEC 1) (integer) 1 2) (integer) 1 ``` `EXEC`命令会返回一个数组,数组中元素为之前QUEUED COMMANDS的返回结果,顺序和命令被添加到队列中的顺序相同。 在事务上下文中,所有命令(`EXEC`除外)都会返回`QUEUED`。 #### Errors inside transaction 在使用事务时,可能遇到如下两种errors: - 将命令添加到queue中时可能发生失败,该时机在`EXEC`被执行之前。例如,command可能存在语法错误,或是存在内存错误等,都可能导致命令添加到queue失败 - 调用`EXEC`对入队的命令实际执行时,可能发生异常,例如在实际执行command时,对string类型的value执行了list操作 对于`EXEC`时产生的错误,并没有特殊处理:`即使事务中部分命令实际执行失败,其他的命令也都会被执行`。 示例如下所示: ```redis Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. MULTI +OK SET a abc +QUEUED LPOP a +QUEUED EXEC *2 +OK -WRONGTYPE Operation against a key holding the wrong kind of value ``` 上述示例中执行了两个命令,其中命令1执行成功而命令2执行失败。 需要注意的是,`事务中即使某个命令执行失败,queue中的其他命令仍然会被执行`,redis在执行某条命令失败时,并不会对别的命令执行造成影响。 #### rollback for redis Transaction 对于redis transaction,其并不对`rollback`做支持,rollback会对redis的性能造成巨大影响,也会影响redis的易用性。 #### Discard the command queue 如果想要对事务进行abort,可以调用`DISCARD`命令,在该场景下,并不会有命令被实际执行,并且连接状态也会恢复为正常: ```redis > SET foo 1 OK > MULTI OK > INCR foo QUEUED > DISCARD OK > GET foo "1" ``` #### Optimistic locking using check-and-set 在redis transaction中,`WATCH`命令用于提供`CAS`行为。`watched keys`将会被监控,并探知其是否发生变化。 在执行`EXEC`命令前,如果存在任一key发生过修改,那么整个事务都会发生`abort`,并且会返回`NULL`,用以提示事务失败。 如下是一个`read, compute, write`的示例: ``` val = GET mykey val = val + 1 SET mykey val ``` 上述逻辑在`只存在一个客户端`的场景下工作正常,但是当存在多个客户端时,将会发生竞争。由于上述逻辑并不是原子的,故而可能出现如下场景: 1. client A read old value 2. client B read old value 3. client A compute `old value + 1` 4. client B compute `old value + 1` 5. client A set new value with `old value + 1` 6. client B set new value with `old value + 1` 故而,在多client场景下,假设old value为10,即使client A和client B都对value进行了incr,最后new value的值仍有可能为11而不是12 通过WATCH机制能够解决该问题 ``` WATCH mykey val = GET mykey val = val + 1 MULTI SET mykey $val EXEC ``` 在上述示例中,在进入事务上下文前,client对mykey进行了watch并完成新值的计算,之后,进入事务上下文后,用new value设置mykey,并调用`EXEC`命令。 如果WATCH和EXEC之间,存在其他client修改了mykey的值,那么当前事务将会失败。 只需要在发生竞争时重新执行上述流程,那么其即是乐观锁。 #### WATCH `WATCH`命令会让`EXEC`是条件的: - 只有当所有watched keys都未修改的前提下,才会让redis实际执行transaction `watched keys`可能发生如下的修改: - watched keys可能被其他client修改 - watched keys可能被redis本身修改,redis本身的修改包含如下场景 - expiration - eviction 如果在`对keys进行watch`和实际调用`exec`之间,keys发生的变化,整个transaction都会被abort。 > 在redis 6.0.9之前,expired keys并不会造成redis transaction被abort > 在本事务内的命令并不会造成WATCH condition被触发,因为WATCH机制的时间范围为keys watched的时间点到exec调用前的时间点,而queued commands在调用exec后才会实际执行 `watch`命令可以被多次调用,所有的watch命令都会生效,并且在watch被调用后就开始监控key的变化,监控一直到`EXEC`被调用后才结束。 对于`WATCH`命令,可以传递任意数量的参数。 在`EXEC`命令被调用后,所有的watched keys都会被`unwatched`,不管事务是否被aborted。并且,当client连接关闭后,所有keys都会被unwatched。 对于`discard`命令,其在调用后所有watched keys也会自动被`unwatched`。 #### UNWATCH 可以通过`UNWATCH`命令(无参数)来清空所有的watched keys。 通常,在调用`MULTI`进入事务前,会执行如下操作: - `WATCH mykey` - `GET mykey` 如果`在GET mykey`后,`调用MULTI`之前,如果在读取mykey的值后不再想执行后续事务了,那么可以直接调用`UNWATCH`,对先前监视的所有key取消监视。 #### Using WATCH to implement ZPOP 如下是一个使用WATCH的示例 ```redis WATCH zset element = ZRANGE zset 0 0 MULTI ZREM zset element EXEC ``` > 如果`EXEC失败,那么其将返回Null`,所以仅需对之前操作进行重试即可 ## Data Types ### Redis Strings Redis strings存储字节序列,包含文本、serialized objects、binary array。strings通常被用于缓存,但是支持额外的功能,例如counters和位操作。 redis keys也为strings。 string data type可用于诸多用例,例如对html fragement/html page的缓存。 #### SET / GET 通过set和get命令,可以对string value进行设置和获取。 ```redis-cli > SET bike:1 Deimos OK > GET bike:1 "Deimos" ``` 在使用`SET`命令时,如果key已存在对应的值,那么set指定的value将会对已经存在的值进行替换。`即使key对应的旧值并不是strings类型,set也会对其进行替换`。 values可以是`strings of every kind`(包含binary data),故而支持在value中存储jpeg image。value的值不能超过512MB. #### set with additional arguments 在使用`set`命令时,可以为其提供额外的参数,例如`NX | XX`. - `NX`: 仅当redis不存在对应的key时才进行设置,否则失败(返回nil) - `XX`: 仅当redis存在对应的key时的才进行设置,否则失败(返回nil) #### GETSET GETSET命令将会将key设置为指定的new value,并且返回oldvalue的值。 ```redis-cli 127.0.0.1:6379> get bike:1 (nil) 127.0.0.1:6379> GETSET bike:1 3 (nil) 127.0.0.1:6379> GET bike:1 "3" ``` #### MSET / MGET strings类型支持通过`mset, mget`命令来一次性获取和设置多个keys,这将能降低RTT带来的延迟。 ```redis-cli > mset bike:1 "Deimos" bike:2 "Ares" bike:3 "Vanth" OK > mget bike:1 bike:2 bike:3 1) "Deimos" 2) "Ares" 3) "Vanth" ``` #### strings as counters strings类型支持atomic increment: ```redis-cli > set total_crashes 0 OK > incr total_crashes (integer) 1 > incrby total_crashes 10 (integer) 11 ``` `incr`命令会将string value转化为integer,并且对其进行加一操作。类似命令还有`incrby, decr, drcrby`。 #### Limits 默认情况下,单个redis string的最大限制为`512MB`。 ### JSON redis支持对json值的存储、更新和获取。redis json可以和redis query engine进行协作,从而允许`index and query json documents`。 > 在redis 8中内置支持了RedisJSON,否则需要手动安装RedisJSON module。 #### `JSON.SET` `JSON.SET`命令支持将redis的key设置为JSON value,示例如下: ```redis-cli 127.0.0.1:6379> JSON.SET bike $ '"Hyperion"' OK 127.0.0.1:6379> JSON.GET bike $ "[\"Hyperion\"]" 127.0.0.1:6379> type bike ReJSON-RL 127.0.0.1:6379> JSON.TYPE bike $ 1) "string" ``` 在上述示例中,`$`代表的是指向json document中value的`path`: - 在上述示例中,`$`代表root 除此之外,JSON还支持其他string operation。`JSON.STRLNE`支持获取string长度,并且可以通过`JSON.STRAPPEND`来在当前字符串后追加其他字符串: ```redis-cli > JSON.STRLEN bike $ 1) (integer) 8 > JSON.STRAPPEND bike $ '" (Enduro bikes)"' 1) (integer) 23 > JSON.GET bike $ "[\"Hyperion (Enduro bikes)\"]" ``` #### json数值操作 RedisJSON支持`increment`和`multiply`操作: ```redis-cli > JSON.SET crashes $ 0 OK > JSON.NUMINCRBY crashes $ 1 "[1]" > JSON.NUMINCRBY crashes $ 1.5 "[2.5]" > JSON.NUMINCRBY crashes $ -0.75 "[1.75]" > JSON.NUMMULTBY crashes $ 24 "[42]" ``` #### json数组操作 RedisJSON支持通过`JSON.SET`将值赋值为数组,`path expression`支持数组操作 ```redis-cli > JSON.SET newbike $ '["Deimos", {"crashes": 0}, null]' OK > JSON.GET newbike $ "[[\"Deimos\",{\"crashes\":0},null]]" > JSON.GET newbike $[1].crashes "[0]" > JSON.DEL newbike $[-1] (integer) 1 > JSON.GET newbike $ "[[\"Deimos\",{\"crashes\":0}]]" ``` ##### `JSON.DEL` `JSON.DEL`支持通过`path`对json值进行删除。 ##### `JSON.ARRAPPEND` 支持向json array中追加值。 ##### `JSON.ARRTRIM` 支持对json array进行裁剪。 ```redis-cli > JSON.SET riders $ [] OK > JSON.ARRAPPEND riders $ '"Norem"' 1) (integer) 1 > JSON.GET riders $ "[[\"Norem\"]]" > JSON.ARRINSERT riders $ 1 '"Prickett"' '"Royce"' '"Castilla"' 1) (integer) 4 > JSON.GET riders $ "[[\"Norem\",\"Prickett\",\"Royce\",\"Castilla\"]]" > JSON.ARRTRIM riders $ 1 1 1) (integer) 1 > JSON.GET riders $ "[[\"Prickett\"]]" > JSON.ARRPOP riders $ 1) "\"Prickett\"" > JSON.ARRPOP riders $ 1) (nil) ``` #### json object操作 json oject操作同样有其自己的命令,示例如下: ```redis-cli > JSON.SET bike:1 $ '{"model": "Deimos", "brand": "Ergonom", "price": 4972}' OK > JSON.OBJLEN bike:1 $ 1) (integer) 3 > JSON.OBJKEYS bike:1 $ 1) 1) "model" 2) "brand" 3) "price"> JSON.SET bike:1 $ '{"model": "Deimos", "brand": "Ergonom", "price": 4972}' ``` #### format output redis-cli支持对json内容的输出进行格式化,步骤如下: - 在执行`redis-cli`时指定`--raw`选项 - 通过formatting keywords来进行格式化 - `INDENT` - `NEWLINE` - `SPACE` ```bash $ redis-cli --raw > JSON.GET obj INDENT "\t" NEWLINE "\n" SPACE " " $ [ { "name": "Leonard Cohen", "lastSeen": 1478476800, "loggedOut": true } ] ``` #### Limitation 传递给command的json值最大深度只能为128,如果嵌套深度大于128,那么command将返回错误。 ### Redis lists Redis lists为string values的链表,redis list通常用于如下场景: - 实现stack和queue - 用于backgroup worker system的队列管理 #### Blocking commands redis lists中支持阻塞命令 - `BLPOP`: 从`head of a list`移除并且返回一个element,如果list为空,该command会阻塞,直至list被填充元素或发生超时 - `BLMOVE`: 从source list中pop一个element,并且将其push到target list中。如果source list为空,那么command将会被阻塞,直到source list中出现new element > 在上述文档描述中,`阻塞`实际指的是针对客户端的阻塞。该`Blocking Command`命令的调用会实际的阻塞客户端,直到redis server返回结果。 > > 但是,`server并不会被blocking command所阻塞`。redis server为单线程模型,当用户发送blocking command给server,并且该command无法立即被执行而导致客户端阻塞时,`server会挂起该客户端的连接`,并且转而处理其他请求,直至该客户端的阻塞命令满足执行条件时,才会将挂起的连接唤醒重新执行。 > > 故而,`Blocking Command`并不会对server造成阻塞,而是会阻塞客户端的调用。 #### Queue(first in, first out) 依次调用`LPUSH`后再依次调用`RPOP`,可模拟队列行为,元素的弹出顺序和元素的添加顺序相同: ```redis-cli > LPUSH bikes:repairs bike:1 (integer) 1 > LPUSH bikes:repairs bike:2 (integer) 2 > RPOP bikes:repairs "bike:1" > RPOP bikes:repairs "bike:2" ``` #### Stack(first in, last out) 依次调用`LPUSH`后再依次调用`LPOP`,可模拟栈的行为,元素的移除顺序和添加顺序相反: ```redis-cli > LPUSH bikes:repairs bike:1 (integer) 1 > LPUSH bikes:repairs bike:2 (integer) 2 > LPOP bikes:repairs "bike:2" > LPOP bikes:repairs "bike:1" ``` #### check length of list 可通过`LLEN`命令来检查list的长度 ```redis-cli > LLEN bikes:repairs (integer) 0 ``` #### Atomically pop one element from one list and push to another 通过`lmove`命令能够实现原子的`从srclist移除并添加到dstlist`的操作 ```redis-cli > LPUSH bikes:repairs bike:1 (integer) 1 > LPUSH bikes:repairs bike:2 (integer) 2 > LMOVE bikes:repairs bikes:finished LEFT LEFT "bike:2" > LRANGE bikes:repairs 0 -1 1) "bike:1" > LRANGE bikes:finished 0 -1 1) "bike:2" ``` #### trim the list 可以通过`LTRIM`命令来完成对list的裁剪操作: ```redis-cli > LPUSH bikes:repairs bike:1 (integer) 1 > LPUSH bikes:repairs bike:2 (integer) 2 > LMOVE bikes:repairs bikes:finished LEFT LEFT "bike:2" > LRANGE bikes:repairs 0 -1 1) "bike:1" > LRANGE bikes:finished 0 -1 1) "bike:2" ``` #### Redis List Impl redis lists是通过Linked List来实现的,其元素的添加操作并开销永远是常量的,并不会和Array一样因扩容而可能导致内存的复制。 #### `LPUSH, RPUSH` `LPUSH`会将元素添加到list的最左端(头部),而`RPUSH`则会将新元素添加奥list的最右端(尾部)。 LPUSH和RPUSH接收的参数都是可变的,在单次调用中可以向list中添加多个元素。 例如,向空list中调用`lpush 1 2 3`时,其等价于`lpush 1; lpush 2; lpush3`,即调用后list中元素为`3,2,1` #### `LRANGE` `LRANGE`命令能够从list中解析范围内的数据,其接收两个indexes,为range中开始和结束元素的位置。index可以是负的,负数代表从尾端开始计数: - `-1`代表最后的元素 - `-2`代表倒数第二个元素 ```redis-cli > RPUSH bikes:repairs bike:1 (integer) 1 > RPUSH bikes:repairs bike:2 (integer) 2 > LPUSH bikes:repairs bike:important_bike (integer) 3 > LRANGE bikes:repairs 0 -1 1) "bike:important_bike" 2) "bike:1" 3) "bike:2" ``` #### `LPOP, RPOP` lists支持pop元素,从list中移除元素,并且获取元素的值。lists支持从list的左端和右端pop元素,使用示例如下所示: ```redis-cli > RPUSH bikes:repairs bike:1 bike:2 bike:3 (integer) 3 > RPOP bikes:repairs "bike:3" > LPOP bikes:repairs "bike:1" > RPOP bikes:repairs "bike:2" > RPOP bikes:repairs (nil) ``` #### Common use cases for lists redis lists拥有如下有代表性的用例场景: - 记录用户最新上传的推文 - 用于进程间的通信,使用consumer-producer pattern,其中生产者向lists中推送内容,而消费者消费lists中的内容 #### Capped lists - `latest n` 在许多用例场景下,会使用lists来存储latest items,例如`social network updates, logs`等。 redis允许将lists作为`拥有容量上限的集合使用`,可以通过`LTRIM`命令来实现`only remembering the latest N items and discarding all the oldest items`。 `LTRIM`命令和`LRANGE`类似,但是`LRANGE`用于获取获取list中指定范围内的元素,`LTRIM`会将选中的范围作为`new list value`,所有位于选中范围之外的元素都会从list中被移除。 `LTRIM`的使用示例如下所示: ```redis-cli > RPUSH bikes:repairs bike:1 bike:2 bike:3 bike:4 bike:5 (integer) 5 > LTRIM bikes:repairs 0 2 OK > LRANGE bikes:repairs 0 -1 1) "bike:1" 2) "bike:2" 3) "bike:3" ``` `LTRIM 0 2`会令redis保留index位于`[0, 2]`范围内的3个元素,并且移除其他的元素。将`push操作`和`LTRIM`操作组合,可以实现`add a new element and discard elements exceeding a limt`的操作。 例如,使用`LRANGE -3 -1`可用于实现`仅保留最近添加的三个元素`的场景 ```redis-cli > RPUSH bikes:repairs bike:1 bike:2 bike:3 bike:4 bike:5 (integer) 5 > LTRIM bikes:repairs -3 -1 OK > LRANGE bikes:repairs 0 -1 1) "bike:3" 2) "bike:4" 3) "bike:5" ``` #### Blocking operations on lists lists的`blocking operation`特性令其适合用于实现queues,并广泛用于进程间通信系统。 在通过redis lists实现进程间通信系统时,如果某些时刻list为空,并不存在任何元素,那么此时消费者client在调用`pop`操作时只会返回为空。通常来讲,consumer会等待一定的时间并且重新尝试调用pop,该操作被称为`polling`,其通常被认为是一种不好的实现: - 其会强制redis/client来处理无用的命令(`当list为空时,pop请求只会返回为空而不会任何的实际处理`) - 会增加`delay to processing of items`,因为worker在接收到redis server返回的null时,其会等待一定的时间。为了令delay更小,可以在调用POP操作之间等待更短的时间,但是其可能方法前一个问题(`当pop调用之间的时间间隔更小时,redis server可能会处理更多的无用命令`) 故而,redis实现支持`BRPOP`和`BLPOP`命令,其命令在list为空时会阻塞:`上述命令造成的阻塞会在list中被添加新元素时返回,如果直到设置的超时到达后,该操作也会返回`。 `BRPOP`的使用示例如下所示: ```redis-cli > RPUSH bikes:repairs bike:1 bike:2 (integer) 2 > BRPOP bikes:repairs 1 1) "bikes:repairs" 2) "bike:2" > BRPOP bikes:repairs 1 1) "bikes:repairs" 2) "bike:1" > BRPOP bikes:repairs 1 (nil) (2.01s) ```` 上述示例中,`BRPOP bikes:repairs 1`代表`wait for elements in the list bikes:repairs`,但是`当list中元素为空时,最多等待1s。` 当为`BRPOP`指定timeout为0时,代表会永久等待elements。并且,`可以为BRPOP`命令指定多个lists,其会`等待所有的list,并且当任何一个list中接收到元素时,当前BRPOP命令会立刻返回`。 示例如下: ```redis-client # client 1 等待 event-queue:1, event-queue:2, event-queue:3三个list client 1> brpop event-queue:1 event-queue:2 event-queue:3 1000 # client 2 向event-queue:2 中追加元素 client 2> rpush event-queue:2 baka (integer) 1 # client 1 立刻返回,返回结果如下 1) "event-queue:2" 2) "baka" (19.55s) ``` ##### BRPOP - 对于`BRPOP`命令造成的阻塞,其处理是按照顺序的:`the first client that blocked waiting for a list, is served first when an element is pushed by some other client, and so forth` - `BRPOP`命令的返回结果其结构和`RPOP`命令不同:`BRPOP`返回的是一个包含两个元素的array,`arrary[0]`为list对应的key,`array[1]`为弹出的元素(因为BRPOP可以等待多个list) - 如果超时后list中仍然没有可获取的元素,那么将会返回null #### Automatic Creation and removal of keys 在先前示例中,向list中添加元素时,并没有预先创建空的list,或是在list中没有元素时将list手动移除。 在redis中,`list的创建和删除都是redis的职责`: - 当list中不再包含元素时,redis会自动删除list对应的key - 当想要对不存在的key中添加元素时,redis会自动创建一个empty list 故而,可以整理除如下准则: - 当将元素添加到一个聚合数据类型时,如果target key不存在,那么在添加元素前一个empty aggregate data type将会被自动创建 - 当从aggregate data type中移除元素时,如果移除后该aggregate data type中为空,那么key将会被自动销毁(`stream data type`除外) - 调用一个read-only command(例如`LLEN`)或write command(用于移除元素)对一个`empty key`做操作时,其返回结果和针对`an key holding an empty aggregate type of type the command expects to find`的操作一直 上述三个准则的示例如下: 准则1示例如下所示,当new_bikes不存在时,通过`LPUSH`命令向其中添加元素,一个empty list在添加前会自动创建 ```redis-cli > DEL new_bikes (integer) 0 > LPUSH new_bikes bike:1 bike:2 bike:3 (integer) 3 ``` 准则2示例如下所示,当pop出所有的元素后,key将会被自动销毁,通过`EXISTS`命令返回的结果为0: ```redis-cli > LPUSH bikes:repairs bike:1 bike:2 bike:3 (integer) 3 > EXISTS bikes:repairs (integer) 1 > LPOP bikes:repairs "bike:3" > LPOP bikes:repairs "bike:2" > LPOP bikes:repairs "bike:1" > EXISTS bikes:repairs (integer) 0 ``` 准则3的示例如下所示,当key不存在时,对该key进行`read-only`操作和`remove element`操作所返回的结果,和对`empty aggregated data type`操作所返回的结果一致: ```redis-cli > DEL bikes:repairs (integer) 0 > LLEN bikes:repairs (integer) 0 > LPOP bikes:repairs (nil) ``` ### redis sets redis set是一个unordered collection of unique strings,通过redis sets可以高效进行如下操作: - track unique items - represent relations - perform common set operations such as intersection, unions, and differences #### basic commands - `SADD`: 向set中添加new member - `SREM`: 从set中移除指定member - `SISMEMBER`: 检查给定的string是否位于set中 - `SINTER`: 返回两个或更多set的交集 - `SCARD`: 返回set的大小(cardinality) #### SADD `SADD`命令会向set中添加新的元素,示例如下: ```redis-cli > SADD bikes:racing:france bike:1 bike:2 bike:3 (integer) 3 > SMEMBERS bikes:racing:france 1) bike:3 2) bike:1 3) bike:2 ``` #### SMEMBERS 在上述示例中,`SMEMBERS`命令会返回set中所有的元素。`redis`并不保证元素的返回顺序,每次调用`SMEMBERS`命令都可能以任何顺序返回set中的元素。 #### SDIFF 可以通过SDIFF来返回两个sets的差异(差集)。例如,可以通过`SDIFF`命令查看有哪些元素位于`set1`中但是不位于`set2`中,示例如下: ```redis-cli > SADD bikes:racing:usa bike:1 bike:4 (integer) 2 > SDIFF bikes:racing:france bikes:racing:usa 1) "bike:3" 2) "bike:2" ``` 上述示例中,则通过`SDIFF`命令展示了`bikes:racing:france`和`bikes:racing:usa`两个set的差集。 SDIFF命令在`difference between all sets is empty`时,会返回一个empty array。 #### SINTER 可以通过SINTER命令来取多个sets的交集。 ```redis-cli > SADD bikes:racing:france bike:1 bike:2 bike:3 (integer) 3 > SADD bikes:racing:usa bike:1 bike:4 (integer) 2 > SADD bikes:racing:italy bike:1 bike:2 bike:3 bike:4 (integer) 4 > SINTER bikes:racing:france bikes:racing:usa bikes:racing:italy 1) "bike:1" > SUNION bikes:racing:france bikes:racing:usa bikes:racing:italy 1) "bike:2" 2) "bike:1" 3) "bike:4" 4) "bike:3" > SDIFF bikes:racing:france bikes:racing:usa bikes:racing:italy (empty array) > SDIFF bikes:racing:france bikes:racing:usa 1) "bike:3" 2) "bike:2" > SDIFF bikes:racing:usa bikes:racing:france 1) "bike:4" ``` #### SREM 可以通过`SREM`命令来移除set中的元素,可以一次性移除一个或多个。 #### SPOP `SPOP`命令支持随机移除一个element。 #### SRANDMEMBER `SRANDMEMBER`命令支持随机返回一个set中的元素,但是不对其实际移除 上述命令的使用示例如下所示: ```redis-cli > SADD bikes:racing:france bike:1 bike:2 bike:3 bike:4 bike:5 (integer) 5 > SREM bikes:racing:france bike:1 (integer) 1 > SPOP bikes:racing:france "bike:3" > SMEMBERS bikes:racing:france 1) "bike:2" 2) "bike:4" 3) "bike:5" > SRANDMEMBER bikes:racing:france "bike:2" ``` ### redis hashes redis hashes为记录`field-value pair`集合的数据结构,可以使用hashes来表示基本对象或存储counter的分组,示例如下: #### 对象表示 ```redis-cli > HSET bike:1 model Deimos brand Ergonom type 'Enduro bikes' price 4972 (integer) 4 > HGET bike:1 model "Deimos" > HGET bike:1 price "4972" > HGETALL bike:1 1) "model" 2) "Deimos" 3) "brand" 4) "Ergonom" 5) "type" 6) "Enduro bikes" 7) "price" 8) "4972" ``` 通常来讲,可以存储在hash中的fields数量并没有限制。 命令`HSET`可用于向hash中设置多个fields,而命令`HGET`可以用于获取一个field,`HMGET`可以用于获取多个field。 ```redis-cli > HMGET bike:1 model price no-such-field 1) "Deimos" 2) "4972" 3) (nil) ``` 同样的,hash结构支持对单个field进行操作,例如`HINCRBY` ```redis-cli > HINCRBY bike:1 price 100 (integer) 5072 > HINCRBY bike:1 price -100 (integer) 4972 ``` #### counters 将hash用于存储counters分组的示例如下所示: ```redis-cli > HINCRBY bike:1:stats rides 1 (integer) 1 > HINCRBY bike:1:stats rides 1 (integer) 2 > HINCRBY bike:1:stats rides 1 (integer) 3 > HINCRBY bike:1:stats crashes 1 (integer) 1 > HINCRBY bike:1:stats owners 1 (integer) 1 > HGET bike:1:stats rides "3" > HMGET bike:1:stats owners crashes 1) "1" 2) "1" ``` #### Field Expiration 在`redis open source 7.4`中,支持为独立的hash field指定超时: - `HEXPIRE`: set the remaining TTL in seconds - `HPEXPIRE`: set the remaining TTL in milliseconds - `HEXPIREAT`: set expiration time to a timestamp specified in seconds - `HPEXPIREAT`: set the expiration time to a timestamp specified in milliseconds 如上所示,在指定超时时,可以通过时间戳来指定,也可以通过TTL来指定。 同时,获取超时事件也可以通过`时间戳`和`TTL`来获取: - `HEXPIRETIME`: get the expiration time as timestamp in seconds - `HPEXPIRETIME`: get the expiration time as timestamp in milliseconds - `HTTL`: get the remaining ttl in seconds - `HPTTL`: get the remaining ttl in milliseconds 如果想要移除指定hash field的expration,可以通过如下方式: - `HPERSIST`: 移除hash field的超时 ##### Common field expiration use cases - `Event Tracking`:使用hash key来存储`最后一小时的事件`。其中,field为每个事件,而设置事件field的ttl为1小时,并可使用`HLEN`来统计最后一小时的事件数量。 - `Fraud Detection`:通常,用户行为进行分析时,可按小时记录用户的事件数量。可通过hash结果记录过去48小时中每小时的操作数量,其中hash field代表用户某一个小时内的操作数。每个hash field的过期时间都为48h。 - `Customer session management`: 可以通过hash来存储用户数据。为每个session创建一个hash key,并且向hash key中添加session field。当session过期时,自动对session key和session field进行expire操作。 - `Active Session Tracking`: 将所有的active sessions存储再一个hash key中。每当session变为inactive时,将session的TTL设置为过期。可以使用`HLEN`来统计活跃的sessions数量。 ##### Field Expiration examples `对于hash field ixpiration的支持在官方client libraries中尚不可用`,但是可以在`python(redis-py)`和`java(jedis)`的beta版本client libraries中使用。 如下python示例展示了如何使用field expiration: ```py event = { 'air_quality': 256, 'battery_level':89 } r.hset('sensor:sensor1', mapping=event) # set the TTL for two hash fields to 60 seconds r.hexpire('sensor:sensor1', 60, 'air_quality', 'battery_level') ttl = r.httl('sensor:sensor1', 'air_quality', 'battery_level') print(ttl) # prints [60, 60] # set the TTL of the 'air_quality' field in milliseconds r.hpexpire('sensor:sensor1', 60000, 'air_quality') # and retrieve it pttl = r.hpttl('sensor:sensor1', 'air_quality') print(pttl) # prints [59994] # your actual value may vary # set the expiration of 'air_quality' to now + 24 hours # (similar to setting the TTL to 24 hours) r.hexpireat('sensor:sensor1', datetime.now() + timedelta(hours=24), 'air_quality') # and retrieve it expire_time = r.hexpiretime('sensor:sensor1', 'air_quality') print(expire_time) # prints [1717668041] # your actual value may vary ``` ### Sorted sets redis sorted set是一个包含unique strings的集合,其中unique strings会关联一个score,并且strings会按照score进行排序。`如果多个string存在相同的score,那么拥有相同score的strings将会按照字典序进行排序`。 sorted sets的用例场景包括如下: - `Leaderboards`: 可以通过sorted sets维护一个ordered list,其可被用于实现排行榜 - `RateLimiter`: 通过sorted list,可以构建一个`sliding-window rate limiter`,从而避免过多的api调用 - 在实现`sliding-window rate limiter`时,可以将时间戳作为score,因为可以快速获取一个时间范围内的调用数量 可以将sorted sets看作是set和hash的混合, - 和set一样,sorted sets由unique strings组成 - 和hash一样,sorted sets中元素由一个关联的floating point value,被称为score sorted set的使用示例如下: ```redis-cli > ZADD racer_scores 10 "Norem" (integer) 1 > ZADD racer_scores 12 "Castilla" (integer) 1 > ZADD racer_scores 8 "Sam-Bodden" 10 "Royce" 6 "Ford" 14 "Prickett" (integer) 4 ``` `ZADD`和`SADD`类似,但是其接收一个用于表示`score`的额外参数。和`SADD`类似,可以使用`ZADD`来添加多个`score-value pairs`。 > #### Implementation > sorted sets实现的数据结构中,同时使用了`skip list`和`hash table`两种数据结构,故而每次向zset中添加元素时,其操作的复杂度为o(log(n))`。 > > 并且,在获取元素时,由于元素已经被排序,获取操作无需其他的额外开销。 `ZRANGE`的顺序为从小到大,`ZREVRANGE`的顺序则是从大到小 ```redis-cli > ZRANGE racer_scores 0 -1 1) "Ford" 2) "Sam-Bodden" 3) "Norem" 4) "Royce" 5) "Castilla" 6) "Prickett" > ZREVRANGE racer_scores 0 -1 1) "Prickett" 2) "Castilla" 3) "Royce" 4) "Norem" 5) "Sam-Bodden" 6) "Ford" ``` 在上述示例中,0和-1代表index位于`[0, len-1]`范围内的元素。(负数代表的含义和`LRANGE`命令中相同) 在`ZRANGE`命令中指定`withscores`,也能够在返回zset中value时同时返回score: ```redis-cli > ZRANGE racer_scores 0 -1 withscores 1) "Ford" 2) "6" 3) "Sam-Bodden" 4) "8" 5) "Norem" 6) "10" 7) "Royce" 8) "10" 9) "Castilla" 10) "12" 11) "Prickett" 12) "14" ``` #### ZRANGEBYSCORE 除了上述操作外,sorted sets还支持对`operate on ranges`。可以通过`ZRANGEBYSCORE`来实现`get all racers with 10 or fewer points`的操作: ```redis-cli > ZRANGEBYSCORE racer_scores -inf 10 1) "Ford" 2) "Sam-Bodden" 3) "Norem" 4) "Royce" ``` 上述示例中,`ZRANGEBYSCORE racer_score -inf 10`向redis请求`返回score位于negative infinity和10之间(both included)的元素`。 #### ZREMRANGEBYSCORE / ZREM 如果想要丛zset中移除元素,可以调用`ZREM`命令。同样的,sorted sets也支持`remove ranges of elements`的操作,可通过`ZREMRANGEBYSCORE`命令来实现。 ```redis-cli > ZREM racer_scores "Castilla" (integer) 1 > ZREMRANGEBYSCORE racer_scores -inf 9 (integer) 2 > ZRANGE racer_scores 0 -1 1) "Norem" 2) "Royce" 3) "Prickett" ``` 上述示例中,通过`ZREMRANGEBYSCORE racer_scores -inf 9`命令实现了`remove all the racers with strictly fewer than 10 points`的操作。 `ZREMRANGEBYSCORE`会返回其移除的元素个数。 ##### Inclusive and exclusive 在使用`ZRANGEBYSCORE`指定`min`和`max`时,可以分别将其指定为`-inf`和`+inf`。故而,在获取zset中比`xx`大或比`xx`小的所有元素时,可以使用`-inf`和`+inf`,此时无需知道当前zset中的最大/最小元素。 默认情况下,`interval specified by min and max is closed`(inclusive)。但是,可以将其指定为`open interval`(exclusive),只需要在score前添加`(`符号即可,示例如下: ```redis-cli ZRANGEBYSCORE zset (1 5 ``` 上述示例中,会返回位于`(1, 5]`区间内的元素。 而`ZRANGEBYSCORE zset (5 (10`命令则是会返回`(5, 10)`区间内的元素。 #### ZRANK / ZREVRANK sorted sets还支持`get-rank operation`,通过`ZRANK`可以返回`position of an element in the set of ordered elements`。 `ZREVRANK`命令的作用和`ZRANK`类似,但是`ZREVRANK`返回的值为`从大到小的降序ranking`。 使用示例如下所示: ```redis-cli > ZRANK racer_scores "Norem" (integer) 0 > ZREVRANK racer_scores "Norem" (integer) 2 ``` #### `Lexicographical scores` 自redis 2.8其,引入了`getting ranges lexicograhpically`的新特性,其假设sorted set中的所有元素都拥有相同的score。 与`lexicographical ranges`进行交互的主要命令如下: - ZRANGEBYLEX - ZREVRANGEBYLEX - ZREMRANGEBYLEX - ZLEXCOUNT 使用示例如下所示: ```redis-cli > ZADD racer_scores 0 "Norem" 0 "Sam-Bodden" 0 "Royce" 0 "Castilla" 0 "Prickett" 0 "Ford" (integer) 3 > ZRANGE racer_scores 0 -1 1) "Castilla" 2) "Ford" 3) "Norem" 4) "Prickett" 5) "Royce" 6) "Sam-Bodden" > ZRANGEBYLEX racer_scores [A [L 1) "Castilla" 2) "Ford" ``` 在上述示例中,可以通过`ZRANGEBYLEX`按照字典序对range进行请求。 ##### ZRANGEBYLEX ZRANGEBYLEX的语法如下: ``` ZRANGEBYLEX key min max [limit offset count] ``` 和`ZRANGEBYSCORE`命令不同的是,`ZRANGEBYSCORE`在指定范围时,默认是`included`的;而`ZRANGEBYLEX`必须通过`[`和`(`来显式指定inclusive或exclusive。 而在指定min和max时,`-`和`+`分别则代表negatively infinite和positive infinite。故而,`ZRANGEBYLEX myzset - +`命令代表返回zset中所有的元素。 #### Updating the score: leaderboards 支持对sorted set中元素的score进行更新。在更新sorted set中元素的score时,只需要`再次调用ZADD命令即可`,sorted set会更新score,更新操作的时间复杂度为`O(log(N))`。 #### Leaderboard Example 在通过zset实现leaderboard时,由如下两种方式对user score进行更新: - 在得知user当前score的情况下,可以直接通过`ZADD`命令来进行覆盖 - 如果想要针对当前的score进行`增加`操作时,可以使用`ZINCRBY`命令 ```redis-cli > ZADD racer_scores 100 "Wood" (integer) 1 > ZADD racer_scores 100 "Henshaw" (integer) 1 > ZADD racer_scores 150 "Henshaw" (integer) 0 > ZINCRBY racer_scores 50 "Wood" "150" > ZINCRBY racer_scores 50 "Henshaw" "200" ``` ##### ZADD 当当前添加的元素已经在sorted set中存在时,`ZADD`命令会返回`0`,否则`ZADD`命令会返回`1`。 ##### ZINCRBY 而`ZINCRBY`命令则是会返回更新后的new score。 ### redis Streams Redis Stream类型数据结构的行为类似于append-only log,但是实现了`o(1)`时间复杂度的`random access`和复杂的消费策略、consumer groups。通过redis stream,可以实时的记录事件并对事件做同步分发。 通用的redis stream用例如下: - event sourcing(e.g., tracking user actions) - sensor monitoring - notifications(e.g., storing a record of each user's notifications in a separate stream) redis会为每个stream entry生成一个unique ID。可以使用IDs来获取其关联的entries,或读取和处理stream中所有的后续entries。 redis stream支持一些trimming strategies(用于避免stream的无尽增长)。并且,redis stream也支持多种消费策略(XREAD, XREADGROUP, XRANGE)。 #### Examples ##### 添加stream entry 在如下示例中,当racers通过检查点时,将会为每个racer添加一个stream entry,stream entry中包含racer name, speed, position, location ID信息,示例如下: ```redis-cli > XADD race:france * rider Castilla speed 30.2 position 1 location_id 1 "1692632086370-0" > XADD race:france * rider Norem speed 28.8 position 3 location_id 1 "1692632094485-0" > XADD race:france * rider Prickett speed 29.7 position 2 location_id 1 "1692632102976-0" ``` #### 从指定id开始读取stream entries 在如下示例中,将从stream entry ID `1692632086370-0`开始读取两条stream entries: ```redis-cli > XRANGE race:france 1692632086370-0 + COUNT 2 1) 1) "1692632086370-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "30.2" 5) "position" 6) "1" 7) "location_id" 8) "1" 2) 1) "1692632094485-0" 2) 1) "rider" 2) "Norem" 3) "speed" 4) "28.8" 5) "position" 6) "3" 7) "location_id" 8) "1" ``` #### 从末尾开始读取 在如下示例中,会从`end of the stream`开始读取entries,最多读取100条entries,并在没有entries被写入的情况下最多阻塞300ms ```redis-cli > XREAD COUNT 100 BLOCK 300 STREAMS race:france $ (nil) ``` #### Stream basics stream为`append-only`数据结构,其基础的write command为`XADD`,会向指定stream中添加一个新的entry。 每个stream entry都由一个或多个field-value pairs组成,类似dictionary或redis hash: ```redis-cli > XADD race:france * rider Castilla speed 29.9 position 1 location_id 2 "1692632147973-0" ``` ##### XADD 上述示例中,通过`XADD`向key为`race:france`中添加了值为`rider: Castilla, speed:29.9, position: 1, location_id: 2`的entry,并使用了auto-generated entry ID `1692632147973-0`作为返回值。 XADD命令的描述如下: ```redis-cli XADD key [NOMKSTREAM] [KEEPREF | DELREF | ACKED] [ [= | ~] threshold [LIMIT count]] <* | id> field value [field value ...] ``` 在`XADD race:france * rider Castilla speed 29.9 position 1 location_id 2`的命令示例中, - 第一个参数`race:france`代表key name - 第二个参数为entry id,而`*`代表stream中所有的entry id - 在上述示例中,为第二个参数传入`*`代表`希望server生成一个新的ID` - 所有新生成的ID都应该单调递增,即新生成的ID将会比过去所有entries的ID都要大 - 需要显式指定ID而不是新生成的场景比较罕见 - 位于第一个和第二个参数之后的为`field-value pairs` ##### XLEN 可以通过`XLEN`命令来获取Stream中items的个数: ```redis-cli > XLEN race:france (integer) 4 ``` #### Entry IDs entry ID由`XADD`命令返回,并且可以对给定stream中的entries进行唯一标识。entry ID由两部分组成: ``` - ``` - `millisecondsTime`: 该部分代表生成stream id时,redis node的本地时间。但是,如果current milliseconds time比`previous entry time`要小,则会使用`previous entry time`而不是`current milliseconds`。 - 该设计主要是为了解决`时钟回拨`的问题,即使在redis node回拨本地时间的场景下,新生成的ID仍然能够保证单调递增 - `sequenceNumber`: 该部分主要是为了处理`同一ms内创建多条entries的问题` - sequence number的宽度为64bit ##### entry ID设计 entry ID中包含millisecondsTime的原因是,Reids Stream支持`range queries by ID`。因为`ID`关联entry的生成时间,故而可以不花费额外成本的情况下按照`time range`对entries进行查询。 当在某种场景下,用户可能需要`incremental IDs that are not related to time but are actually associated to another external system ID`,此时`XADD`则可以在第二个参数接收一个实际的ID而不是`*`通配符。 - `*`符号会触发auto-generation 手动指定entry ID的示例如下所示: ```redis-cli > XADD race:usa 0-1 racer Castilla 0-1 > XADD race:usa 0-2 racer Norem 0-2 ``` `在通过XADD手动指定entry ID时,后添加的entry ID必须大于先前指定的entry ID`,否则将会返回error。 ```redis-cli > XADD race:usa 0-1 racer Prickett (error) ERR The ID specified in XADD is equal or smaller than the target stream top item ``` 在redis 7及之后,可以仅显式指定`millisecondsTime`的部分,指定后`sequenceNumber`的部分将会自动生成并填充,示例如下所示: ```redis-cli > XADD race:usa 0-* racer Prickett 0-3 ``` #### Redis Stream consumer query model 向redis stream中添加entry的操作可以通过`XADD`进行实现。而从redis stream从读取数据,redis stream支持如下query model: - `Listening for new items`: 和unix command `tail -f`类似,reids stream consumer会监听`new messages that are appended to the stream`。 - 但是,和BLPOP这类阻塞操作不同的是,对于`BLPOP`,一个给定元素只会被传递给`一个client`;而在使用stream时,我们希望`new messages appended to the stream时`,新消息对多个consumers可见。(`tail -f`同样支持新被添加到log文件中的内容被多个进程可见) - 故而,在`Listening for new items`这种query model下,`stream is able to fan out messages to multiple clients` - `Query by range`: 除了上述类似`tail -f`的query model外,可能还希望将redis stream以另一种方式进行使用:并不将redis stream作为messaging system,而是将其看作`time series store` - 在将其作为`time series store`的场景下,其仍然能访问最新的messages,但是其还支持`get messages by ranges of time`的操作,或是`iterate the messages using a cursor to incrementally check all the history` - `Consumer group`: 上述两种场景都是从`consuemrs`的视角来读取/处理消息,但是,redis stream支持另一种抽象:`a stream of messages that can be partitioned to multiple consumers that are processing such messages`。 - 在该场景下,并非每个consumer都必须处理所有的messsages,每个consumer可以获取不同的messages并进行处理 redis stream通过不同的命令支持了以上三种query model。 #### Querying by range: XRANGE and XREVRANGE 为了根据范围查询stream,需要指定两个id:`start ID`和`end ID`。指定的范围为`inclusive`的,包含`start ID`和`end ID`。 `+`和`-`则是代表`greatest ID`和`smallest ID`,示例如下所示: ```redis-cli > XRANGE race:france - + 1) 1) "1692632086370-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "30.2" 5) "position" 6) "1" 7) "location_id" 8) "1" 2) 1) "1692632094485-0" 2) 1) "rider" 2) "Norem" 3) "speed" 4) "28.8" 5) "position" 6) "3" 7) "location_id" 8) "1" 3) 1) "1692632102976-0" 2) 1) "rider" 2) "Prickett" 3) "speed" 4) "29.7" 5) "position" 6) "2" 7) "location_id" 8) "1" 4) 1) "1692632147973-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "29.9" 5) "position" 6) "1" 7) "location_id" 8) "2" ``` 如上所示,返回的每个entry都是由`ID`和`field-value pairs`组成的数组。由于entry ID和时间`currentMillisTime`相关联,故而可以通过XRANGE根据时间范围来查询records。 ##### query by time range `并且,在根据时间范围查询records时,可以省略sequence part的部分`: - 在省略sequence part时,`start`的sequence part将会被设置为0,而`end`的sequence part将会被设置为最大值。 - 故而,可以通过两个milliseconds unix time来进行时间范围内的查询,可以获取在该时间范围内生成的entries(`range is inclusive`) 示例如下 ```redis-cli > XRANGE race:france 1692632086369 1692632086371 1) 1) "1692632086370-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "30.2" 5) "position" 6) "1" 7) "location_id" 8) "1" ``` ##### count option `XRANGE`在范围查询时,还支持指定一个COUNT选项,用于`get first N items`。如果想要进行增量查询,可以用`上次查询的最大ID + 1`作为新一轮查询的`start`。 增量迭代示例如下: ```redis-cli > XRANGE race:france - + COUNT 2 1) 1) "1692632086370-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "30.2" 5) "position" 6) "1" 7) "location_id" 8) "1" 2) 1) "1692632094485-0" 2) 1) "rider" 2) "Norem" 3) "speed" 4) "28.8" 5) "position" 6) "3" 7) "location_id" 8) "1" # 通过(符号指定了左开右闭的区间 > XRANGE race:france (1692632094485-0 + COUNT 2 1) 1) "1692632102976-0" 2) 1) "rider" 2) "Prickett" 3) "speed" 4) "29.7" 5) "position" 6) "2" 7) "location_id" 8) "1" 2) 1) "1692632147973-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "29.9" 5) "position" 6) "1" 7) "location_id" 8) "2" # 返回为空,迭代完成 > XRANGE race:france (1692632147973-0 + COUNT 2 (empty array) ``` `XRANGE`操作查找的时间复杂度为`O(long(N))`,切返回M个元素的时间复杂度为`O(M)`。 命令`XREVRANGE`和`XRANGE`集合等价,但是会按照逆序来返回元素,故而,针对`XREVRANGE`传参时参数的顺序也需要颠倒 ```redis-cli > XREVRANGE race:france + - COUNT 1 1) 1) "1692632147973-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "29.9" 5) "position" 6) "1" 7) "location_id" 8) "2" ``` #### Listening for new items with XREAD 在某些场景下,我们可能希望对`new items arriving to the stream`进行订阅,如下两种场景都希望对`new items`进行订阅 - 在`Redis Pub/Sub`场景中,通过redis stream来对channel进行订阅 - 在Redis blocking lists场景中,从redis stream中等并并获取new element 但是,上述两种场景在如下方面有所不同 - 一个stream可以包含多个clients(consumers),对于new item,默认情况下`会从传递给每一个等待当前stream的consumer`。 - 在`Pub/Sub`场景和上述行为相符,其会将new item传递给每一个consumer,即`fan out` - 在blocking lists场景和上述行为并不相符,每个consumer都会获取到不同的element,相同的element只会被传递给一个consumer - 其对message的处理也有不同: - `Pub/Sub`场景下,将会对消息进行`fire and forget`操作,消息将永远不会被存储 - 在使用`blocking lists`时,如果消息被client接收,其将会从list中移除 - Stream Consumer Groups提供了`Pub/Sub`或`blocking lsits`都无法实现的控制:不同的groups可以针对相同stream进行订阅;并且对被处理items进行显式的ack;可以对`unprocessed messages`进行声明;只有`private past history of messages`对client可见 对于提供`Listening for new items arriving into stream`支持的命令,被称为`XREAD`。其使用示例如下: ```redis-cli > XREAD COUNT 2 STREAMS race:france 0 1) 1) "race:france" 2) 1) 1) "1692632086370-0" 2) 1) "rider" 2) "Castilla" 3) "speed" 4) "30.2" 5) "position" 6) "1" 7) "location_id" 8) "1" 2) 1) "1692632094485-0" 2) 1) "rider" 2) "Norem" 3) "speed" 4) "28.8" 5) "position" 6) "3" 7) "location_id" 8) "1" ``` ##### XREAD with STREAMS option 上述示例为`XREAD`命令的`non-blocking`形式,并且`COUNT option并非强制的`;唯一的强制option为`STREAMS`。 XREAD在和STREAMS一起使用时,并且需要指定`a list of keys`和`maximum ID already seen for each stream by the calling consumer`,故而,该命令只会向consumer返回`messages with and ID greater than the one we specified`。 在上述示例中,命令为`STREAMS race:france 0`,其代表`race:france流中所有ID比0-0大的消息`。故而,返回的结构中,顶层为stream的key name,`因为上述命令可以指定多个stream,针对多个stream进行监听`。 在指定`STREAMS` option时,必须后缀key names,故而STREAMS选项必须为最后一个option,任何其他option必须要放在STREAMS之前。 除了streams之外,还可以为`XREAD`指定`last ID we own`。 ##### XREAD with BLOCK argument 上述示例为`non-blocking`形式,除此之外还可以通过`BLOCK`参数将命令转化为`blocking`形式: ```redis-cli > XREAD BLOCK 0 STREAMS race:france $ ``` 在上述示例中,并没有指定`COUNT`,而是指定了`BLOCK`选项,并设置`timeout`为`0 milliseconds`(代表永远不会超时)。 并且,BLOCK版本的示例并没有传递正常的ID,而是传递了一个特殊的ID `$`,该符号代表`XREAD`应当使用`stream已经存储的最大ID`来作为last ID。故而,在指定last ID为`$`后,只会接收到`new messages`。其行为和`unix command`中的`tail -f`类似。 在指定了BLOCK选中后,其行为如下: - `if the command is able to serve our request immediately without blocking`,那么其会立马被执行 - 如果当前不满足获取entry的条件,那么client会发生阻塞 通常,在希望`从new entries`开始消费时,会从`ID $`开始,在获取到`ID $`对应的entry后,下一次消费从`last message recevied`开始。 XREAD的blocking形式也支持监听多个streams,也可以指定多个key names。如果至少存在一个stream中`存在元素的ID并命令指定的ID更大`,那么将会立马返回结果;否则,该命令将会阻塞,直到有一个stream获取到新的data。 和blocking list操作类似,`blocking stream reads`对`clients wating for data`而言是公平的,也支持FIFO。`the first client that blocked for a given stream will be the first to be unlocked when new items are available`。 #### Consumer groups 当从不同的clients消费相同stream时,可通过`XREAD`来进行读取,此时可将message `fan-out`给多个clients,在此种场景下,同一条消息会被多个clients进行处理。 在某些场景下,可能不希望上述行为,而是希望每个client都负责处理stream中的不同消息subset,特别是在消息处理耗时较长的场景下。假设拥有3个消费者`C1, C2, C3`和包含消息`1,2,3,4,5,6,7`的stream,则消费者对stream进行消费时,消费情形可能如下: ``` 1 -> C1 2 -> C2 3 -> C3 4 -> C1 5 -> C2 6 -> C3 7 -> C1 ``` 为了实现上述消费方式,redis引入了`consumer groups`的概念。`consumer group`从stream中获取数据,并且`serves multiple consumers`。consumer group提供了如下保证: - `每条消息只会被传递给一个消费者,不存在一条消息传递给多个消费者的情形` - 在consumer group内consumer根据name进行标识,name是大小写敏感的。即使在redis client断连后,stream consumer group也会保留对应的状态,当client重连后会被标识为之前的consumer - 每个consumer group都拥有`first ID never consumed`的概念,当consumer请求新消息是,其仅可向consumer提供尚未被传递过的消息 - 当消费消息时,需要通过特定命令来进行显式的ack。 - 在redis中,`ack`代表如下含义:该消息已经被正确的处理,并且该消息可在consumer group中被淘汰 - consumer group会追踪当前所有的`pending messages` - `pending messages`代表已经被传递给consumer但是尚未被ack的消息 - 由于consumer group对pending message的追踪,当访问stream的message hisory(记录message被传递给哪个consumer instance)时,每个consumer只能看到被传递给其自身的messages 在某种程度上,consumer group可以被看作是`amount of state abount a stream`: ``` +----------------------------------------+ | consumer_group_name: mygroup | | consumer_group_stream: somekey | | last_delivered_id: 1292309234234-92 | | | | consumers: | | "consumer-1" with pending messages | | 1292309234234-4 | | 1292309234232-8 | | "consumer-42" with pending messages | | ... (and so forth) | +----------------------------------------+ ``` consumer group能够为consumer instance提供`history of pending messages`,并且当consumer请求new messages时,只会向其传递`ID大于last_delivered_id的message`。 对于redis stream而言,其可以包含多个consumer groups。并且,对于同一redis stream,可以同时包含`consumer reading without consumer groups via XREAD`和`consumer reading via XREADGROUP in consumer group`两种类型的consumer。 consumer group包含如下的基本命令: - XGROUP - XREADGROUP - XACK - XACKDEL ##### Creating a consumer group 可以通过如下方式来为stream创建一个consumer group: ```redis-cli > XGROUP CREATE race:france france_riders $ OK ``` 在上述示例中,在通过command创建consumer group时,以`$`的形式指定了一个`ID`。该ID是必要的,该ID将作为consumer group创建时的`last message ID`,在第一个consumer连接时,会根据向consumer传递`大于last message ID的消息`。 - 当将last message ID指定为`$`时,只有`new messages arriving in the stream from now on`会被传递给consumer group中的consumer instances。由于`$`代表`current greatest ID in the stream`,指定`$`代表`consuming only new messages` - 如果在此处指定了`0`,那么consumer group将会消费`all the messages in the stream history` - 此处,也可以指定其他的`valid ID`,`consumer group will start delivering messages that are greater than the ID you specify` ##### Create the stream automatically `XGROUP CREATE`命令会期望target stream存在,并且当target stream不存在时会返回异常。如果target stream不存在,可以通过在末尾指定`MKSTREAM`选项来自动创建stream。 `XGROUP CREATE`同样支持自动创建stream,可通过`MKSTREAM`的subcommand作为最后一个参数: ```redis-cli XGROUP CREATE race:italy italy_riders $ MKSTREAM ``` 在调用该命令后,consumer group会被创建,之后可以使用`XREADGROUP`命令来通过consumer group读取消息。 ##### XREADGROUP XREADGROUP命令和XREAD命令类似,并同样提供`BLOCK`选项: - `GROUP`: 该option是一个mandatory option,在使用`XREADGROUP`时必须指定该选项。 - `GROUP`包含两个arguments: - the name of consumer group - the name of consumer that is attemping to read - `COUNT`: XREADGROUP同样支持该option,和XREAD中的`COUNT` option等价 在如下示例中,向`race:italy` stream中添加riders并且并且尝试通过consumer group进行读取: ```redis-cli > XADD race:italy * rider Castilla "1692632639151-0" > XADD race:italy * rider Royce "1692632647899-0" > XADD race:italy * rider Sam-Bodden "1692632662819-0" > XADD race:italy * rider Prickett "1692632670501-0" > XADD race:italy * rider Norem "1692632678249-0" > XREADGROUP GROUP italy_riders Alice COUNT 1 STREAMS race:italy > 1) 1) "race:italy" 2) 1) 1) "1692632639151-0" 2) 1) "rider" 2) "Castilla" ``` XGROUPREAD的返回结构和XREAD类似。 在上述命令中,`STREAMS` option之后指定了`>`作为special ID: - special ID `>`只在consumer group上下文中有效,其代表`messages never delivered to other consumers so far` 除了指定`>`作为id外,还支持将其指定为`0`或其他有效ID。在指定id为`>`外的其他id时,`XREADGROUP command will just provide us with history of pending messages, in that case we will never see new messages in the group`。 XREADGROUP命令基于指定id的不同,行为拥有如下区别: - 如果指定的id为`>`, 那么`XREADGROUP`命令将只会返回`new messages never delivered to other consumers so far`,并且,作为副作用,会更新consumer group的last ID - 如果id为其他有效的`numerical ID`,`XREADGROUP command will let us access our history of pending messages`,即`set of messages that are delivered to this specified consumer and never acknowledged so far` 可以指定ID为`0`来测试该行为,并且结果如下: - we'll just see the only pending message ```redis-cli > XREADGROUP GROUP italy_riders Alice STREAMS race:italy 0 1) 1) "race:italy" 2) 1) 1) "1692632639151-0" 2) 1) "rider" 2) "Castilla" ``` 但是,如果通过ack将该message标记为processed,那么其将不再作为`pending messages history`的一部分,故而,即使继续调用`XREADGROUP`也不再包含该message: ```redis-cli > XACK race:italy italy_riders 1692632639151-0 (integer) 1 > XREADGROUP GROUP italy_riders Alice STREAMS race:italy 0 1) 1) "race:italy" 2) (empty array) ``` > 在上述示例中,用于指定id为非`>`后,只会返回pending messages,在对pending messages进行ack后,那么被ack的消息在下次XREADGROUP调用时将不会被返回 `上述操作都通过名为Alice的consumer进行的操作,如下示例中将通过名为Bob的consumer进行操作`。 ```redis-cli > XREADGROUP GROUP italy_riders Bob COUNT 2 STREAMS race:italy > 1) 1) "race:italy" 2) 1) 1) "1692632647899-0" 2) 1) "rider" 2) "Royce" 2) 1) "1692632662819-0" 2) 1) "rider" 2) "Sam-Bodden" ``` 在上述示例中,`group相同但是consumer为Bob而不是Alice`。并且redis仅会返回new messages,前面示例中的`Castilla`消息由于已经被传递给Alice并不会被传递给Bob。 通过上述方式,Alice, Bob和group中的其他consumer可以从相同的stream中读取不同的消息。 XREADGROUP有如下方面需要关注: - consumer并不需要显式创建,`consumers are auto-created the first time they are mentioned` - 在使用XREADGROUP时,可以同时读取多个keys。但是,想要令该操作可行,`必须create a consumer group with the same name in every stream`。该方式通常不会被使用,但是在技术上是可行的 - `XREADGROUP`为一个`write command`,其只能在master redis instance上被调用。因为该命令调用存在side effect,该side effect会对consumer group造成修改 > 在使用XREADGROUP命令时, 只有当传递的id为`>`时,才会对consumer group的last ID造成修改。否则,只会读取pending messages history,并不会实际的修改last ID。故而,当consumer实例重启进行XREADGROUP pending messages时,并不会触发side effect对其他consumers的消费造成影响。 如下示例为一个Ruby实现的consumer, ```ruby require 'redis' if ARGV.length == 0 puts "Please specify a consumer name" exit 1 end ConsumerName = ARGV[0] GroupName = "mygroup" r = Redis.new def process_message(id,msg) puts "[#{ConsumerName}] #{id} = #{msg.inspect}" end $lastid = '0-0' puts "Consumer #{ConsumerName} starting..." check_backlog = true while true # Pick the ID based on the iteration: the first time we want to # read our pending messages, in case we crashed and are recovering. # Once we consumed our history, we can start getting new messages. if check_backlog myid = $lastid else myid = '>' end items = r.xreadgroup('GROUP',GroupName,ConsumerName,'BLOCK','2000','COUNT','10','STREAMS',:my_stream_key,myid) if items == nil puts "Timeout!" next end # If we receive an empty reply, it means we were consuming our history # and that the history is now empty. Let's start to consume new messages. check_backlog = false if items[0][1].length == 0 items[0][1].each{|i| id,fields = i # Process the message process_message(id,fields) # Acknowledge the message as processed r.xack(:my_stream_key,GroupName,id) $lastid = id } end ``` 在上述consumer实现中,`consumer启动时会对history进行消费`,即`list of pending messages`。(因为consumer可能在之前发生过崩溃,故而在重启时可能希望对`messages delivered to us without getting acked`进行重新读取)。在该场景下,`we might process a message multiple times or one time`。 并且,在consumer实现中,一旦history被消费完成,`XREADGROUP`命令将会返回empty list,此时便可以切换到`special ID >`,从而对new messages进行消费。 ##### Recovering from permanent failures 在上述ruby实现的consumers中,多个consumers将会加入相同的consumer group,每个consumer都会消费和处理`a subset of messages`。当consumer从failure中进行恢复时,会重新读取`pending messages that were delivered just to them`。 但是,在现实场景中,可能会出现`consumer may permanently fail and never recover`的场景。 redis consumer group对`permanently fail`的场景提供了专门的特性,支持对`pending messages of a given consumer`进行认领,故而`such pending messages will change ownership and will be re-assigned to a different consumer`。 在使用上述特性时,consumer必须检查`list of pending messages`,并`claim specific messages using special command`。否则,对于permanently fail的consumer,其pending messages将会被pending forever,即pending messages一直被分配给`old consumer that fail permanently`。 ###### XPENDING 通过`XPENDING`命令,可以查看pending entries in the consumer group。该命令是`read-only`的,调用该命令并不会导致任何消息的`ownership`被变更。 如下为XPENDING命令调用的示例: ```redis-cli > XPENDING race:italy italy_riders 1) (integer) 2 2) "1692632647899-0" 3) "1692632662819-0" 4) 1) 1) "Bob" 2) "2" ``` 当调用该command时,command的输出将会包含如下内容: - `total number of pending messages in the consumer group` - `lower and higher message ID among the pending messages` - `a list of consumers and the number of pending messages they have` 在上述示例中,仅名为`Bob`的consumer存在2条`pending messages`。 `XPENDING命令支持传递更多的参数来获取更多信息`,`full command signature`如下: ```redis-cli XPENDING [[IDLE ] []] ``` 如上述示例所示,可以为`XREADGROUP`命令指定如下内容: - `start-id`和`end-id`: 可以将其指定为`-`和`+` - `count`: 用于控制该command返回information的数量 - `consumer-name`: 该选项为optional的,指定该选项后,仅会输出`messages pending for a given consumer` 为`XPENDINGS`命令指定更多选项的示例如下所示 ```redis-cli > XPENDING race:italy italy_riders - + 10 1) 1) "1692632647899-0" 2) "Bob" 3) (integer) 74642 4) (integer) 1 2) 1) "1692632662819-0" 2) "Bob" 3) (integer) 74642 4) (integer) 1 ``` 在上述示例中,会输出`details for each message`,具体包含如下信息: - `ID`: message id - `consumer name`: 该pending message对应的consumer - `idle time in milliseconds`:代表该message被传递给consumer后,经过的毫秒数 - `the number of times that a given message was delivered`: 该消息已经被传递的次数 在上述示例中,返回的两条`pending messages`都是针对`Bob`的,并且两条消息都被传递了超过一分钟,都只被传递过一次。 ###### checking the message content 在获取到message detail后,可以根据message ID来查询message的内容,只需将message ID同时作为`XRANGE`命令的`start-id`和`end-id`即可。示例如下所示: ```redis-cli > XRANGE race:italy 1692632647899-0 1692632647899-0 1) 1) "1692632647899-0" 2) 1) "rider" 2) "Royce" ``` 通过上述介绍的命令,可以制定`permanently fail`场景下的处理策略: - 当Bob存在超过一分钟都没有处理的pending messages时,Bob可能无法快速被恢复,此时Alice可以针对Bob的pending messages进行`claim`,并且代替Bob对pending messages进行处理 ###### XCLAIM `XCLAIM`命令的形式如下: ```redis-cli XCLAIM ... ``` XCLAIM命令的作用如下: - 对于``和``所指定的stream和consumer group,希望将``所指定的message都能更改ownership,将messages分配给``所指定的consumer。 - 除此之外,我们还提供了``,仅当指定message的`idle time`比指定的`min-idle-time`更大时才有效 - 指定`min-idle-time`在多个clients尝试同时对message进行claim时会起作用,当第一个client对message进行claim后,idle-time会重置,故而当第二个client尝试对message进行claim时,会因不满足min-idle-time的条件而失败 作为claim操作的side effect,`claiming a message will reset its idle time and will increment its number of deliveries counter`。 XCLAIM的使用示例如下所示: ```redis-cli > XCLAIM race:italy italy_riders Alice 60000 1692632647899-0 1) 1) "1692632647899-0" 2) 1) "rider" 2) "Royce" ``` 该消息会`claimed by Alice`,此时consumer Alice可以对该message进行处理并ack,即使original consumer恢复失败,pending messages也能够被继续处理。 ###### JUSTID option 在上述示例中,在成功对message进行`XCLAIM`后,会返回该message本身。可以通过`JUSTID` option来修改返回的结构,在指定该option后,返回内容为`just IDs of the messages successfully claimed`。 通过`JUSTID` option,可以降低client和server之间使用的带宽。 `claiming并非一定要位于consumer自身的进程中,其也可以被实现在独立的进程中`: - `可以使用独立的进程来扫描pending messages list,并且将pending messages分配给明显处于活跃状态的consumers` ##### Automatic claiming 在redis 6.2版本中,添加了`XAUTOCLAIM`命令,实现了上一章节中描述的claiming process。`XPENDING`和`XCLAIM`命令为各种不同的recovery机制提供了基础。而`XAUTOCLAIM`命令优化了通用的`claiming process`,令`claiming process`由redis来进行管理,为大多的recovery提供简单的解决方案。 `XAUTOCLAIM`命令会识别idle messages并且转移message的ownership给指定consumer。`XAUTOCLAIM`命令的形式如下所示: ```redis-cli XAUTOCLAIM [COUNT count] [JUSTID] ``` 故而,可以通过如下方式来使用automatic claiming: ```redis-cli > XAUTOCLAIM race:italy italy_riders Alice 60000 0-0 COUNT 1 1) "0-0" 2) 1) 1) "1692632662819-0" 2) 1) "rider" 2) "Sam-Bodden" ``` 和`XCLAIM`类似,其返回结果为`an array of the claimed messsages`。但是,其还会返回一个stream ID(`在上述示例中为0-0`)。返回的stream ID可以用于对pending entries进行迭代。该stream ID为一个cursor,可以将其用作下次XAUTOCLAIM调用的`start`: ```redis-cli > XAUTOCLAIM race:italy italy_riders Lora 60000 (1692632662819-0 COUNT 1 1) "1692632662819-0" 2) 1) 1) "1692632647899-0" 2) 1) "rider" 2) "Royce" ``` 当`XAUTOCLAIM`返回“0-0”作为cursor时,代表其到达了`end of the consumer group pending entries list`。其并不代表没有新的idle pending messages,可以重新从begining of the stream来调用`XAUTOCLAIM`。 ##### Claiming and the delivery counter 通过`XPENDING`命令输出的counter代表该message的被传递次数,该counter在两种场景下会增加: - when a message is successfully claimed via `XCLAIM` - when a `XREADGROUP` call is used in order to access the history of pending messages 当存在failure时,message可能会被传递多次,但是message最终会被处理并ack。但是,在处理特定的消息时,可能会在处理逻辑中抛出异常,在该类场景下consumer会持续的在处理该消息时抛出异常。故而,可以通过delivery counter来探知那些不可处理的message。`一旦delivery counter到达给定的值时,可以将该消息发送给另一个stream,并且向系统的管理员发送notification。`这就是redis stream实现`dead letter`的基础。 ##### working with multiple consumer groups redis stream可以关联多个consumer groups,每个entries都会被传递给每个consumer group。在consumer group内,每个consumer instance处理一部分entries。 当consumer对message进行处理时,其会使用`XACK`命令来对message进行确认,`并且从consumer group的Pending Entries List(PEL)中移除该entry reference`。但是,`被ack的message仍然保存在stream中`,且consumer group A中对message的ack并不会影响consumer group B的PEL,group A在对message进行ack后,message仍然位于group B的PEL中,直到group B中的consumer对message进行ack,此时message才从group B的PEL中被移除。 通常来说,如果想要从stream中删除entries,必须要等到所有的consumer groups都对entries进行了ack,应用需要实现复杂的逻辑。 ###### Enhanced deletion control in Redis 8.2 从redis 8.2开始,一些命令为`entries在多个consumer groups间的处理`提供了增强控制: - `XADD`支持KEEPREF, DELREF, ACKED模式 - `XTRIM`同样支持KEEPREF, DELREF, ACKED选项 如下选项控制consumer group references是如何被处理的: - `KEEPREF`(默认): Preserves existing references to entries in all consumer groups' PELs - `DELREF`: Removes all references to entries from consumer groups' PELs, effectively cleaning up all traces of the messages - `ACKED`: Only processes entries that have been acked by all consumer groups `ACKED` mode对于`coordinating deletion across multiple consumer groups`的复杂逻辑十分有用,确认entires在所有consumer groups在完成对其的处理后才移除。 ##### Stream Observability 缺乏可观测性的消息系统将十分难以使用,一个透明的消息系统需要令如下信息可观测: - who is consuming messages - what messages are pending - the set of consumer groups active in a given stream 在前面章节中,已经介绍了`XPENDINGS`命令,通过其可以观测处于`处理中`状态的消息,并且能够获取消息的idle time和number of deliveries。 `XINFO`命令和sub-commands一起使用,可以用于获取stream和consumer group相关的信息。 `XINFO`命令的使用示例如下: ```redis-cli > XINFO STREAM race:italy 1) "length" 2) (integer) 5 3) "radix-tree-keys" 4) (integer) 1 5) "radix-tree-nodes" 6) (integer) 2 7) "last-generated-id" 8) "1692632678249-0" 9) "groups" 10) (integer) 1 11) "first-entry" 12) 1) "1692632639151-0" 2) 1) "rider" 2) "Castilla" 13) "last-entry" 14) 1) "1692632678249-0" 2) 1) "rider" 2) "Norem" ``` 上述示例中,通过`XINFO`命令获取了stream本身的信息,输出展示了stream内部的编码方式,并且记录了stream中的第一条消息和最后一条消息。 如果想要获取和stream相关的consumer group消息,参照如下示例: ```redis-cli > XINFO GROUPS race:italy 1) 1) "name" 2) "italy_riders" 3) "consumers" 4) (integer) 3 5) "pending" 6) (integer) 2 7) "last-delivered-id" 8) "1692632662819-0" ``` 如果想要查看consumer group中注册的consumer实例,可以通过`XINFO CONSUMERS`命令来进行查看: ```redis-cli > XINFO CONSUMERS race:italy italy_riders 1) 1) "name" 2) "Alice" 3) "pending" 4) (integer) 1 5) "idle" 6) (integer) 177546 2) 1) "name" 2) "Bob" 3) "pending" 4) (integer) 0 5) "idle" 6) (integer) 424686 3) 1) "name" 2) "Lora" 3) "pending" 4) (integer) 1 5) "idle" 6) (integer) 72241 ``` ##### Difference with kafka partitions Redis stream中的consumer group可能在某些方面类似于kafka中基于分区的consumer groups,但是仍然存在较大差别。 在redis strema中,`partition`这一概念是逻辑的,实际上stream所有的messages都存在相同的key中。故而,redis stream中consumer instance并不从实际的partition中读取信息,也不涉及partition在consumer间的分配。 > 例如,如果consumer C3在某个时刻fail permanently,redis在所有新消息到达时都会传递给C1和C2,将好像redis stream只存在2个逻辑分区。 类似的,如果某个consumer处理消息比其他consumers都快,那么该consumer将在单位时间内按比例收到更多的消息。Redis会追踪所有尚未被ack的消息,并且记录哪条消息被哪个consumer接收,`the ID of the first message never delivered to any consumer`也会被redis记录。 在redis Stream中: - 如果redis stream的数量和consumer的数量都为1,那么消息将是按照顺序被处理的 - 如果stream的数量为1,consumer的数量为n,那么可以将负载均衡给n个consumers,但是,在这种情况下消息的消费可能是无序的 - 当使用n个stream和n个consumers时,一个consumer只用于处理所有streams中的一部分,可以将`1 stream->1 consumer`拓展到`n stream->n consuimer` #### Capped Streams 在许多应用中,并不想在stream中永久存储data。有时,需要限制stream中entries的最大数量。 ##### XADD with MAXLEN redis stream对上述特性提供了支持,在使用`XADD`命令时,支持指定`MAXLEN`选项,示例如下所示: ```redis-cli > XADD race:italy MAXLEN 2 * rider Jones "1692633189161-0" > XADD race:italy MAXLEN 2 * rider Wood "1692633198206-0" > XADD race:italy MAXLEN 2 * rider Henshaw "1692633208557-0" > XLEN race:italy (integer) 2 > XRANGE race:italy - + 1) 1) "1692633198206-0" 2) 1) "rider" 2) "Wood" 2) 1) "1692633208557-0" 2) 1) "rider" 2) "Henshaw" ``` 当使用`MAXLEN`选项时,如果达到了指定长度,那么old entries将自动被淘汰,从而确保stream处于恒定的大小。 `trimming with MAXLEN`的开销在部分场景下可能会变得很大:为了内存效率,stream由radix-tree结构表示。radix-tree由macro nodes组成,单个macro node中包含多个elements,对单个macro node的修改并不高效。 故而,支持按照如下形式来使用`XADD`命令,并支持`MAXLEN`选项: ```redis-cli XADD race:italy MAXLEN ~ 1000 * ... entry fields here ... ``` 在上述示例中,在`MAXLEN`和`count`之间指定了`~`,代表`并不需要将长度上限严格限制为1000`。该长度可以是`1000`,可以是`1010`,只保证该长度比1000大。在指定了`~`后,只有当允许移除整个节点时,trimming操作才会被实际执行。指定`~`能够让`MAXLEN`操作更加高效。 ##### XTRIM 同样的,redis还支持`XTRIM`命令,其执行和`MAXLEN`类似: ```redis-cli > XTRIM race:italy MAXLEN 10 (integer) 0 > XTRIM mystream MAXLEN ~ 10 (integer) 0 ``` 除此之外,`XTRIM`命令还支持不同的trimming strategies: - `MINID`:该trimming strategy支持对`entries with IDs lower than the on specified`进行淘汰 ##### Trimming with consumer group Awareness 从redis 8.2开始,`XADD with trimming options`和`XTRIM`命令都支持`enhanced control over how trimming interacts with consumer groups`,其支持`KEEPREF, DELREF, ACKED`三个选项: ```redis-cli XADD mystream KEEPREF MAXLEN 1000 * field value XTRIM mystream ACKED MAXLEN 1000 ``` - `KEEPREF`(default): Trim entries according to the strategy but preserves references references in consumer groups' PELs - `DELREF`: Trims entries and removes all references from consumer groups' PELs - `ACKED`: Only Trims entries that have been acknowledged by all consumer groups `ACKED`模式在多个consumer groups之间维护数据完整性十分有用,其能够保证entries只有`当被所有的consumer groups都处理完成之后`才会被移除 #### Special IDs in streams API 在redis API中,存在部分`Special IDs`: - `-, +`: 这两个特殊ID分别代表`the smallest ID possible`和`the greatest ID possible` - `$`: 该ID代表`stream中已经存在的最大ID`。在使用`XREADGROUP`命令时,如果只希望读取new entries,可以使用该`special ID`。同样的,可以将consumer group的`last delivered ID`设置为`$`,从而`just deliver new entries to consumers in the group`。 - `>`: 该ID代表`last delivered ID of a consumer group`,该ID的适用范围仅位于同一`consumer group`内,并且该ID仅在`XREADGROUP`命令中使用,代表`we want only entries that were never delivered to other consumers so far`。 - `*`: 该ID仅在`XADD`命令中被使用,代表为new entry自动选中ID #### Persistence, replication and message safety stream和redis中的其他数据结构一样,`is asynchronously replicated to replicas`,并且持久化到`RDB`和`AOF`文件中。并且,`consumer group的full state也会被传播到AOF, RDB, replcias中`。 故而,如果`message is pending in the master, also the replica will have the same information`。并且,当重启后,AOF也会恢复consumer group的状态。 redis streams和consumer groups将会被持久化,并且`replicated using the Redis default replication`: - 如果消息的持久化十分重要,那么`AOF必须使用strong fsync policy` - `redis asynchronously replication`并不能保证`xadd`/`consumer group state changes`能被同步到replica: - 当发生failover(故障转移,指哨兵或集群模式下主节点发生故障,replica节点被升级为主节点),可能主节点的变化尚未被同步到replica,此时failover将会产生data missing - `WAIT命令可以强制使changes被传播给replicas`,该命令仅会降低数据丢失的可能,但是并无法完全避免数据的丢失。 - 在发生故障转移时,redis仅会执行`best effort check`,从而转移到`replica which is the most updated`,在某些失败场景下,转移到的replica仍然可能缺失部分数据 #### removing single items from a stream stream同样支持`removing items from the middle of a stream`,尽管stream为append-only data structure, 但是该功能仍然在许多场景下十分有用。 ##### XDEL `XDEL`命令的使用示例如下所示: ```redis-cli > XRANGE race:italy - + COUNT 2 1) 1) "1692633198206-0" 2) 1) "rider" 2) "Wood" 2) 1) "1692633208557-0" 2) 1) "rider" 2) "Henshaw" > XDEL race:italy 1692633208557-0 (integer) 1 > XRANGE race:italy - + COUNT 2 1) 1) "1692633198206-0" 2) 1) "rider" 2) "Wood" ``` ##### Enhanced deletion with XDELEX 从redis 8.2开始,`XDELEX`命令对entry删除提供了增强的控制,尤其是针对consumer groups。和其他enhanced commands一样,其支持`KEEPREF, DELREF, ACKED`三种模式: ```redis-cli XDELEX mystream ACKED IDS 2 1692633198206-0 1692633208557-0 ``` - 在使用`ACKED`模式时,仅在指定entries已经被所有consumer groups都acknowledged之后,才能够对entries进行删除 - 在使用`DELREF`模式时,会移除指定entries,并清除所有consumer groups' PEL中的指定entries - 在使用`KEEPREF`模式时,会移除指定entries,并对consumer groups' PEL中的entries引用进行保留 > 对于通过KEEPREF模式移除entries的场景,如果entries从stream中被移除,但是PEL中仍然存在entries references,`此时XCLAIM操作并无法对该类entries进行认领` #### zero length streams redis stream数据类型和redis的其他数据结构存在差别, - 当其他数据结构中不再包含任何元素时,作为`calling commands that remove elements`的副作用,该key本身也会被自动移除 - 例如,当通过`ZREM`命令移除zset中的最后一个元素时,该zset本身也会被彻底移除 - 而redis stream类型的数据则是允许`stay at zero elements`,即使通过`MAXLEN` option或是`XDEL`调用令stream类型数据的entries数量为0,该stream也不会被自动移除 目前,即使当stream没有对应的consumer groups,当某command导致stream中的entries数量为0时,stream也不会被删除 #### Total latency of consuming a message 对于非阻塞命令(例如`XRANGE, XREAD, XREADGROUP without BLOCK option`)redis是同步对其进行处理的,其处理同其他的一般redis命令相同,对于这类commands并不需要讨论其latency。 但是,对于`delay of processing a message`,其拥有如下定义: - `in the context of blocking consumers in a consumer group, from the moment the message is produced via XADD to the moment the message is obtained by the consumer because XREADGROUP returned with the message` #### The model redis uses in order to route stream messages redis通过如下方式来管理`blocking operation waiting for data`: - 对于blocked client,其会在hash table中被引用。hash table中的key其对应的blocking consumers至少存在一个,而hash table中的value则是`a list of consumers that are waiting for such key`。 - 通过上述管理方式,如果一个key接收到消息,则可以解析所有等待该data的clients - 当发生写操作,例如`XADD`时,其会调用`signalKeyAsReady`方法。该方法会将key添加到`a list of keys that need to be processed`,该list中的key对于blocked consumers存在新的数据,被称为`ready keys` - `ready keys`后续会被处理,故而在相同的event loop cycle中,该key可能还会接收到其他的写操作(XADD) - 在返回event loop之前,`ready keys`最终被处理,对于每一个key,`the list of clients waiting for data`都会被扫描,如果适用,那么client将会接收到new data。 如上述描述所示,在返回到event loop之前,对`调用XADD的client`和`clients blocked to consume messages`其reply都会出现在ouput buffers中。故而,`caller of XADD`收到reply的时间将会和`blocked clients`接收到新消息的时间几乎相同 上述model是push-based的,将消息添加到consumer buffers的操作将直接由`XADD`调用来执行。 ### Redis Geospatial redis geospatial支持对坐标进行存储和查询,该数据结构主要用于`find nearby points within a given radius or bounding box`。 #### Bike Rental stations Example 假设当前需要支持`基于当前位置查找最近的bike rental station`功能,如下是redis geospatial的示例。 ```redis-cli > GEOADD bikes:rentable -122.27652 37.805186 station:1 (integer) 1 > GEOADD bikes:rentable -122.2674626 37.8062344 station:2 (integer) 1 > GEOADD bikes:rentable -122.2469854 37.8104049 station:3 (integer) 1 ``` 在上述示例中,向geospatial index中添加了多个自行车租赁点的location。 ```redis-cli > GEOSEARCH bikes:rentable FROMLONLAT -122.2612767 37.7936847 BYRADIUS 5 km WITHDIST 1) 1) "station:1" 2) "1.8523" 2) 1) "station:2" 2) "1.4979" 3) 1) "station:3" 2) "2.2441" ``` 而在上述命令中,则是`find all locations within a 5 kilometer radius of a given location`,并对每个location都返回了对应的距离 ### Redis bitmaps 在redis中,bitmaps并不是实际的数据结构,而是针对String类型的一系列`bit operations`,bitmaps的对外表现为和`bit vector`类似。 对于bitmaps其最大大小为`512MB`,其最多可以包含`2^32`个不同的bits。 redis支持对一个或多个strings执行位运算 #### Example 假设存在1000个骑行运动员正在乡间竞速,并且他们的自行车上装有编号为`0~999`的传感器,并需要检查在当前小时传感器是否ping过服务器,以此来确认运动员状态。 该场景可以适用bitmap,bitmap的key代表current hour: - Rider 123在`2024-01-01 00:00:00`时刻ping过server,此时可以通过`setbit`命令将123代表的bit置位 - 可以通过`GETBIT`命令来检查是否rdier 456在一小时内ping过server ```redis-cli > SETBIT pings:2024-01-01-00:00 123 1 (integer) 0 > GETBIT pings:2024-01-01-00:00 123 1 > GETBIT pings:2024-01-01-00:00 456 0 ``` #### Bit Operations Bit Operations分为两种类型: - `single bit operations`: 例如将某个bit改为0或1,或获取某个bit的值 - `operations on groups of bits`: 例如counting the number of set bits in a given range of bits bitmap的一个巨大优势是其能够在存储信息的同时极大的节省存储空间。对于存储single bit information,其能够在`512MB`的空间限制下存储`4 billion`位的值。 ##### single bit operations - `SETBIT`: SETBIT命令的第一个参数为bit number,而第二个参数为`0或1`。如果`addressed bit is outside the current string length`,那么该SETBIT操作将自动拓展string的长度 - `GETBIT`: GETBIT命令会返回指定位置bit的值。`out of range bits`(addressing a bit that is outside then length of the string stored into the target key)其值将会被视为`0` ##### operations on group of bits redis支持对`group of bits`进行如下操作: - `BITOP`: `performs bit-wise operations between different strings`。支持的operators有`AND, OR, XOR, NOT, DIFF, DIFF1, ANDOR, ONE` - `BITCOUNT`: 该命令用于统计置为1的bit个数 - `BITPOS`: 该命令用于查找`the first bit having specified value of 0 or 1` bitcount和bitpos都支持对bitmap的指定范围来进行操作,`该范围可以通过bit或byte来指定,从0开始`,默认情况下,范围指是基于`BYTE`的,如果要基于`BIT`指定范围,需要手动指定`BIT` option。 bitcount的使用示例如下: ```redis-cli > BITCOUNT pings:2024-01-01-00:00 (integer) 1 ``` ##### longest streak of daily visits 可以通过bitmap来记录用户的最长连续访问时间。可以用0来表示`the day you made your website public`,并且`set the bit every time the user visits the web site`。`bit index`则是当前时间基于`day 0`已经经过的天数。 这样,针对每个user,都通过一个small string来存储了用户的访问记录。并且,可以简单的计算用户的最长连续访问天数。 bitmaps可以被轻松的拆分为多个key,通常来讲,也应当避免操作过大的key。将大的bitmap拆分为多个key时,通用策略是`限制每个key`存储`M`个bits,并且通过`{bit-index}/M`来决定当前bit位于哪个key,而`{bit-index} MOD M`则用于当前bit位于key的哪个位置。 ### Probabilistic `Probabilistic data structure`向使用者提供了统计数据的`近似值`,例如`计数、频率、排名`等,而并非精确值。使用Probabilistic data structure可以提高计算效率。 #### HyperLogLog `HyperLogLog`数据结构用于估计set中的基数,HyperLogLog并不能保证结果的精确性,但是能够提高空间的使用效率。 > Redis的HyperLogLog实现最多占用12KB空间,并提供了`0.81%`的标准误差。 通常,统计items中的唯一项个数需要花费的空间和items的个数成正比,但是,有一系列算法可以`牺牲精确性来换取内存使用大小`: - 其能够返回唯一项个数的大致估计值,并且估计值存在标准误差,在redis HyperLogLog的实现中,标准误差小于1% - 在使用该算法时,并不需要使用和items个数成正比的内存空间,而是使用常量大小的内存(在最坏情况下,使用空间大小为12KB,当HyperLogLog中包含元素较少时,其使用的空间也远小于12KB) 在redis中,HyperLogLog是编码成Redis strings的。故而,对于`HyperLogLog`类型,可以通过`GET`来进行序列化,并通过`SET`来进行反序列化。 在使用HyperLogLog时,其API如下: - `PFADD`: 可以通过该命令将new item添加到HyperLogLog - `PFCOUNT`: 当想要获取唯一项个数的近似值时,可以调用`PFCOUNT`命令 - `PFMERGE`: 如果想要对两个不同的HyperlogLog进行合并,可以使用`PFMERGE`命令 HyperLogLog的使用示例如下所示: ```redis-cli > PFADD bikes Hyperion Deimos Phoebe Quaoar (integer) 1 > PFCOUNT bikes (integer) 4 > PFADD commuter_bikes Salacia Mimas Quaoar (integer) 1 > PFMERGE all_bikes bikes commuter_bikes OK > PFCOUNT all_bikes (integer) 6 ``` HyperLogLog数据结构通常有如下用例场景:统计页面或网站的每天用户访问量 #### Bloom Filter Bloom Filter也是一种`Probabilistic data structure`,用于检测在set中item是否存在,其使用少量的的固定大小存储空间。 Bloom Filter并不会将items都存储在set中,相对的,其仅存储item在hash后的结果,故而在部分程度上会牺牲精确性。`通过对精确性的牺牲,Bloom Filter拥有十分高的内存效率,并且其执行效率也很高`。 `Bloom Filter仅能够保证某元素在set中不存在,但是关于元素的存在其仅能给出一个估计值`: - 当Bloom Filter的返回结果表示某item不存在于set中时,`返回结果是精确的,该item一定在set中不存在` - 但是,如果若Bloom Filter返回结果表示某item存在时,`每N个存在的返回结果就有一个是错误的`。 Bloom Filter通常用于`negative answer will prevent more costly operations`的场景,例如`用户名称是否被占用,信用卡是否被丢失,用户是否看过广告等` ##### Example 假设自行车生产商已经生产了百万种不同型号的自行车,目前需要`在为新model指定名称时避免指定旧model已经使用过的名称`。 在该种用例场景下,可以使用bloom filter来检测重复。在如下实例中,创建的bloom filter可容纳100w entries,并且错误率仅为0.1%。 ```redis-cli > BF.RESERVE bikes:models 0.001 1000000 OK > BF.ADD bikes:models "Smoky Mountain Striker" (integer) 1 > BF.EXISTS bikes:models "Smoky Mountain Striker" (integer) 1 > BF.MADD bikes:models "Rocky Mountain Racer" "Cloudy City Cruiser" "Windy City Wippet" 1) (integer) 1 2) (integer) 1 3) (integer) 1 > BF.MEXISTS bikes:models "Rocky Mountain Racer" "Cloudy City Cruiser" "Windy City Wippet" 1) (integer) 1 2) (integer) 1 3) (integer) 1 ``` > 在上述示例中,即使bloom filter中仅存在少量元素,返回的`存在`结果也存在误报的可能,即元素其实根本不存在。 ##### Reserving Bloom filters 在使用bloom filters时,大部分sizing工作都会自动完成: ```redis-cli BF.RESERVE {key} {error_rate} {capacity} [EXPANSION expansion] [NONSCALING] ``` - `error_rate`: 该参数代表`false positive rate`,该rate为0和1之间的decimal,例如,当希望的false positive rate为`0.1%`(1 in 1000)时,需要将error_rate设置为0.001 - `expected capacity(capacity)`: 该参数代表`bloom filter中期望包含的元素数量`。需要确保该值的准确性: - 如果该值设置过大,其会浪费内存空间 - 如果该值设置过小,那么filter被填充满后,`a new one will have to be stacked on top of it (sub-filter stacking)` - `when a filter consists of multiple sub-filters stacked on top of each other`,`其新增操作的延迟仍然会保持不变;但是存在性检查的延迟会增加` - 存在性检查的原理如下:首先,会对top filter检查元素的存在性,如果返回为false,那么会继续检查以一个sub-filter,`该机制会导致存在性检查的延迟增加` - `scaling(EXPANSION)`: 在向bloom filter中添加数据时,并不会因为数据结构的填充而失败。在向filter中添加元素时,error rate也会随之增加。为了保证错误率和bloom filter创建时指定的error rate相近,bloom filter需要自动扩容:`当capacity限制达到时,需要自动创建额外的sub-filter`。 - `new sub filter`的size大小等于`last-sub-filter-size * EXPANSION`。 - 如果filter中存储的items数量未知,可以将`EXPANSION`设置为`2 or more`,从而减少sub-filters的数量。 - 否则,可以将`EXPANSION`设置为`1`,从而减少内存的消耗 - 默认的EXPANSION为2 > 在向filter中添加new sub-filter时,相比于前一个filter,会为new sub-filter分配更多的hash function - `NONSCALING`: 如果想要禁用scale,可以指定`NONSCALING`。如果达到了initially assigned capacity,error rate将会开始增加。 ##### total size of bloom filter bloom filter实际使用的内存大小是根据`a function of choosen error rate`来决定的: - hash functions的最佳数量为`ceil(-ln(error_rate) / ln(2))` - 即要求的error_rate越小,hash function的数量应该越多 - 在给定期望error_rate和最有hash functions数量的前提下,对每个items需要的bits个数为` -ln(error_rate) / ln(2)^2` - 故而,bloom filter需要的bits数量为`capacity * -ln(error_rate) / ln(2)^2` - 在`1%` error rate的前提下,需要`7`个hash functions,每个item需要`9.585`bits - 在`0.1%` error rate的前提下,需要`10`个hash functions,每个item需要`14.378`bits - 在`0.01%` error rate的前提下,需要`14`个hash fucntions,每个item需要`19.170`bits 而相比于bloom filter,使用`redis set`来membership testing时,需要耗费的内存大小为 ``` memory_with_sets = capacity*(192b + value) ``` 对于ip地址,每个item大概需要在`40 bytes`(320bits),而在使用error rate为`0.01%`的bloom filter时,每个item仅需`19.170`bits ##### Performance 向bloom filter执行插入操作的时间复杂度为`O(K)`,其中`K`为hash functions的数量。 对bloom filter执行存在性检查的时间复杂度为`O(K)`或`O(K*n)`(针对stacked filters场景),`n`为stacked filters的数量 #### Cuckoo filter Cuckoo filter和Bloom filter类似,同样用于检测item在set中是否存在,其也提供了`a very fast and space efficient way`。除此之外,Cuckoo filter允许对item进行删除,且在部分场景下相比Bloom filter而言Cuckoo filter的性能要更好。 Bloom filter和Cuckoo filter的实现逻辑如下: - Bloom filter是一个bit array,在`hash function决定的位置bit将会被置为1`。 - 而Cuckoo filter则是一个bucket array,`storing the fingerprints of the values in one of the buckets at positions decided by the two hash function`。 - 通过两个hash function,能够计算出两个可能的bucket position,而item的fingerprint则存储在两个bucket的其中一个 - 对于item x的membership query会针对x的fingerprint查找possible bucket,并且在查询到对应的fingerprint时返回true - 对于Cuckoo filter,其`fingerprint size`会决定其false positive rate ##### User Cases 在应用中,Cuckoo filter拥有如下使用示例: - `Targeted ad campaigns`: 在该场景下,Cuckoo filter主要用于处理`用户是否参与了指定活动`。为每个活动都使用一个Cuckoo filter,并且向Cuckoo filter中添加目标用户的id。每次用户访问时,都进行如下校验: - 如果用户id包含在cuckoo filter中,则代表用户没有参与过活动,向用户展示广告 - 如果用户点击广告并进行参与,从cuckoo filter中移除user id - 如果用户id不包含在cuckoo filter中,那么代表该用户已经参加过该活动,尝试下一个ad/Cuckoo filter - `Discount code`: 该场景下,Cuckoo filter主要用于处理`折扣码/优惠券是否已经被使用`。可以向Cuckoo Filter中注入所有的折扣码,在每次尝试使用折扣码时,都通过Cuckoo Filter校验: - 如果cuckoo filter表示该折扣码不存在,则校验失败,折扣码已经被使用 - 如果cuckoo filter表示该折扣码存在,则继续通过maindatabase来进行校验(`适配false positive的场景`),如果maindatabase校验通过,则将该折扣码从cuckoo filter中也移除 ##### Example ```redis-cli > CF.RESERVE bikes:models 1000 OK > CF.ADD bikes:models "Smoky Mountain Striker" (integer) 1 > CF.EXISTS bikes:models "Smoky Mountain Striker" (integer) 1 > CF.EXISTS bikes:models "Terrible Bike Name" (integer) 0 > CF.DEL bikes:models "Smoky Mountain Striker" (integer) 1 ``` 上述是Cuckoo Filter的使用示例,其通过`CF.RESERVE`创建了初始容量为1000的cuckoo filter。 > 当key不存在时,直接调用`CF.ADD`命令,也能自动创建一个新的Cuckoo filter,但是通过`CF.RESERVE`命令创建能够按需指定容量。 ##### Cuckoo vs Bloom Filter 在插入items时,Bloom Filter的性能和可拓展性通常要更好。`但是,Cuckoo filter的check operation执行更快,并且允许删除操作`。 ##### Sizing Cuckoo filters 在Cuckoo filters中,一个bucket可以包含多个entries,每个entry都可以存储一个fingerprint。如果cuckoo filter中所有的entries都存储了fingerprint,那么将没有empty slot来存储新的元素,此时,cuckoo filter将被看作`full`。 在使用cuckoo filter时,应该保留一定比例的空闲空间。 当在创建一个新cuckoo filter时,需要指定其capacity和bucket size: ```redis-cli CF.RESERVE {key} {capacity} [BUCKETSIZE bucketSize] [MAXITERATIONS maxIterations] [EXPANSION expansion] ``` - `capacity`: - capacity可以通过如下公式来计算`n * f / a` - `n`代表`numbe of items` - `f`代表`fingerprint length in bits`,如下为8 - `a`代表`fill rate or load factor (0<=a<=1) - 基于Cuckoo filter的工作机制,filter在capacity到达上限之前就会声明自身为full,故而fill rate永远不会到达100% - `bucksize`: - bucksize代表每个buckets中可以存储的元素个数,bucket size越大,fill rate越高,但是error rate也会越高,并且会略微影响性能 - `error rate`的计算公式为`error_rate = (buckets * hash_functions)/2^fingerprint_size = (buckets*2)/256` - 当bucket size为1时,fill rate为55%,false positive rate大概为`2/256`,即约等于`0.78%`,这也是可以实现的最小false positive rate - 当`buckets`变大时,error rate也会线性的增加,filter的fill rate也会增加。当bucket size为3时,false positive rate约为`2.34%`,并且fill rate约为80%。当bucket size为4时,false positive rate约为`3.12%`,fill rate约为95% - `EXPANSION`: - `when filter self-declare itself full`,其会自动拓展,生成额外的sub-filter,该操作会降低性能并增加error rate。新创建`sub-filter`的容量为`prev_sub_filter_size * EXPANSION` - 该默认值为1 - `MAXITERATIONS`: - `MAXITERATIONS`代表`the number of attempts to find a slot for incoming fingerprint`。一旦filter为full后,如果`MAXITERATIONS`越大,插入越慢。 - 该默认值为20 #### t-digest t-digest是一种`probabilistic data structure`,其允许在不对`set中所有数据`进行实际存储与排序的情况下获取数据的`percentile point`。故而,其可以针对如下场景:`What's the average latency for 99% of my database operations` - 在不使用`t-digest`时,如果要获取上述指标,需要对每位用户都存储平均延迟,并且对平均延迟进行排序,排除最后的百分之一数据,并计算剩余数据的平均值。该操作过程十分耗时 - 而通过t-digest可以解决该方面的问题 t-digest可以用于其他百分位相关的问题,例如`trimmed means`: - `A trimmed mean is the mean value from the sketch, excluding observation values outside the low and high cutoff percentiles.`例如,`0.1 trimmed means`代表排除最低的10%和最高的10%之后计算出的平均值 ##### Use Cases - `Hardware/Software monitoring`: - 当测量online server response latency,可以需要观测如下指标 - What are the 50th, 90th, and 99th percentiles of the measured latencies - Which fraction of the measured latencies are less than 25 milliseconds - What is the mean latency, ignoring outliers? or What is the mean latency between the 10th and the 90th percentile? - `Online Gaming`: - 当online gaming platform涉及数百万用户时,可能需要观测如下指标: - Your score is better than x percent of the game sessions played. - There were about y game sessions where people scored larger than you. - To have a better score than 90% of the games played, your score should be z - `Network traffic`: - 在对网络传输中的ip packets进行监测时,如需探测ddos攻击,可能需要观测如下指标: - 过去1s的packets数量是否超过了过去所有packets数量的99% - 在正常网络环境下,期望看到多少packets? ##### Examples 在如下示例中,将会创建一个`compression为100`的t-digest并且向其中添加item。在t-digest数据结构中,`compression`参数用于在内存消耗和精确度之间做权衡。`compression的默认值为100`,当compression值指定的更大时,t-digest的精确度会更高。 ```redis-cli > TDIGEST.CREATE bikes:sales COMPRESSION 100 OK > TDIGEST.ADD bikes:sales 21 OK > TDIGEST.ADD bikes:sales 150 95 75 34 OK ``` ##### Estimating fractions or ranks by values t-digest中一个有用的特性为`CDF`(definition of rank),其能给出小于或等于给定值的fraction。该特性能够解决`What's the percentage of observations with a value lower or equal to X`。 > 更精确的说,`TDIGEST.CDF will return the estimated fraction of observations in the sketch that are smaller than X plus half the number of observations that are equal to X. ` > 也可以使用`TDIGEST.RANK`命令,相比于返回fraction,其会返回value的rank。 ```redis-cli > TDIGEST.CREATE racer_ages OK > TDIGEST.ADD racer_ages 45.88 44.2 58.03 19.76 39.84 69.28 50.97 25.41 19.27 85.71 42.63 OK > TDIGEST.CDF racer_ages 50 1) "0.63636363636363635" > TDIGEST.RANK racer_ages 50 1) (integer) 7 > TDIGEST.RANK racer_ages 50 40 1) (integer) 7 2) (integer) 4 ``` 同样的,TDIGEST也支持`TDIGEST.REVRANK`命令,其返回的结果是`the number of (observations larger than a given value + half the observations equal to the given value)`。 ##### Estimating values by fractions or ranks `TDIGEST.QUANTILE key fraction ...`命令可以根据fraction来获取`an estimation of the value (floating point) that is smaller than the given fraction of observations.`。 `TDIGEST.BYRANK key rank ...`命令可以根据rank来获取`an estimation of the value (floating point) with that rank`。 使用示例如下所示: ```redis-cli > TDIGEST.QUANTILE racer_ages .5 1) "44.200000000000003" > TDIGEST.BYRANK racer_ages 4 1) "42.630000000000003" ``` `TDIGEST.BYREVRANK`命令可以根据`reverse rank`来获取value。 ##### trimmed mean 如果要计算`TDIGEST`结构的`trimmed mean`,可以使用`TDIGEST.TRIMMED_MEAN {key} low_fraction high_fraction`来获取。 ##### TDIGEST.MERGE 可以通过`TDIGEST.MERGE`命令来对sketches进行merge操作。 假设为3台servers测量了latency,此时需要合并多台servers的测量结果,并且获取合并后结果中90%、95%、99%的latency,此时可以使用`TDIGEST.MERGE`命令。 TDIGEST.MERGE命令的格式如下: ```redis-cli TDIGEST.MERGE destKey numKeys sourceKey... [COMPRESSION compression] [OVERRIDE] ``` 在使用上述命令时: - `如果destKey之前不存在`:将会自动创建destKey并且将合并结果设置到key的值 - `如果destKey之前已经存在`: 那么destKey的old value将会和`values of source keys`一起合并。如果需要覆盖原`destkey`的内容,需要指定`OVERRIDE`选项 ##### Retrieving sketch information `TDIGEST.MIN`和`TDIGEST.MAX`命令可以用于获取sketch中的最小值和最大值,使用示例如下: ```redis-cli > TDIGEST.MIN racer_ages "19.27" > TDIGEST.MAX racer_ages "85.709999999999994" ``` `如果TDIGEST为空,那么TDIGEST.MIN和TDIGEST.MAX命令都会返回nan`。 ##### Resetting a sketch 通过`TDIGEST.RESET`命令能够对sketch进行重置,示例如下: ```redis-cli > TDIGEST.RESET racer_ages OK ``` ## Redis Programmability redis提供了programming interface,允许在server执行自定义的脚本。在redis 7及以上,可以使用`Redis Function`来管理和运行脚本;而在redis 6.2或更低的版本,则使用`lua scripting with EVAL command`。 ### Introduce #### Backgroud 在redis中,`Programmability`代表`可以在server端执行任意用户定义的逻辑`,我们将该逻辑片段称之为`scripts`。通过脚本,能够在server端,即数据被存储的地方处理数据。在server端处理用户逻辑,能够降低网络延迟,并且能提高整体性能。 在redis通过一个`embedded, sandboxed scripting engine`来执行用户脚本。目前,redis仅支持单一的脚本引擎,即`lua 5.1 interpreter`。 #### running scripts redis提供了两种方式来运行脚本。从`2.6.0`版本开始,redis支持通过`EVAL`命令来运行server-side scripts。在使用用户脚本时,`脚本逻辑中包含应用的业务逻辑`。脚本的source code必须存储在应用端,redis server仅会临时存储source code。当应用的逻辑发生变动时,script将变得难以维护。 在redis 7.0版本中,引入了Redis Function。function能够将脚本编写与应用逻辑解耦,并且支持脚本的独立开发、测试和部署。如果要使用redis function,需要先对其进行导入,redis function导入后对所有的connections都是可用的。 当redis执行script或function时,能够保证执行过程是原子的。在脚本执行的整个过程中,redis server端的所有其他活动都会被阻塞。 在脚本执行中,应当避免执行`slow script`。如果脚本执行较慢,在执行脚本过程中,所有其他clients都会被阻塞,执行不了任何命令。 #### Read-only scripts `read-only scipts`代表在脚本执行时,并不会对redis中的任何key造成修改。可以通过两种方式来执行`read-only script`: - 在脚本中添加`no-write flag` - 通过`read-only script command`来执行脚本 - 常用read-only command如下: - `EVAL_RO` - `EVALSHA_RO` - `FCALL_RO` `read-only script`拥有如下特性: - read-only scripts可以在replicas上被执行 - 其总是可以被`SCRIPT_KILL`命令被killed - 即使当redis超过内存限制,其也不会`fail with OOM error` - 当发生`write pause`时,其也不会被阻塞 - 在read-only script中,不允许执行任何能对data set造成修改的命令 - `PUBLISH, SPUBLISH, PFCOUNT`目前仍然被视为write command #### Sandboxed script context redis将执行用户脚本的engine放在了sandbox中。sandbox主要用于防止`accidental misuse`并且降低server环境的潜在威胁。 script中永远不应该访问redis server的底层宿主机系统,例如file system、network或执行不受支持的系统调用。 script仅应该操作`redis中存储的数据`和`脚本执行时传入的作为参数的数据`。 #### Maximum execution time 脚本的执行时长受限于最大执行时长(默认情况下为5s)。这个默认超时很大,默认情况下脚本的运行时长应当小于1ms。该超时用于处理脚本执行时非预期的无限循环。 可以通过`修改redis.conf文件`或`使用config set命令`来修改该脚本执行时长上限。影响脚本最长执行时间上限的配置属性为`busy-reply-threshold`。 当脚本达到该超时上限时,其并不会自动被redis终止。中断脚本的执行将可能导致`half-write`的问题。 故而,将脚本执行时长超过限制时,将会发生如下事件: - redis会在日志中添加`脚本执行时间过长` - redis开始从其他clients接收commands,但是会对所有发送normal commands的clients返回`BUSY error`。在该场景下,唯一被允许的命令为`SCRIPT_KILL, FUNCTION_KILL, SHUTDOWN NOSAVE` - 对于`read-only script`,可以使用`SCRIPT_KILL`和`FUNCTION_KILL`命令,因为该脚本未对数据造成任何修改 - 如果`script`在执行过程中哪怕执行了一个write operation,那么只能使用`SHUTDOWN NOSAVE`命令,其会停止server,并且不会将当前的data set保存到磁盘中 ### Scripting with Lua redis允许用户上传lua脚本到server并在server端执行,故而在脚本执行时,对数据的读取和写入操作十分高效,并不需要网络开销。 redis保证脚本的执行是原子的。在执行脚本时,server的其他活动都会被阻塞。 lua允许在脚本中集成部分属于应用的逻辑,例如跨多个keys的条件更新等。 #### Getting Started 可以通过`EVAL`命令在redis中执行script,示例如下: ```redis-cli > EVAL "return 'Hello, scripting!'" 0 "Hello, scripting!" ``` 在上述示例中,`EVAL`命令接收两个参数: - 第一个参数为lua script的source code内容,该脚本内容将在redis engine's context下运行 - 第二个参数为`the number of arguments that follow the script's body` #### Script Parameterization 虽然不建议这样做,但是仍可以在应用程序中动态的生成script source code,示例如下: ```redis-cli redis> EVAL "return 'Hello'" 0 "Hello" redis> EVAL "return 'Scripting!'" 0 "Scripting!" ``` 虽然`在应用程序中动态生成script source code`的操作并未被redis禁止,但是其是不推荐的使用方式,可能会在`script cache`方面造成问题。在该场景下,不应该生成多个拥有微小差异的不同脚本,而是应该将细微的差异提取为script param。 如下实例则展示了参数化后的标准实现方式: ```redis-cli redis> EVAL "return ARGV[1]" 0 Hello "Hello" redis> EVAL "return ARGV[1]" 0 Parameterization! "Parameterization!" ``` 对于lua script执行的参数,其分为如下两种: - input arguments that are names of keys - input arguments that are not names of keys > 为了确保lua script的正确执行,不管是在standalone还是在clustered的部署模式下,script访问的所有key names都必须`显式的作为input key arguments被指定`。 > > script只应该访问那些在`input key argments`中被显式指定的key name。在编写script时,应该遵守该要求。`在redis集群环境下,应通过redis可能操作的键,从而对脚本进行路由。` > > 如果用户编写的脚本使用了未在`input key arguments`中指定的key,redis script engine虽然不会在执行时对此进行校验,但是可能导致script路由到错误的节点,错误的节点执行脚本时仍可能报错。 在调用redis脚本时,作为`非key name`被传递给redis脚本的参数,即是`regular input argument`。 在上述示例中,`Hello`和`Parameterization!`都是作为常规参数被传递的,上述示例中脚本并未用到任何redis key,故而第二个参数被指定为`0`,代表`input key arguments`的个数为0。 在redis script context中,可以通过`KEYS`和`ARGV`这两个global runtime variables来访问传递的参数。 - `KEYS`针对的是input key arguments - `ARGV`针对regular argument 如下就示例展示了input arguments在`KEYS`和`ARGV`之间分配的情况: ```redis-cli redis> EVAL "return { KEYS[1], KEYS[2], ARGV[1], ARGV[2], ARGV[3] }" 2 key1 key2 arg1 arg2 arg3 1) "key1" 2) "key2" 3) "arg1" 4) "arg2" 5) "arg3" ``` #### Interact with redis from a script 在lua script中,可以通过`redis.call`或`redis.pcall`来调用redis commands。 `call`和`pcall`大致是相同的,都会执行redis command,但是在针对runtime error的处理上这两个function之间有所不同: - `redis.call`:对于`redis.call`方法抛出的error会被直接返回给客户端 - `redis.pcall`: 对于`redis.pcall`方法抛出的异常,其会被返回给script's execution context,`for possible handling` lua script中和redis的交互示例如下所示: ```redis-cli > EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar OK ``` #### Script Cache 当调用`EVAL`时,在请求中也会包含source code内容,如果对相同的script content进行重复调用,不仅会浪费网络带宽,redis也会产生额外开销。为了节省网络带宽和计算资源,redis对script提供了缓存机制。 在redis中,每个通过`EVAL`执行的script,都会被存储在由server维护的专用缓存中。缓存的内容按照script的`SHA1 digest sum`来进行组织,SHA1 digest sum在cache中能够唯一标识脚本。 就像上述中提到的,在应用中动态生成脚本内容是不推荐的。如果不对脚本进行参数化,那么动态生成脚本的source code,并多次对脚本进行调用,将会占用redis额外的内存资源来对脚本内容进行缓存。脚本source code应该尽量通用。 脚本可以通过`SCRIPT LOAD`命令加载到cache中,server并不会实际对脚本进行执行,而是只会对其编译并加载到cache中。一旦完成加载后,可以通过脚本的SHA1 digest sum来执行脚本。 `SCRIPT LOAD`的使用示例如下所示: ```redis-cli redis> SCRIPT LOAD "return 'Immabe a cached script'" "c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f" redis> EVALSHA c664a3bf70bd1d45c4284ffebb65a6f2299bfc9f 0 "Immabe a cached script" ``` 在实际使用`eval`时,`org.springframework.data.redis.core.script.DefaultScriptExecutor#execute`方法中已经实现了`evalSha first, and eval it if script not exists`的逻辑,故而无需手动eval。 ##### Cache volatility redis script cache是易失的。script cache并不会被看作是database的一部分,且其不会被持久化。缓存的脚本随时都由可能丢失。 在如下场景下,script cache会被清空 - when server restarts - during fail-over - 显式调用`SCRIPT FLUSH`时 在应用使用scripts时,应该一直使用`EVALSHA`来执行脚本,当sha1 digest不存在时redis cache会返回error,例如 ```redis-cli redis> EVALSHA ffffffffffffffffffffffffffffffffffffffff 0 (error) NOSCRIPT No matching script ``` 在cache中不存在脚本的场景下,应用需要通过`SCRIPT LOAD`先导入脚本内容,然后调用`EVALSHA`命令来根据sha1 digest运行cache中的脚本。对于大多数的api,都提供了自动执行该过程的API。 ##### evalsha in context of pipelining 在pipeline场景下执行`EVALSHA`命令时,pipeline中的commands是按顺序执行的,但是执行的间隙中可能穿插来自其他clients的commands`。 在pipeline执行时,可能返回`NOSCRIPT error`,但是该错误无法被处理。 因此,在pipeline环境下,client library通常直接使用`eval`而不是`evalsha -> script load -> evalsha`。 ##### Script Cache Semantics 在执行常规操作时,来自应用的脚本会被一直保存在cache中,直到server被重启或缓存被清空。 清空缓存的唯一方式是通过显式调用`SCRIPT FLUSH`命令,执行该命令会完全清空script cache,移除目前执行过的所有脚本内容。通常来说,该命令只在`当前redis实例需要初始化,并后续为其他应用服务`时被调用。 #### Script Command `SCRIPT`命令提供了一系列用于控制scripting subsystem的方法: - `SCRIPT FLUSH`: 该命令是清空redis script cache的唯一方式 - `SCRIPT EXISTS`: 可接收一个或多个sha1 digests作为参数,该命令会返回值为`0 or 1`的数组。其中`1`代表脚本在cache中存在,而`0`代表脚本在cache中不存在。 - `SCRIPT LOAD`: 该命令会向redis cache中注册一个特定的脚本。在执行`EVALSHA`命令之前执行该命令,能够确保`EVALSHA`命令不会失败 - `SCRIPT KILL`: 该命令是打断`long-running script`的唯一方式。当script的执行时长超过限制(默认为5s)时,如果该脚本执行时未对数据进行任何修改,则可以通过`SCRIPT KILL`命令来终止 - `SCRIPT DEBUG`: 用于控制`redis lua scripts debugger`的使用 #### Script Replication 在standalone部署场景下,单个master实例管理整个database,而在集群部署模式下,至少会存在3个masters共同管理database。对于master实例,redis会使用replication来维护一个或多个replicas。 由于scripts可能会修改数据,故而redis需要保证所有由script执行的写操作都要被同步给replicas,从而replicas的数据一致性。 在script replication方面,存在如下两种方法: - `verbatim replication`:master会将script的source code发送给replicas,replicas再执行接收到的脚本。这种模式能够节省replication brandwith,因为在循环场景下短脚本能够产生大量的命令。 - 但是,这种模式下,replicas会对master的工作进行redo,即使针对读指令,这将会浪费资源,并且要求脚本中的写操作具有确定性 - `effects replication`: 只有脚本中对数据造成修改的命令才会被同步,replicas针对`被同步的写指令`进行执行,并不会像前一个模式一样重新执行脚本。 - 这这种模式下,关于replication的brandwith会增加,但是该模式本质上的确定的,无需写操作具有确定性 > 在redis 3.2之前,`verbatim script replication`是唯一受支持的模式,直到Redis 3.2加入`effects replication`。 #### Replicating commands instead of Scripts 从redis 3.2开始,可以选择`effects replication`的同步方式,该方式会`replicate commands generated by the script`。 > 从redis 5.0开始,script effects replication为默认模式,并不需要显式启用。 在该模式下,当lua脚本被执行时,redis会收集`all the commands executed by Lua scripting engine that actually modify the dataset`。当脚本执行完成之后,commands sequence将会被封装到`multi/exec`中,并且被发送给replcias和AOF。 该种replication方式在如下用例场景下具有优势: - script的计算速度很慢,但是最终script的修改能够被统计为较少的write commands - 当启用script effects replication时,`non-determinstic function`校验将会被移除。故而,可以在脚本中自由的使用`TIME`或`SRANDMEMBER`这类非确定性的指令 - The Lua PRNG in this mode is seeded randomly on every call 除非通过server配置或默认启用了effect replication,否则若需令脚本同步按照effect replication的方式进行同步,必须在脚本执行任何write command之前执行如下lua命令: ```lua redis.replicate_commands() ``` 在调用`redis.replicate_commands`方法时: - 如果effect replication被启用,那么返回值为true - 如果在脚本已经执行过write command之后再调用该方法,那么该方法返回值为false,并且会使用normal whole script replication 该方法在Redis 7.0中被标记为废弃,如果仍然调用它,那么其返回值一直为true #### Scripts with deterministic writes 从redis 5.0开始,script replication默认情况下为effect-based而不是verbatim;而在redis 7.0,verbatim script replication的方式被完全移除。故而,如下内容只针对版本低于7.0并且没有使用effect-based的script replication。 在使用verbatim script replication的情况下,主要注意`only change the database in a deterministic way`。在redis实例执行script时,一直到redis 5.0,默认都是通过`sending the script itself`方式来传播脚本的执行到replicas和AOF中的。在replica上,被传递的脚本会被重新执行,脚本对database的修改必须可重现。 通常,发送脚本本身占用的带宽要比发送`脚本产生的命令`占用的带宽要小,cpu消耗也更小。 但是,`sending the script itself`这种replication方式并非对所有的场景都是可行的。 `verbatim scripts replication`要求脚本拥有如下属性: - 在`arguments相同,input data set相同`的场景下,脚本必须产生相同的redis write commands - 脚本执行的操作不能依赖于任何`hidden(non-explicit) information`或`state that may change as the script execution proceeds or between different executions of the script` - 脚本的执行也不能依赖于任何io设备的外部输入 例如,`使用系统时间、调用返回随机值的redis命令、使用redis的随机数生成器`都会导致`scripts that will not evaluate consistently` 为了保证脚本的确定性行为,redis做了如下处理: - lua不会export任何`访问系统时间或外部状态的命令` - 如果脚本在调用`random command`(例如`RANDOMKEY, SRANDMEMBER, TIME`)之后,又调用了`Redis command able to alter the data set`,那么redis将会`block the script with error`。 - 上述即代表`read-only scripts that don't modify dataset can call those commands` - `random command`并不代表使用随机数的command,而是代表`non-deterministic command`(例如TIME) - 在redis 4.0中,`例如SMEMBERS这类以随机顺序返回元素的命令`,在`被lua脚本调用时`表现出的行为不同,在将数据返回给lua脚本时会根据字典序进行排序。 - 故而,在redis 4.0环境下,lua脚本中调用`redis.call("SMEMBERS", KEYS[1])`时,总是会按相同的顺序来返回Set中的元素。 - 但是,从redis 5.0开始,又不会执行该排序,因为可以`effect replication`被设置为默认 - lua的伪随机数生成function `math.random`已经被redis修改,故而在每次执行时都会使用相同的seed。 - 故而,每次script执行时,调用`match.random`总是会生成相同序列的数字 由上述描述可知,redis修改了lua的伪随机数生成器(只在verbatim replication下成立),故而每次运行lua脚本时,随机数生成器返回的数值序列都相同。 但是,仍然可以通过一定的技巧来生成随机数,示例如下所示: ```ruby require 'rubygems' require 'redis' r = Redis.new RandomPushScript = < 0) do res = redis.call('LPUSH',KEYS[1],math.random()) i = i-1 end return res EOF r.del(:mylist) puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)]) ``` 每次上述程序运行,resulting list都会拥有相同的元素: ```redis-cli redis> LRANGE mylist 0 -1 1) "0.74509509873814" 2) "0.87390407681181" 3) "0.36876626981831" 4) "0.6921941534114" 5) "0.7857992587545" 6) "0.57730350670279" 7) "0.87046522734243" 8) "0.09637165539729" 9) "0.74990198051087" 10) "0.17082803611217" ``` 为了让脚本确定,并且让其产生不同的random elements,可以像脚本中添加额外参数,用于对lua的伪随机数生成器进行seed。脚本示例如下所示: ```ruby RandomPushScript = < 0) do res = redis.call('LPUSH',KEYS[1],math.random()) i = i-1 end return res EOF r.del(:mylist) puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32)) ``` 上述示例中,通过`math.randomseed`方法来将ruby生成的随机数作为了lua随机数生成器的种子,从而产生了不同的数值序列。 当然,上述脚本的内容仍然是确定的,当传递的`ARGV[2]`相同时,lua随机数生成器生成的数值序列仍然是固定的。 该seed是由client生成的,作为参数被传递给脚本,并且会作为参数被传播给replicas和AOF。这样,能够确保同步给AOF和replicas的changes相同。 #### Debugging Eval scripts 从Redis 3.2开始,Redis支持natvie Lua debugging,redis lua debugger是远程的,由server和client组成。 #### Execution under low memory conditions 当redis的内存使用超过最大限制后,`the first write command encountered in the script that uses additional memory will cause the script to abort`。 但是,当script中的第一条write command没有使用额外内存时,存在例外(例如`DEL, LREM`命令)。在该场景下,redis会允许该脚本中所有的命令运行,从而保证lua脚本执行的原子性。`如果lua脚本中接下来的命令耗费了额外的内存,那么redis内存使用可以超过最大值限制。` #### Eval flags 通常,当运行Eval script时,server并不知道该脚本如何访问database。默认情况下,redis假设所有脚本都会对数据进行`读取和写入`。 但是,从Redis 7.0开始,支持在创建脚本时声明`flags`,用于告知redis将如何访问数据。 在如下示例中,将在脚本的第一行声明flags: ```lua #!lua flags=no-writes,allow-stale local x = redis.call('get','x') return x ``` 当redis看到`#!`的注释时,其将会将脚本看作`声明了flags`,即使没有flags被实际定义,其相比于没有`#!`的脚本仍然存在一系列不同的默认值。 另一个不同的区别是,`scripts without #!`可以运行命令来访问`keys belonging to different cluster hash slots`,但是拥有`#!`的将继承默认的flags,故而其不能对`keys belonging to different cluster hash slots`进行访问。 ### Redis Pub/sub 在redis中,`SUBSCRIBE,UNSUBSCRIBE,PUBLISH`实现了`Publish/Subscribe消息范式`,其规范如下: - 消息的发送者并未直接将消息发送给特定的接受者 - 消息发送者将消息发送给特定的channel,并不知道消息订阅者的情况,甚至不知道消息是否存在订阅者 - 消息的订阅者对一个或者多个channel订阅,并且只会接收订阅的消息 - 消息的订阅者并无法感知消息的发布者 上述规范将消息的发布者和订阅者进行了解耦,增强了拓展性,并且允许更加动态且灵活的网络拓扑。 例如,如果想要对`channel11`和`ch:00`channel进行订阅,client可以发送`SUBSCRIBE`命令: ```redis-cli SUBSCRIBE channel11 ch:00 ``` 其他clients发送到这些channels的消息将会被redis推送给所有订阅这些channels的消息。消息的订阅者将会按照`消息被推送的顺序`来接收消息。 如果一个client订阅了一个或多个channels,那么其不应该发送任何commands,但其可以对channel进行`SUBSCRIBE`和`UNSUBSCRIBE`。 对于`subscription`和`unsubscription`操作的回复`以消息的形式被返回`,client只需要读取连续的消息流即可,消息流中的第一个元素代表消息的类型。 在一个已经订阅的`RESP2`client的上下文中,允许执行的命令为: - `PING` - `PSUBSCRIBE` - `PUNSUBSCRIBE` - `QUIT` - `RESET` - `SSUBSCRIBE` - `SUBSCRIBE` - `SUNSUBSCRIBE` - `UNSUBSCRIBE` 但是,在使用RESP3时,client在`subscribed`状态下可以发送任何commands。 当使用`redis-cli`时,如果处于`subscribed`模式下,那么并无法调用`UNSUBSCRIBE`和`PUNSUBSCRIBE`命令,此时`redis-cli`将无法接收任何命令,并且只能通过`Ctrl + C`来退出。 #### Delivery semantics Redis的`Pub/Sub`机制表现出了`at-most-once`的消息传递语义,其代表消息最多只会被发送一次。一旦消息被redis server发送,不会再重新发送。并且,如果订阅者无法处理该消息(例如网络故障或处理异常),那么该消息将会被永远丢失。 如果应用需要更强的消息传递保证,需要使用`Redis Stream`。在stream中的消息将会被持久化,并且支持`at-most-once`和`at-least-once`两种传递语义。 #### format of pushed messages 消息是`array-reply with three elements`,其中第一个元素为消息的种类: - `subscribe`: 代表我们成功订阅了channel,channel的名称在第二个element中给出。第三个元素代表目前订阅的channels数量 - `unsubscribe`: 代表我们成功取消了对channel的订阅,channel的名称在第二个元素中给出。第三个元素代表目前订阅的channels数量 - 当最后一个元素为0时,代表不再订阅任何channel,此时client可以发送任何类型的redis commands,client不再处于`Pub/Sub`状态 - `message`: 代表当前message是被其他client通过`PUBLISH`命令发布的消息。第二个元素为`name of the originating channel`,第三个元素则是实际消息的`payload` #### Database & Scoping `Pub/Sub`机制和key space没有关联。`Pub/Sub`机制并不会被任何层面干扰,包括database numbers。 在db 10中发布的消息,仍然可以被db1上的subscriber接收。 如果需要对channel进行作用域限制,可以为channel name前缀环境名称(test, staging, production...)。 #### Example 对channel进行监听可以使用`SUBSCRIBE`命令 ```redis-cli 127.0.0.1:6379> subscribe first second 1) "subscribe" 2) "first" 3) (integer) 1 1) "subscribe" 2) "second" 3) (integer) 2 1) "message" 2) "first" 3) "fuckyou" 1) "message" 2) "second" 3) "shit" ``` 向channel发送消息可以使用`PUBLISH`命令 ```redis-cli 127.0.0.1:6379> publish first fuckyou (integer) 1 127.0.0.1:6379> publish second shit (integer) 1 ``` #### Pattern-matching subscriptions redis pub/sub实现支持pattern matching。clients支持对`glob-style` pattern进行订阅,并接收所有发送到匹配channel的消息。 例如 ```redis-cli PSUBSCRIBE news.* ``` 上述指令会订阅所有发送到`news.art.figurative`, `news.music.jazz`等channel的消息。所有`global-style patterns`都有效,也支持多个wildcards。 ```redis-cli PUNSUBSCRIBE news.* ``` 上述命令将会根据pattern对channels进行取消订阅。 `Messages received as a result of pattern matching`其发送格式将会有所不同,其将包含4个elements: - 消息的type将会是`pmessage`: 这代表该消息通过pattern-matching subscription匹配到的 - 第二个元素为original pattern matched - 第三个元素为name of the originating channel - 第四个元素为message payload 通过`psubscribe`接收到的消息结构如下: ```redis-cli 127.0.0.1:6379> psubscribe f* 1) "psubscribe" 2) "f*" 3) (integer) 1 1) "pmessage" 2) "f*" 3) "first" 4) "suki" ``` #### Messages matching both a pattern and a channel subscription 如果client订阅了多个patterns,那么可能多次接收到相同的消息,示例如下所示: ```redis-cli SUBSCRIBE foo PSUBSCRIBE f* ``` 在上述示例中,如果消息被发送到`foo` channel,那么client将会接收到两条消息:type为`message`的消息和type为`pmessage`的消息。 #### the meaning of the subscription count with pattern matching 在`subscribe, unsubscribe, psubscribe, punsubscribe`消息类型中,消息中最后的元素代表仍然处于活跃状态的订阅数量。该数量代表`total number of channels and patterns the client is still subscribed to`。故而,如果该数量变为0,代表client会退出`Pub/Sub`状态,client取消了对所有channels和patterns的订阅。 #### Sharded Pub/Sub 从reids 7开始,引入了shard channels,其将被分配给slots,且分配的算法和`assign keys to slots`的算法相同。并且,`a shard message must be sent to a node that owns the slot the shard channel is hashed to`。集群将会确保published shard messages将会被转发给shard中所有的节点,故而client在订阅shard channel时,可以连接shard中的任意一个节点,不管是`master responseible for the slot`还是`any of master's replicas`。 Sharded Pub/Sub能够帮助`Pub/Sub`在集群模式下的拓展。其将消息的传播范围限制在`shard of a cluster`之内。故而,相比于`global Pub/Sub`模式下`每个消息都会被传播到cluster中的所有节点`,`Sharded Pub/Sub`能够减少cluster bus传播的数据量。 `SSUBSCRIBE, SUNSUBSCRIBE, SPUBLISH`命令能够用于`sharded pub/sub`场景。 > 在redis的定义中,`a shard is defined as a collection of nodes that serve the same set of slots and that replicate from each other`。 > 在未引入`Shard Pub/Sub`机制之前,`Pub/Sub`的channel在集群中并不会被hash到slot。此时,`cluster中的每个node独立的维护订阅关系,不同节点之间的订阅并不共享`。并且,发送给某一节点的消息将会广播到整个cluster中所有的nodes。 #### Redis Keyspace notifaction Keyspace notification允许客户端针对`Pub/Sub channels`进行订阅,从而接收`events affecting the Redis data set`。 接收事件的示例如下: - all the commands affecting a given key - all the keys receiving an LPUSH operation - all the keys expiring in the database 0 > 在使用`Pub/Sub`时,如果client失去了对redis的连接,并重新连接后,在client丢失连接的时间范围内,所有的事件都会丢失 ##### Type of events `keyspace notifications are implemented by sending two distinct types of events for every operation affecting the redis data space.` 例如对`database 0`中的`meykey`执行的`DEL`操作,将会传递两个消息,等价于如下publish语句: ```redis-cli PUBLISH __keyspace@0__:mykey del PUBLISH __keyevent@0__:del mykey ``` 其中`__keyspace@0__:mykey`代表所有针对`mykey`的事件;`__keyevent@0__:del`只针对`mykey`的`del` operation。 第一种类型的事件,其channel名称包含keyspace prefix,被称为`key-space notification`。 第二种类型的事件,channel名称中包含keyevent prfix, 被称为`Key-event notification`。 在前面示例中,对mykey的del event生成了两条消息: - `The key-space channel receives as message the name of the event` - `The key-event channel receives as message the name of the key` ##### Configuration 默认情况下,keyspace event notification处于disabled状态,因为其并非必须特性且会消耗部分CPU资源。可以通过`redis.conf`中的`notify-keyspace-events`配置或通过`CONFIG SET`来对其进行启用。 将该参数设置为空字符串会禁用notifications,为了启用该特性,需要将指定为非空,字符串由如下字符组成: - 字符串中至少应该存在`K或E`,否则事件将不会被传递 - 如果要对lists启用`key-space events`,那么可以配置为`Kl` - 可以将参数设置为`KEA`为大部分data types启用events 字符的含义如下表所示: ``` K Keyspace events, published with __keyspace@__ prefix. E Keyevent events, published with __keyevent@__ prefix. g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ... $ String commands l List commands s Set commands h Hash commands z Sorted set commands t Stream commands d Module key type events x Expired events (events generated every time a key expires) e Evicted events (events generated when a key is evicted for maxmemory) m Key miss events generated when a key that doesn't exist is accessed (Note: not included in the 'A' class) n New key events generated whenever a new key is created (Note: not included in the 'A' class) o Overwritten events generated every time a key is overwritten (Note: not included in the 'A' class) c Type-changed events generated every time a key's type changes (Note: not included in the 'A' class) A Alias for "g$lshztdxe", so that the "AKE" string means all the events except "m", "n", "o" and "c". ``` ##### Timing of expired events 如果key存在`ttl`,那么redis将会在如下两种时机下让key过期: - 当key被command访问,并发现key过期 - 当backgroud system查找到expired keys 当key通过上述两种机制之一被发现过期时,会生成expired events。redis`并不保证`在key的ttl到期后`立马`会产生expired event。 如果该key未持续被command访问,且同时存在多个key关联ttl,那么`the key time to live drops to zero`的时间和`the exipred event`生成的时间可能存在较大的延迟。 `Expired events`将会在redis server删除key时被生成,并非`ttl`减为0的时间。 ##### Events in cluster redis cluster中的每个node都会生成其自己的keyspace相关事件。但是,和集群环境下`Pub/Sub`机制不同的是,`events notifications`并不会被广播到cluster中的其他节点。keyspace events是`node-specific`的,从cluster接收到所有的keyspace events时,clients需要为每个node都进行订阅。 ## Redis Cluster Speification ### Main properties and rationales of the design #### Redis Cluster Goals Redis Cluster为redis的分布式实现,其目标按照重要性排序如下: - 高性能,并且支持线性拓展到1000 nodes,并不会使用代理,并且不会对values执行merge operations - `Acceptable degree of write safty`: 系统会尝试保留所有来源clients的写操作 - `Availability`: redis cluster在`绝大多数master nodes可达,并且对每个master node都至少有一个replica可达`的场景下维持生存。此外,在使用`replicas migration`时,如果master不再拥有任何replica,那么其会从其他`拥有多个replicas的master`处接收一个replica。 #### Implemented subset redis cluster实现了`所有非分布式版本redis支持的single key commands`。对于`performing complex multi-key operations`的命令,例如`set unions and intersections`,仅针对`所有keys都被hash到相同slot`的场景提供了实现。 redis cluster实现了`hash tags`的概念,用于强制让部分keys被存储到相同的slot。但是,在manual resharding的期间,multi-ey operations可能并不可用,而single-key operations则是永远可用的。 Redis Cluster并不像standalone版本的redis一样支持multiple databases,其只支持database 0。并且,`SELECT`命令在集群环境下不允许被执行。 ### Client and Server roles in the Redis cluster protocol 在redis集群中,nodes负责对数据进行存储,并维护集群的状态,以及将keys映射到对应的nodes。cluster nodes可以自动发现其他的ndoes,并探知`non-working nodes`,在错误发生时将replica nodes提升为master。 cluster中所有的nodes都会通过TCP bus和名为`Redis Cluster Bus`的二进制协议来连结。其中,`TCP bus`是一种逻辑上的结构,代表cluster nodes中任两个nodes之间都会相互连接。 nodes使用gossip protocol来传播关于cluster的信息,可用于`发现新节点、发送ping packet确保所有的其他节点状态正常、指定条件下发送的消息`。`在集群间传播Pub/Sub消息、在用户请求时手动failover`也会使用到cluster bus。 因为cluster nodes不能代理请求,故client可通过`redirection errors(MOVED, ASK)`被重定向到其他节点。client理论上可以对集群中的任何节点发送请求,并在需要时重定向到其他节点,`故client无需持有集群的状态`。但是,client可以缓存keys和nodes的关系,从而提升性能。 #### write safety redis cluster在nodes之间使用了asynchronous replication,并使用了`last failover wins`的`implicit merge function`。 > `last failover wins implicit merge function`代表在集群中,如果某节点被选举为新的主节点,那么该选举出节点的数据就称为了该分片的权威数据,分片中所有其他replicas都会从新选举的节点中复制数据并覆盖自己原先数据,从而保证failover后所有replicas和master node数据的一致性。 在发生`network partition`时,总会存在一个window of time,在该时间范围内部分写操作可能会丢失。但是,在不同场景下,window可能会有所不同,这取决于: - client is connected to the majority of masters - client is connected to the minority of masters ##### connected to the majority of masters Redis Cluster `tries harder` to retain writes that are performed by clients connected to the majority of masters, compared to writes performed in the minority side. > 在上述描述中,`tries harder`是由redis cluster的failover选举机制决定的,相比于`minority of master`的写操作会直接丢失,针对`majority of masters`的写操作绝大部分会被保留,基于`last failover wins`的策略。 如下示例展示了`loss of acknowledged writes received in the majority partitions during failures`的场景: - 写操作可能已经发送给master,且mater响应了该写请求,但是该写操作尚未通过asynchronous replication被同步到replica nodes。如果在该时刻master发生故障,那么该写操作将永远丢失。 - 另一种理论上可能的`write loss`模式如下: - master因为`network partition`而不可被访问 - master的其中一个replica发生failover - 过了一段时间后master又重新可访问 - `A client with an out-of-date routing table`可能会向old master发送写请求,如果此时`old master尚未被转化为cluster的replica`,那么该写操作又可能会被丢失 上述描述中,第二种场景不太可能发生,因为在`master nodes unable to communicate with the majority of the other masters for enough time to be failed over`(`for enough time to be failed over`代表无法通信的时间已经达到触发failover的时间)的前提下,master ndoes将无法接收写请求,并且即使在`network partition`已经被修复的场景下,在一段时间内写请求仍然会被拒绝,用以其他节点通知该master节点集群状态的变化。除此之外,该场景还需要client的routing table尚未被更新。 ##### connected to the minority of masters 针对`minority side of a partition`的写请求将会又有更大的`丢失写操作窗口`。例如,如果`minority of masters拥有一个或多个clients`,redis cluster将会在`network partition`期间丢失相当多数量的写操作,因为在`the masters are failed over in the majority side`时,所有发送到`minority of masters`的写请求都会丢失。 具体而言,如果master要发生fail over,那么其必须至少在`NODE_TIMEOUT`时间范围内无法被`majority of masters`访问 - 如果network partition在该时间限制前恢复,并不会丢失任何写请求 - 如果`network partition`的持续时间超过`NODE_TIMEOUT`,那么所有针对`minority side`的写操作(直到达到NODE_TIMEOUT)都会被丢失 - 因为minority side在`NODE_TIMEOUT`达到后,如果仍然无法连接majority,将拒绝接收写请求,故而minority不可访问后window存在上限 #### Availability redis cluster在`minority side of partition`方将不可用。在`majority side of the partition`,假设其拥有`majority of masters`,并对每个不可达的master都拥有一个replica,那么在时间超过`NODE_TIMEOUT`之后,一个replica将会被重新选举变为新的master,此时redis cluster将重新可访问。 > 上述描述具体含义如下: > - 当network partition发生后,majority partition部分可用,而minority partition不可用 > - 当NODE_TIMEOUT达到后,majority partition会重新选举master,然后majority partition完全可用 > - 当network partition恢复后,minority partition将作为replicas加入到集群,作为replcias提供服务 > 故而,redis cluster允许在少数节点发生故障时进行恢复(需要majority of masters),但是对于`large net splits`场景,redis cluster并不适用。 如果cluster由N个master nodes组成,每个master node都拥有一个replica,那么cluster的majority side在一个节点partitioned之后仍然可访问,但是当两个节点partitioned之后,仍然能正常访问的概率为`1-(1/(N*2-1))`。 redis cluster目前存在`replicas migration`的特性,replicas将会迁移到`orphaned masters`(masters no longer having replicas),这将在很多场景下提高redis cluster的可访问性。故而,每次正常处理failure event后,cluster都会重新调整replicas layout,以更高的抵御下一次failure。 #### Performance 在redis cluster nodes中,并不会将commands代理到正确的节点,而是通过将client重定向到正确的节点。 最终,clients会获取cluster的最新状态,状态中记录了每个node处理那些keys集合,故而在正常操作时client能够直接连接正确的节点。 因为redis cluster使用了asynchronous replication,向node执行写操作时,操作返回前node并不会等待其他nodes的ack。 并且,在redis cluster环境下,multi-key操作只允许keys被hash到同一个slot时可用。触发发生resharding,否则数据绝不会在节点之间进行移动。 redis cluster的常规操完全和单个redis实例相同,即在`Redis Cluster with N master nodes`的场景下,其理论性能大致为`cluster_with_n_nodes_performance = single_instance_performance * N`,故而redis cluster的性能是可线性拓展的。 并且,对redis cluster的查询操作也通过在一个`round trip`中完成,而clients通常会维持和ndoes的长连接,故而redis cluster操作的延迟和single instance场景下的延迟也大致相同。 Redis Cluster的目标如下:`在保证高性能和高可拓展性的前提下,使用弱但是合理的data safety和可用性`。 #### Why merge operations are avoided Redis Cluster设计避免了多个nodes针对相同的key进行写操作的场景。在redis中,value可能会很大,例如lists或sorted sets可能会包含数百万个元素。并且,redis data type的结构也可能很复杂。在这种场景下,对这些value的转移和合并可能会造成很大的性能瓶颈。故而,redis cluster摈弃了该设计。 ### Overview of Redis Cluster main components #### key distribution model cluster的key space被划分为了16384(2^14)个slots,故而cluster中最多包含`16384`个master nodes(推荐的集群节点最大数量约为1000)。 cluster中的每个master node处理16384中的一个子集。在没有`cluster reconfiguration`正在进行时(即hash slots没有从一个节点移动到另一个节点),cluster处于稳定状态。当cluster处于稳定状态时,一个slot只会对应一个master node。(master node可能含义多个replicas,并在在net splits或failures场景下replica可能会代替master node)。 将keys映射到hash slots的算法如下所示: ``` HASH_SLOT = CRC16(key) mod 16384 ``` #### hash tags 在计算key对应的hash slot时,有一种例外,其用于实现`hash tags`。`hash tags`是用于确保`multiple keys被分配到相同hash slot`的机制。其用于在redis cluster环境下实现multi-key操作。 为了实现hash tags,当key中包含`{}`的pattern时,只有`{`和`}`之间的子串才会被用于hash计算。然而,当key中包含多个`{}`包围的部分时,其规则如下: - IF the key contains a `{` character - AND IF there is a `}` to the right of `{` - AND IF there are one or more characters between the first occurrence of `{` and the following first occurrence of `}` is hashed 具体示例如下: - `{user1000}.following`和`{user1000}.followers`都会被hash到相同的slot,二者都根据`user1000`来计算hash slot - 对于`foo{}{bar}`,由于其第一个`{`和后续第一个`}`之间并没有包含任何字符,故而`foo{}{bar}`的key整体都会被用于hash计算 - `foo{{bar}}zap`其子串`{bar`将会被用于计算hash slot,因为第一个`{`和其后第一个`}`间包围的字符串为`{bar` - 对于`foo{bar}{zap}`,其子串`bar`将被用于hash slot的计算 - 在将二进制数据作为key时,可以令key以`{}`开头,其会保证根据整体来计算hash slot ##### global-style patterns 部分commands接收glob-style pattern,例如`KEYS`, `SCAN`, `SORT`等命令,其针对`patterns that imply a single slot`的场景进行了优化。其代表`if all keys that can match a pattern must belong to a specific slot`,那么在查找匹配该pattern的keys时,只有该slot会被检查。该pattern slot优化在redis 8.0中被引入。 在pattern满足如下条件时,该优化被命中: - pattern contains a hash tag - there no wildcards or escape characters before the hash tag, and - the hashtag within curly braces doesn't contain any wildcards or excape characters #### Cluster node attributes ##### node ID 在cluster中,每个node都有一个unique name。node name由`160 bit`随机数字的`hex`来表示,当node启动时,第一时间就会获取unique name。(通常,会使用/dev/urandom)。node会将ID存储在node的配置文件中,并且会一直对该ID进行重用,直至配置文件被删除或通过`CLUSTER RESET`来强制重置。 node ID用于在整个cluster之间表示每个node,对于cluster中的node,允许变更其IP但是维持其node ID不变。并且,cluster也支持检测IP/port的变更,并且通过gossip协议对cluster进行reconfigure。 ##### node Attributes 对于cluster而言,`node ID`并非是每个node关联的唯一信息,但是其是唯一会保持全局一致的属性。cluster中每个node都包含了一系列关联的信息,一些信息是与`cluster configuration detail of this specific node`相关的,该信息是最终一致的。 - 全局一致属性:node ID在任何时刻都是全局一致的属性,集群中所有节点对node ID的属性认知相同 - 最终一致属性:如`slot归属,主从关系`的属性,短期内不同节点的认知可能并不一致,但是随着时间的流逝cluster中各节点的认知会趋于相同 - 本地存储属性:该类属性为节点私有的,仅在单个节点存储,不再nodes间进行共享和同步,例如`本地连接数、最后ping时间`等属性 > 在上述描述中, > - `全局一致`代表集群中所有节点`在任何时刻`对相同属性的认知完全一样 > - `最终一致`代表集群中所有节点`在某些时刻`对相同属性的认知可能会有所不同,但在长期会趋于相同 > - `instead local to each node`: 代表该属性保存在node本地,不同node的属性并不共享 在cluster中,`every node maintains the following information about other nodes that it is aware of in the cluster`: - node的`ID, IP, port` - 一系列flags - 如果node被标记为replica,记录其master信息 - last time the node was pinged - last time the pong was received - current configuration epoch of the node - link state - the set of hash slots served ##### CLUSTER NODES `CLUSTER NODES`命令可以被发送给cluster中的任何节点,用于提供集群的状态以及每个节点的信息。 如下为`CLUSTER NODES`被发送给master node后输出的信息示例: ```redis-cli $ redis-cli cluster nodes d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364 3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729 d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095 ``` 在上述示例中,列出的不同fields按顺序为: - node id - address:port - flags - last ping sent - last pong received - configuration epoch - link state - slots #### Cluster bus 每个redis cluster node都拥有一个额外的tcp port用于接收`incoming connections from other redis cluster nodes`,该port将会在`data port`的基础上`+10000`获得,其也可以通过cluster-port config来进行指定。 例如,当redis在`6379`端口上监听client connections,那么可以不向`redis.conf`中添加`cluster-port`参数,cluster bus port会被定为`16379`。 同样的,也可以在`redis.conf`中配置`cluster-port`为`20000`,cluster bus port将会被定为`20000`。 `node-to-node`的交流完全通过`cluster bus`和`cluster bus protocol`来进行。`cluster bus protocol`为一个二进制协议,由`frames of different types and sizes`组成。 #### Cluster topology redis cluster是一个`full mesh`结构,集群中每个node都和其他所有节点都单独建立了一个TCP连接。 在包含N个节点的cluster中,每个node都有`N-1`个outgoing TCP连接和`N-1`个incoming connections。 这些tcp连接会一致保持活跃,且并非是按需创建的。当一个node期望其他node的pong响应时,在`waiting long enough to mark the node as unreachable`之前,其会尝试对connection进行刷新,从头开始对node进行来reconnecting。(重新连接后会重新发送PING) redis cluster nodes组成了full mesh,但是node使用了`gossip protocol`和`configuration update mechanism`用于避免`nodes之间的消息交换过于频繁`。故而,消息交换的数量并非指数级增长。 #### node handshake node会从cluster bus port接收连接,当接收到ping时会返回响应,即使发出ping的node并不可信。除了ping packet之外,如果接收方node认为发送方节点并不是cluster中的一员,那么接收方会丢弃所有发送方除了ping packet之外的packets。 只有在如下两种场景下,一个node才会将应一个node认作集群的一部分: - 如果一个节点通过`MEET` message来展示其自身(CLUSTER MEET命令)。meet message和`PING` message类似,但是会强制接收方将其作为cluster的一部分来接受。只有当系统管理员通过`CLUSTER MEET ip port`命令来发送请求时,node才向其他nodes发送meet message。 - 如果一个已经受信任的节点通过gossip传播了关于`node C`的消息,那么其他节点也会将`node C`注册为集群的一部分。 - 例如,`A knows B`,`B knows C`,最终B会向A发送关于C的gossip messages。当该行为发生时,A会将C注册为network的补一份,并且尝试和C建立连接 上述示例代表,只要将节点加入到`connected graph`,其最终自动会形成一个`fully connected graph`(尚未连接的nodes会两两自动建立连接)。在cluster中,只要管理员强制建立信任关系,节点能够自动发现其他节点。 ### Redirection and Resharding #### MOVED Redirection 一个redis client可以向cluster中所有的节点发送请求,包括replica nodes。该node会分析请求,如果请求时可接受的(在query中只有一个key,或多个key被hash到相同的slot中),那么其会分析key由哪个node负责: - 如果node本身负责该slot,那么该请求将会被执行 - 否则,node将会向client返回`MOVED error`,示例如下所示: ```redis-cli GET x -MOVED 3999 127.0.0.1:6381 ``` 上述error代表key对应的hash slot(3999)和负责该key实例的`endpoint:port`值。client需要重新向`endpoint:port`发送请求。 > 上面示例中,`endpoint`可以是一个ip address,hostname,或可以为空(`-MOVED 3999 :5380) > - 当endpoint为空时,代表server node拥有unknown endpoint,client应该将下一个请求发送给`当前请求的endpoint`,但是使用响应中的port 在client重新发送请求之前,如果间隔时间较长,此时cluster configuration可能会发生变动,此时再发送请求,如果负责该key的节点发生变化,目前节点仍可能会返回一个MOVED error。 client不必须,但是应该记住`hash slot 3999被127.0.0.1:6381管理`。之后如果新command对该hash slot发送请求,那么其可以直接针对正确的节点发送请求,避免重定向带来的开销。 对client而言,另一个可选项是`在接收到MOVED redirection之后,通过CLUSTER SHARDS命令清空整个client-side cluster layout`。通常而言,当遇到重定向时,更可能是多个slots都被reconfigured,此时尽快更新client configuration是更好的策略。 当集群处于稳定状态下时,最终所有客户端都会获取到`map of hash slots`,确保cluster效率更高,client无需重定向就能查询到正确的nodes。 #### Live reconfiguration ##### ADDSLOTS/DELSLOTS Redis Cluster支持在运行时动态添加、移除nodes的能力。对redis cluster而言,节点的增加和移除被抽象为相同的行为:`moving a hash slot from one node to another`。上述机制也能被用于cluster的rebalance。 - 将新节点添加到cluster时,`some set of hash slots from existing nodes`将会被移动到新的节点 - 将节点从cluster中被移除时,`hash slots assigned to that node`将会被移动到其他的existing nodes - 在cluster进行rebalance时,`a given set of hash slots`将会在节点之间进行移动 实现上述内容的核心是`hash slots的移动`。从实际角度来看,hash slots是一系列keys的集合,故而在redis cluster进行resharding时,就是将keys从一个实例移动到另一个实例。`移动hash slot`代表`移动被hash到该slot的keys`。 `CLUSTER`命令拥有如下`subcommands`来操作redis cluster node中的`slot translation table`: - `CLUSTER ADDSLOTS slot1 [slot2]...[slotN]` - `CLUSTER DELSLOTS slot1 [slot2]...[slotN]` - `CLUSTER ADDSLOTSRANGE start-slot1 end-slot1 [start-slot2 end-slot2]...[start-slotN end-slotN]` - `CLUSTER DELSLOTSRANGE start-slot1 end-slot1 [start-slot2 end-slot2]...[start-slotN end-slotN]` - `CLUSTER SETSLOT slot NODE node` - `CLUSTER SETSLOT slot MIGRATING node` - `CLUSTER SETSLOT slot IMPORTING node` 在上述命令中,`ADDSLOTS, DELSLOTS, ADDSLOTSRANGE, DELSLOTSRANGE`四个命令用于`assign/remove slots to redis node`。(`Assigning a slot means to tell a given master node that it will be in charge of storing and serving content for the specified hash slot`) 当hash slots被分配后,其将会通过gossip protocol在cluster的范围内传播该信息。 `ADDSLOTS/ADDSLOTSRANGE`命令通常在`新集群被从头创建,为每个master节点分配所有16384个hash slots的子集时`被使用。 `DELSLOTS/DELSLOTSRANGE`则主要在手动修改cluster configuration时被使用,通常其使用场景较少。 > `ADDSLOTS, DELSLOTS, ADDSLOTSRANGE, DELSLOTSRANGE`命令只修改slot归属,`不负责数据的迁移或删除`。 - `SETSLOT slot Node node`: assign a slot to a specific node ID ##### MIGRATING/IMPORTING 除了通过ADDSLOTS/DELSLOTS改变slots的归属外,slot还可以被设置为两个特殊的状态`MIGRATING`, `IMPORTING`。上述这两个特殊状态用于`migrate a hash slot from one node to another`。(由于ADDSLOTS/DELSLOTS只负责修改slot归属,并不负责数据迁移,故而在数据迁移时需要通过`MIGRATING/IMPORTING`来支持) `SETSLOT`命令则用作则如下: - `SETSLOT slot MIGRATING/IMPORTING node`:主要用于将hash slot从一个节点迁移到另一个节点 - 当slot被标记为`MIGRATING`时,代表node为迁移的`源节点`。当该节点接收到关于该hash slot的请求时: - 如果请求中的key存在,则服务该请求 - 如果请求中的key不存在,则可能已经迁移到目标节点中,此时会使用`ASK`重定向,将请求转发到迁移的`目标节点` - 当slot被设置为`IMPORTING`时,代表node为迁移的`目标节点`。该节点只在`request is preceded by an ASKING command`时,才会服务该请求 - 如果client在发送针对slot的请求前,没有先发送`ASKING`命令,那么当前请求将会通过`MOVED` redirection error被转发给`source node`(此时,数据迁移尚未完成,迁移的源节点仍然被视为`owner of the slot`) 按照上述描述,slot迁移的示例如下所示: - 假设拥有两个master nodes,分别为`A, B`,如果希望将`slot 8`从node A移动到`node B`,那么发送的命令如下所示: - 向node B发送`CLUSTER SETSLOT 8 IMPORTING A` - 该命令将node B标记为了目标节点,并指定源节点为A - 向node A发送`CLUSTER SETSLOT 8 MIGRATING B` - 该命令将目标A标记为了源节点,并指明了目标节点B 此时,节点A作为slot迁移的源节点,在迁移过程中仍然被cluster看作是`owner of hash slot`。故而,当其他节点接收到关于`迁移中slot`的请求时,仍然将其重定向到node A,此时,A对于请求的处理如下: - 如果请求的key仍在node A中存在,那么node A处理该请求 - 如果请求的key不再node A中,那么其会通过`ASK`将请求重定向到B,此时客户端首先向node B发送`ASKING`,然后再重新发送请求 > 在节点迁移过程中,不仅允许对key执行读操作,也允许对其执行写操作。例如,若key之前在集群中不存在,并且被hash到`slot 8`,此时`slot 8`正在迁移。故而,client针对key的创建操作会先被`MOVED error`重定向到源节点`node A`,`node A`中并没有该key,故而会被`ASK`重定向到目标节点`node B`。此时,`node B`会处理client的key创建请求。 > > `故而,在slot迁移的过程中,针对slot中key的创建,统一在目标节点处写入,创建操作并不由源节点处理。` ##### MIGRATE 可以通过在`redis-cli`中执行如下命令来将slot中的keys来进行迁移 ###### `CLUSTER GETKEYSINSLOT` ```redis-cli CLUSTER GETKEYSINSLOT slot count ``` 该命令将会返回指定hash slot中`count`数量的keys。对于返回的keys,可以通过`MIGRATE`命令来进行迁移,从A到B的迁移是原子的(来源和目标instance都会会locked)。 ###### MIGRATE ```redis-cli MIGRATE target_host target_port "" target_database id timeout KEYS key1 key2 ... ``` 上述命令将会连接到目标节点,发送`serialized version of the key`,并且一旦接收到`OK` code之后,源节点中key将会被删除。 > 故而,在任何时刻,对于外部client而言key要么存在于A中,要么存在于B中。 在redis cluster中,无需将database指定为`0`之外的值,此处支持`database`选项主要是为了redis cluster外的其他场景。`MIGRATE`命令经过优化,即使在移动`long lists`之类的复杂key也能保持较快的速度。 当迁移过程结束之后,`SETSLOT NODE `命令将会被发送给涉及迁移的两个节点,从而将slots重新设置回其正常状态(之前状态为MIGRATING/IMPORTING)。通常情况下,该命令也会被发送给其他所有节点,从而避免`new configuration`在集群间的传播耗费太长时间。 #### ASK redirection 在如上章节中,介绍了`MOVED`和`ASK`两种重定向方式,其区别如下: - `MOVED`响应代表hash slot永远由其他节点负责,`应该将后续的所有请求`都发送给指定节点 - `ASK`则是代表`只将下一个请求`发送给指定节点 `ASK`的重定向机制在`slot migration`过程中是很重要的。就上章节的示例而言,在`slot migration`的过程中,对`slot 8`中的key进行查找时,该key极可能在A也可能在B,故而在slot迁移的过程中,`总是应该先查询A再查询B`。 再client也要遵循上述行为,应确保slot从A迁移到B时: - 若查询slot中的key,应首先查询A节点再查询B节点 - 若slot的状态为`IMPORTING`,代表该节点为slot迁移的目标节点,client在针对该节点的`IMPORTING slot`进行查询时,应首先发送`ASKING`命令 > 基本上,`ASKING`是一个`仅一次有效`的flag,在client发送`ASKING`命令后,能够访问node的`IMPORTING slot` ASK重定向的语义如下: - 如果client接收到`ASK`重定向,那么仅将`被重定向的请求`发送给指定的目标节点,但是`后续针对该slot的节点仍应发送给原节点` - 将被重定向的请求发送给目标节点时,首先应发送`ASKING`命令 - 此时,`ASK`重定向并不会导致client本地的`slot-node map`被更新 一旦关于`slot`的迁移完成,`A`节点后续针对该`slot`的请求将返回`MOVED error`,client将永久的将`slot`映射到`B`节点。 #### Client connections and redirection handling 为了提高效率,redis cluster clients维护了一个`map of the current slot configuration`。但是,该配置并不需要实时保持最新,当client联系错误的节点时,将会触发重定向,client可以根据重定向来更新其内部slot映射。 通常,clients会在如下两种场景来拉取`slots和mapped node addresses`的完整列表: - 在client启动时,会注入初始的slots配置 - 当client接收到`MOVED`重定向时(`ASK`重定向并不会触发该行为,因为`ASK`重定向是一次性的) 在client接收到`MOVED`重定向时,可以`仅对导致重定向的slot`进行更新,但是,该更新方式通常是低效的:通常`slot configuration`是批量被修改的,并不会只修改一个slot。例如replica被提升为master后,所有old master中的slots都需要被重新映射到新master。 > 故而,在触发`MOVED`重定向时,直接拉取slots到nodes的所有映射会更加简单。 client可以发送`CLUSTER SLOTS`命令来获取`an array of slot ranges and associated master and replica nodes serving the specified ranges`。 ##### CLUSTER SLOTS 如下示例展示了`CLUSTER SLOTS`命令的使用: ```redis-cli 127.0.0.1:7000> cluster slots 1) 1) (integer) 5461 2) (integer) 10922 3) 1) "127.0.0.1" 2) (integer) 7001 4) 1) "127.0.0.1" 2) (integer) 7004 2) 1) (integer) 0 2) (integer) 5460 3) 1) "127.0.0.1" 2) (integer) 7000 4) 1) "127.0.0.1" 2) (integer) 7003 3) 1) (integer) 10923 2) (integer) 16383 3) 1) "127.0.0.1" 2) (integer) 7002 4) 1) "127.0.0.1" 2) (integer) 7005 ``` 在`CLUSTER SLOTS`返回的数组中, - 每个元素的`前两个子元素`代表`slot range`的开始位置和结束位置 - 例如`1)`中的slot range的开始位置为`5461`,结束位置为`10922` - 每个元素的`其他子元素`代表`address-port` pairs,其中,第一个address-port pair为`master serving the slot`,额外的`address-port pairs`代表`replicas serving the same slot` - 例如`1)`中指定的slot range中,master为`127.0.0.1:7001`,而replica则是`127.0.0.1:7004` - Replicas will be listed only when not in an error condition 如果cluster的配置有误,`CLUSTER SLOTS`并不保证返回的范围会覆盖所有`16384`个slots。对于该种场景,clients在初始化`slots configuration map`时,会将`未分配slots`的target node设置为`NULL`,并且当用户尝试访问的key属于这些`unassigned slots`时,会报异常。 当slot发现为unassigned时,在向调用方返回异常之前,client会尝试拉取slots configuration,并且校验cluster当前是否被正确配置。 #### Multi-keys operations 在使用`hash tags`时,client可以在集群访问下进行multi-key operations,示例如下: ```redis-cli MSET {user:1000}.name Angela {user:1000}.surname White ``` 但是,在key所属的slot正在进行`resharding`时,`multi-key` operations有可能无法被执行: - 当resharding过程中,对于`multi-key operations`,如果targeting keys都存在,并且被hash到相同的slot中(都位于source或destination node`中),那么该multi-keys operation是可执行的 - `如果targeting keys不存在,或者在resharding过程中在source node和destination node中都存在`,那么将会发生一个`TRYAGAIN` error,client可以在后续对该操作进行重试,或报告该error 当指定hash slots的迁移完成后,关于该hash slot所有的`multi-key operations`都允许被执行。