Files
rikako-note/中间件/redis/redis.md
2025-09-05 11:14:22 +08:00

592 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [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
## 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超过5skey已经不存在。
上述示例中,`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
相比于pipeliningscripting可以在`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将返回错误。