3619 lines
190 KiB
Markdown
3619 lines
190 KiB
Markdown
- [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)
|
||
- [Scaling reads using replica nodes](#scaling-reads-using-replica-nodes)
|
||
- [Fault Tolerance](#fault-tolerance)
|
||
- [Heartbeat and gossip message](#heartbeat-and-gossip-message)
|
||
|
||
|
||
# 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] [<MAXLEN | MINID> [= | ~] 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>-<sequenceNumber>
|
||
```
|
||
- `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 <key> <groupname> [[IDLE <min-idle-time>] <start-id> <end-id> <count> [<consumer-name>]]
|
||
```
|
||
如上述示例所示,可以为`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 <key> <group> <consumer> <min-idle-time> <ID-1> <ID-2> ... <ID-N>
|
||
```
|
||
XCLAIM命令的作用如下:
|
||
- 对于`<key>`和`<group>`所指定的stream和consumer group,希望将`<ID-X>`所指定的message都能更改ownership,将messages分配给`<consumer>`所指定的consumer。
|
||
- 除此之外,我们还提供了`<min-idle-time>`,仅当指定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 <key> <group> <consumer> <min-idle-time> <start> [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 = <<EOF
|
||
local i = tonumber(ARGV[1])
|
||
local res
|
||
while (i > 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 = <<EOF
|
||
local i = tonumber(ARGV[1])
|
||
local res
|
||
math.randomseed(tonumber(ARGV[2]))
|
||
while (i > 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@<db>__ prefix.
|
||
E Keyevent events, published with __keyevent@<db>__ 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 <slot> NODE <node-id>`命令将会被发送给涉及迁移的两个节点,从而将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`都允许被执行。
|
||
|
||
#### Scaling reads using replica nodes
|
||
通常来讲,replica nodes在接收到命令时,都会将其重定向给负责该hash slot的master node。但是,对于readonly命令,client可以使用replicas来对读操作进行扩展。
|
||
|
||
`READONLY`会告知redis cluster replica node如下信息:当前client`接收读取可能过时的数据`,并且`不会发送写请求`。
|
||
|
||
当connection处于readonly模式时,cluster只会在`the operation involves keys not served by the replica's master node`时,才会向client发送重定向。造成该问题的原因可能如下:
|
||
- the client sent a command about hash slots never served by the master of this replica
|
||
- the cluster was reconfigured and the replica is no longer able to serve commands for a given hash slot
|
||
|
||
当发生该场景时,replica会向client发送重定向,而client则应该更新其`hash slot map`。
|
||
|
||
### Fault Tolerance
|
||
#### Heartbeat and gossip message
|
||
redis cluster nodes会持续的交换ping和pong packets。ping packet和pong packet的结构相同,都包含configuration information。ping packet和pong packet的唯一区别为message type field。
|
||
|
||
> `ping packets和pong packets被统称为heartbeat packets`。
|
||
|
||
通常,在node发送ping packets时,将会导致receiver返回一个pong packet。但是,`node也能够在未接收到ping packets的情况下发送pong packets`,用于向其他节点发送其自身的配置信息。在对`new configuration`进行广播时,该机制十分有用。
|
||
|
||
通常,每过几秒,node都会随机ping一系列其他节点。故而,不管集群的规模如何,每个节点发送的ping packets都是恒定的。(node并不会因为集群规模扩大,向集群所有其他节点发送ping packets导致ping packets的总数上升)。
|
||
|
||
但是,every node makes sure to ping every other node that hasn't sent a ping or received a pong for longer than `NODE_TIME / 2` time。在`NODE_TIME`到期之前,node也会对其他节点尝试重新连接,确保其他节点不会因为当前tcp连接的问题而将其他节点判定为不可访问。
|
||
|
||
当`NODE_TIME`设置的较小时,消息交换会更加频繁,且当节点数量较多时,信息交换的数量可能会相当庞大。因为没过`NODE_TIME / 2`时间,每个节点都要ping所有其他节点,以获取最新的消息。
|
||
|
||
例如,在一个规模为100的集群中,如果将`node timeout`设置为60s,每个节点在每30s都会尝试发送99个ping,故而每个节点每秒钟平均发送`3.3`个ping,整个集群每秒钟平均发送`330`个ping。
|
||
|