#### 第18章:
#### Redis
Redis是一個key-value存儲系統,常被用作緩存或者消息訂閱通知系統。是一個開源的使用C語言編寫、遵守BSD協議、支持網絡、可基于內存亦可持久化的日志型、Key-Value數據庫,并提供多種語言的API。Redis支持數據持久化,支持多種數據結構的存儲,支持master-slave模式的數據備份。
##### Redis的優勢
- 性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
- 豐富的數據類型 – Redis支持二進制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 數據類型操作。
- 原子 – Redis的所有操作都是原子性的,意思就是要么成功執行要么失敗完全不執行。單個操作是原子性的。多個操作也支持事務,即原子性,通過MULTI和EXEC指令包起來,但是不支持回滾(據說是作者一開始就這么設計為了純粹的性能)。
- 豐富的特性 – Redis還支持 publish/subscribe, 通知, key 過期等等特性。
#### 18.1 安裝Redis
##### Redis 的安裝
Linux環境,下載地址:<http://redis.io/download>,下載最新穩定版本并安裝:
```
# wget http://download.redis.io/releases/redis-6.0.8.tar.gz
# tar xzf redis-6.0.8.tar.gz
# cd redis-6.0.8
# make
```
執行完 make 命令后,redis-6.0.8 的 src 目錄下會出現編譯后的 redis 服務程序 redis-server,還有用于測試的客戶端程序 redis-cli:
下面啟動 redis 服務:
```
# cd src
# ./redis-server
```
注意這種方式啟動 redis 使用的是默認配置。也可以通過啟動參數告訴 redis 使用指定配置文件使用下面命令啟動。
```
# cd src
# ./redis-server ../redis.conf
```
**redis.conf** 是一個默認的配置文件。我們可以根據需要使用自己的配置文件。
啟動 redis 服務進程后,就可以使用測試客戶端程序 redis-cli 和 redis 服務交互了。 比如:
```
# cd src
# ./redis-cli
redis> set foo bar
OK
redis> get foo
"bar"
```
#### 18.2 Redis 的配置文件
redis.conf 配置項說明如下:
| 配置項 | 說明 |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| `daemonize no` | Redis 默認不是以守護進程的方式運行,可以通過該配置項修改,使用 yes 啟用守護進程(Windows 不支持守護線程的配置為 no ) |
| `pidfile /var/run/redis.pid` | 當 Redis 以守護進程方式運行時,Redis 默認會把 pid 寫入 /var/run/redis.pid 文件,可以通過 pidfile 指定 |
| `port 6379` | 指定 Redis 監聽端口,默認端口為 6379,作者在自己的一篇博文中解釋了為什么選用 6379 作為默認端口,因為 6379 在手機按鍵上 MERZ 對應的號碼,而 MERZ 取自意大利歌女 Alessia Merz 的名字 |
| `bind 127.0.0.1` | 綁定的主機地址 |
| `timeout 300` | 當客戶端閑置多長秒后關閉連接,如果指定為 0 ,表示關閉該功能 |
| `loglevel notice` | 指定日志記錄級別,Redis 總共支持四個級別:debug、verbose、notice、warning,默認為 notice |
| `logfile stdout` | 日志記錄方式,默認為標準輸出,如果配置 Redis 為守護進程方式運行,而這里又配置為日志記錄方式為標準輸出,則日志將會發送給 /dev/null |
| `databases 16` | 設置數據庫的數量,默認數據庫為0,可以使用SELECT 命令在連接上指定數據庫id |
| `save <seconds> <changes>`Redis 默認配置文件中提供了三個條件:save 900 1 ,save 300 10,save 60 10000分別表示 900 秒(15 分鐘)內有 1 個更改,300 秒(5 分鐘)內有 10 個更改以及 60 秒內有 10000 個更改。 | 用于Redis的AOF持久化,指定在多長時間內,有多少次更新操作,就將數據同步到數據文件,可以多個條件配合 |
| `rdbcompression yes` | 指定存儲至本地數據庫時是否壓縮數據,默認為 yes,Redis 采用 LZF 壓縮,如果為了節省 CPU 時間,可以關閉該選項,但會導致數據庫文件變的巨大 |
| `dbfilename dump.rdb` | 指定本地數據庫文件名,默認值為 dump.rdb |
| `dir ./` | 指定本地數據庫存放目錄 |
| `slaveof <masterip> <masterport>` | 設置當本機為 slave 服務時,設置 master 服務的 IP 地址及端口,在 Redis 啟動時,它會自動從 master 進行數據同步 |
| `masterauth <master-password>` | 當 master 服務設置了密碼保護時,slav 服務連接 master 的密碼 |
| `requirepass foobared` | 設置 Redis 連接密碼,如果配置了連接密碼,客戶端在連接 Redis 時需要通過 AUTH <password> 命令提供密碼,默認關閉 |
| ` maxclients 128` | 設置同一時間最大客戶端連接數,默認無限制,Redis 可以同時打開的客戶端連接數為 Redis 進程可以打開的最大文件描述符數,如果設置 maxclients 0,表示不作限制。當客戶端連接數到達限制時,Redis 會關閉新的連接并向客戶端返回 max number of clients reached 錯誤信息 |
| `maxmemory <bytes>` | 指定 Redis 最大內存限制,Redis 在啟動時會把數據加載到內存中,達到最大內存后,Redis 會先嘗試清除已到期或即將到期的 Key,當此方法處理 后,仍然到達最大內存設置,將無法再進行寫入操作,但仍然可以進行讀取操作。Redis 新的 vm 機制,會把 Key 存放內存,Value 會存放在 swap 區 |
| `appendonly no` | 指定是否在每次更新操作后進行日志記錄,Redis 在默認情況下是異步的把數據寫入磁盤,如果不開啟,可能會在斷電時導致一段時間內的數據丟失。因為 redis 本身同步數據文件是按上面 save 條件來同步的,所以有的數據會在一段時間內只存在于內存中。默認為 no |
| `appendfilename appendonly.aof` | 指定更新日志文件名,默認為 appendonly.aof |
| `appendfsync everysec` | 指定更新日志條件,共有 3 個可選值:**no**:表示等操作系統進行數據緩存同步到磁盤(快)**always**:表示每次更新操作后手動調用 fsync() 將數據寫到磁盤(慢,安全)**everysec**:表示每秒同步一次(折中,默認值) |
| `vm-enabled no` | 指定是否啟用虛擬內存機制,默認值為 no,簡單的介紹一下,VM 機制將數據分頁存放,由 Redis 將訪問量較少的頁即冷數據 swap 到磁盤上,訪問多的頁面由磁盤自動換出到內存中 |
| `vm-swap-file /tmp/redis.swap` | 虛擬內存文件路徑,默認值為 /tmp/redis.swap,不可多個 Redis 實例共享 |
| `vm-max-memory 0` | 將所有大于 vm-max-memory 的數據存入虛擬內存,無論 vm-max-memory 設置多小,所有索引數據都是內存存儲的(Redis 的索引數據 就是 keys),也就是說,當 vm-max-memory 設置為 0 的時候,其實是所有 value 都存在于磁盤。默認值為 0 |
| `vm-page-size 32` | Redis swap 文件分成了很多的 page,一個對象可以保存在多個 page 上面,但一個 page 上不能被多個對象共享,vm-page-size 是要根據存儲的 數據大小來設定的,作者建議如果存儲很多小對象,page 大小最好設置為 32 或者 64bytes;如果存儲很大大對象,則可以使用更大的 page,如果不確定,就使用默認值 |
| `vm-pages 134217728` | 設置 swap 文件中的 page 數量,由于頁表(一種表示頁面空閑或使用的 bitmap)是在放在內存中的,,在磁盤上每 8 個 pages 將消耗 1byte 的內存。 |
| `vm-max-threads 4` | 設置訪問swap文件的線程數,最好不要超過機器的核數,如果設置為0,那么所有對swap文件的操作都是串行的,可能會造成比較長時間的延遲。默認值為4 |
| `glueoutputbuf yes` | 設置在向客戶端應答時,是否把較小的包合并為一個包發送,默認為開啟 |
| `hash-max-zipmap-entries 64 hash-max-zipmap-value 512` | 指定在超過一定的數量或者最大的元素超過某一臨界值時,采用一種特殊的哈希算法 |
| `activerehashing yes` | 指定是否激活重置哈希,默認為開啟(后面在介紹 Redis 的哈希算法時具體介紹) |
| `include /path/to/local.conf` | 指定包含其它的配置文件,可以在同一主機上多個Redis實例之間使用同一份配置文件,而同時各個實例又擁有自己的特定配置文件 |
#### 18.3 Redis 數據類型
Redis有五種數據l類型:
- 字符串
- 哈希
- 列表
- 集合
- 有序集合
##### String(字符串)
String是Redis最基本的數據類型,和memcached一樣的數據類型,一個key對應一個value。
string 類型是二進制安全的。意思是 redis 的 string 可以包含任何數據。比如jpg圖片或者序列化的對象。
string 類型是 Redis 最基本的數據類型,string 類型的值最大能存儲 512MB。
例子:
```
redis 127.0.0.1:6379> SET msg1 "學習Redis"
OK
redis 127.0.0.1:6379> GET msg1
"學習Redis"
```
將key設置為str1使用SET命令。
下表列出了常用的 redis 字符串命令:
| 命令及描述 |
| ------------------------------------------------------------ |
| SET key value 設置指定 key 的值 |
| GET key 獲取指定 key 的值。 |
| GETRANGE key start end 返回 key 中字符串值的子字符 |
| GETSET key value 將給定 key 的值設為 value ,并返回 key 的舊值(old value)。 |
| GETBIT key offset 對 key 所儲存的字符串值,獲取指定偏移量上的位(bit)。 |
| [MGET key1 key2..\] 獲取所有(一個或多個)給定 key 的值。 |
| SETBIT key offset value 對 key 所儲存的字符串值,設置或清除指定偏移量上的位(bit)。 |
| SETEX key seconds value 將值 value 關聯到 key ,并將 key 的過期時間設為 seconds (以秒為單位)。 |
| SETNX key value 只有在 key 不存在時設置 key 的值。 |
| SETRANGE key offset value 用 value 參數覆寫給定 key 所儲存的字符串值,從偏移量 offset 開始。 |
| STRLEN key 返回 key 所儲存的字符串值的長度。 |
| [MSET key value key value ...\] 同時設置一個或多個 key-value 對。 |
| [MSETNX key value key value ...\] 同時設置一個或多個 key-value 對,當且僅當所有給定 key 都不存在。 |
| PSETEX key milliseconds value 這個命令和 SETEX 命令相似,但它以毫秒為單位設置 key 的生存時間,而不是像 SETEX 命令那樣,以秒為單位。 |
| INCR key 將 key 中儲存的數字值增一。 |
| INCRBY key increment 將 key 所儲存的值加上給定的增量值(increment) 。 |
| INCRBYFLOAT key increment 將 key 所儲存的值加上給定的浮點增量值(increment) 。 |
| DECR key 將 key 中儲存的數字值減一。 |
| DECRBY key decrement key 所儲存的值減去給定的減量值(decrement) 。 |
| APPEND key value 如果 key 已經存在并且是一個字符串, APPEND 命令將指定的 value 追加到該 key 原來值(value)的末尾。 |
##### Hash(哈希)
Redis hash 是一個鍵值(key=>value)對集合。
Redis hash 是一個 string 類型的 field 和 value 的映射表,hash 特別適合用于存儲對象。
例子:
```
redis 127.0.0.1:6379> HMSET ob k1 "100" k2 "200"
"OK"
redis 127.0.0.1:6379> HGET ob k1
"100"
redis 127.0.0.1:6379> HGET ob k2
"200"
```
這里設置了一個哈希對象ob。其中k1的值為100,k2的值為200。
我們通過HMSET設置了兩個` field=>value`對, HGET 獲取對應 field對應的 value
每個 hash 可以存儲 2的32次方-1個 鍵值對(40多億)。
下表列出了 redis hash 基本的相關命令:
| 命令及描述 |
| ------------------------------------------------------------ |
| [HDEL key field1 field2\] 刪除一個或多個哈希表字段 |
| HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。 |
| HGET key field 獲取存儲在哈希表中指定字段的值。 |
| HGETALL key 獲取在哈希表中指定 key 的所有字段和值 |
| HINCRBY key field increment 為哈希表 key 中的指定字段的整數值加上增量 increment 。 |
| HINCRBYFLOAT key field increment 為哈希表 key 中的指定字段的浮點數值加上增量 increment 。 |
| HKEYS key 獲取所有哈希表中的字段 |
| HLEN key 獲取哈希表中字段的數量 |
| [HMGET key field1 field2\] 獲取所有給定字段的值 |
| [HMSET key field1 value1 field2 value2 \] 同時將多個 field-value (域-值)對設置到哈希表 key 中。 |
| HSET key field value 將哈希表 key 中的字段 field 的值設為 value 。 |
| HSETNX key field value 只有在字段 field 不存在時,設置哈希表字段的值。 |
| HVALS key 獲取哈希表中所有值。 |
| HSCAN key cursor [MATCH pattern\] [COUNT count] 迭代哈希表中的鍵值對。 |
##### List(列表或稱為隊列)
Redis 列表是簡單的字符串列表,按照插入順序排序。你可以添加一個元素到列表的頭部或者尾部。
例子:
```
redis 127.0.0.1:6379> lpush list1 1
(integer) 1
redis 127.0.0.1:6379> lpush list1 2
(integer) 2
redis 127.0.0.1:6379> lpush list1 3
(integer) 3
redis 127.0.0.1:6379> lrange list1 0 10
1) "3"
2) "2"
3) "1"
redis 127.0.0.1:6379>
```
上面我們使用lpush命令向list1隊列左邊插入數據,如果沒有list1時會自動創建list1隊列對象。lrange命令按照偏移獲取隊列數據。
下表列出了列表相關的基本命令:
| 命令及描述 |
| ------------------------------------------------------------ |
| [BLPOP key1 key2 \] timeout 移出并獲取列表的第一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止。 |
| [BRPOP key1 key2 \] timeout 移出并獲取列表的最后一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止。 |
| BRPOPLPUSH source destination timeout 從列表中彈出一個值,將彈出的元素插入到另外一個列表中并返回它; 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素為止。 |
| LINDEX key index 通過索引獲取列表中的元素 |
| LINSERT key BEFORE\|AFTER pivot value 在列表的元素前或者后插入元素 |
| LLEN key 獲取列表長度 |
| LPOP key 移出并獲取列表的第一個元素 |
| [LPUSH key value1 value2\] 將一個或多個值插入到列表頭部 |
| LPUSHX key value 將一個值插入到已存在的列表頭部 |
| LRANGE key start stop 獲取列表指定范圍內的元素 |
| LREM key count value 移除列表元素 |
| LSET key index value 通過索引設置列表元素的值 |
| LTRIM key start stop 對一個列表進行修剪(trim),就是說,讓列表只保留指定區間內的元素,不在指定區間之內的元素都將被刪除。 |
| RPOP key 移除列表的最后一個元素,返回值為移除的元素。 |
| RPOPLPUSH source destination 移除列表的最后一個元素,并將該元素添加到另一個列表并返回 |
| [RPUSH key value1 value2\] 在列表中添加一個或多個值 |
| RPUSHX key value 為已存在的列表添加值 |
#### 集合Set
Redis 的 Set 是 string 類型的無序集合。
集合是通過哈希表實現的,所以添加,刪除,查找的復雜度都是 O(1)。哈希表(hashtable)是利用哈希函數將特定的鍵映射到特定的值得一種數據結構,它維護著鍵和值的一一對應的關系,并且可以根據鍵快速檢索到值,查詢效率為O(1)。Redis利用哈希函數和連地址法將裝有集合數據的bucket塊通過鏈表鏈接在一起并解決了哈希沖突。
sadd 命令
添加一個 string 元素到 key 對應的 set 集合中,成功返回 1,如果元素已經在集合中返回 0。
```
sadd key member
```
例子:
```
redis 127.0.0.1:6379> DEL runoob
redis 127.0.0.1:6379> sadd runoob redis
(integer) 1
redis 127.0.0.1:6379> sadd runoob mongodb
(integer) 1
redis 127.0.0.1:6379> sadd runoob rabbitmq
(integer) 1
redis 127.0.0.1:6379> sadd runoob rabbitmq
(integer) 0
redis 127.0.0.1:6379> smembers runoob
1) "redis"
2) "rabbitmq"
3) "mongodb"
```
rabbitmq 添加了兩次,但根據集合內元素的唯一性,第二次插入的元素將被忽略。
集合中最大的成員數為 2的32次方-1個(4294967295, 每個集合可存儲40多億個成員)。
下表列出了 Redis 集合基本命令:
| 命令及描述 |
| ------------------------------------------------------------ |
| [SADD key member1 member2\] 向集合添加一個或多個成員 |
| SCARD key 獲取集合的成員數 |
| [SDIFF key1 key2\] 返回第一個集合與其他集合之間的差異。 |
| [SDIFFSTORE destination key1 key2\] 返回給定所有集合的差集并存儲在 destination 中 |
| [SINTER key1 key2\] 返回給定所有集合的交集 |
| [SINTERSTORE destination key1 key2\] 返回給定所有集合的交集并存儲在 destination 中 |
| SISMEMBER key member 判斷 member 元素是否是集合 key 的成員 |
| SMEMBERS key 返回集合中的所有成員 |
| SMOVE source destination member 將 member 元素從 source 集合移動到 destination 集合 |
| SPOP key 移除并返回集合中的一個隨機元素 |
| [SRANDMEMBER key count\] 返回集合中一個或多個隨機數 |
| [SREM key member1 member2\] 移除集合中一個或多個成員 |
| [SUNION key1 key2\] 返回所有給定集合的并集 |
| [SUNIONSTORE destination key1 key2\] 所有給定集合的并集存儲在 destination 集合中 |
| SSCAN key cursor [MATCH pattern\] [COUNT count] 迭代集合中的元素 |
#### zset(sorted set:有序集合)
Redis zset 和 set 一樣也是string類型元素的集合,且不允許重復的成員。有序集合底層的數據結構是跳躍表。
不同的是每個元素都會關聯一個double類型的分數。redis正是通過分數來為集合中的成員進行從小到大的排序。
zset的成員是唯一的,但分數(score)卻可以重復。
### zadd 命令
添加元素到集合,元素在集合中存在則更新對應score
```
zadd key score member
```
例子:
```
redis 127.0.0.1:6379> DEL runoob
redis 127.0.0.1:6379> zadd runoob 0 redis
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 mongodb
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 rabbitmq
(integer) 1
redis 127.0.0.1:6379> zadd runoob 0 rabbitmq
(integer) 0
redis 127.0.0.1:6379> ZRANGEBYSCORE runoob 0 1000
1) "mongodb"
2) "rabbitmq"
3) "redis"
```
下表列出了 redis 有序集合的基本命令:
| 命令及描述 |
| ------------------------------------------------------------ |
| [ZADD key score1 member1 score2 member2\] 向有序集合添加一個或多個成員,或者更新已存在成員的分數 |
| ZCARD key 獲取有序集合的成員數 |
| ZCOUNT key min max 計算在有序集合中指定區間分數的成員數 |
| ZINCRBY key increment member 有序集合中對指定成員的分數加上增量 increment |
| [ZINTERSTORE destination numkeys key key ...\] 計算給定的一個或多個有序集的交集并將結果集存儲在新的有序集合 destination 中 |
| ZLEXCOUNT key min max 在有序集合中計算指定字典區間內成員數量 |
| [ZRANGE key start stop WITHSCORES\] 通過索引區間返回有序集合指定區間內的成員 |
| [ZRANGEBYLEX key min max LIMIT offset count\] 通過字典區間返回有序集合的成員 |
| ZRANGEBYSCORE key min max [WITHSCORES\] [LIMIT] 通過分數返回有序集合指定區間內的成員 |
| ZRANK key member 返回有序集合中指定成員的索引 |
| [ZREM key member member ...\] 移除有序集合中的一個或多個成員 |
| ZREMRANGEBYLEX key min max 移除有序集合中給定的字典區間的所有成員 |
| ZREMRANGEBYRANK key start stop 移除有序集合中給定的排名區間的所有成員 |
| ZREMRANGEBYSCORE key min max 移除有序集合中給定的分數區間的所有成員 |
| [ZREVRANGE key start stop WITHSCORES\] 返回有序集中指定區間內的成員,通過索引,分數從高到低 |
| [ZREVRANGEBYSCORE key max min WITHSCORES\] 返回有序集中指定分數區間內的成員,分數從高到低排序 |
| ZREVRANK key member 返回有序集合中指定成員的排名,有序集成員按分數值遞減(從大到小)排序 |
| ZSCORE key member 返回有序集中,成員的分數值 |
| [ZUNIONSTORE destination numkeys key key ...\] 計算給定的一個或多個有序集的并集,并存儲在新的 key 中 |
| ZSCAN key cursor [MATCH pattern\] [COUNT count] 迭代有序集合中的元素(包括元素成員和元素分值) |
#### 18.4 命令
Redis 命令用于在 redis 服務上執行操作。
要在 redis 服務上執行命令需要一個 redis 客戶端,并啟動客戶端。
啟動 redis 服務器,打開終端并輸入命令`redis-cli`,該命令會連接本地的 redis 服務。
```
$ redis-cli
redis 127.0.0.1:6379>
redis 127.0.0.1:6379> PING
PONG
```
我們連接到本地的 redis 服務并執行 **PING** 命令,該命令用于檢測 redis 服務是否啟動。
如果需要在遠程 redis 服務上執行命令,同樣我們使用的也是redis-cli命令。
```
$redis-cli -h 127.0.0.1 -p 6379 -a "mypass"
redis 127.0.0.1:6379>
redis 127.0.0.1:6379> PING
PONG
```
##### Redis 鍵(key)
Redis 鍵命令用于管理 redis 的鍵。
Redis 鍵相關的基本命令:
| 命令及描述 |
| ------------------------------------------------------------ |
| DEL key該命令用于在 key 存在時刪除 key。 |
| DUMP key序列化給定 key ,并返回被序列化的值。 |
| EXISTS key檢查給定 key 是否存在。 |
| EXPIRE keyseconds 為給定 key 設置過期時間,以秒計。 |
| EXPIREAT key timestamp EXPIREAT 的作用和 EXPIRE 類似,都用于為 key 設置過期時間。 不同在于 EXPIREAT 命令接受的時間參數是 UNIX 時間戳(unix timestamp)。 |
| PEXPIRE key milliseconds設置 key 的過期時間以毫秒計。 |
| PEXPIREAT key milliseconds-timestamp 設置 key 過期時間的時間戳(unix timestamp) 以毫秒計 |
| KEYS pattern查找所有符合給定模式( pattern)的 key 。 |
| MOVE key db 將當前數據庫的 key 移動到給定的數據庫 db 當中。 |
| PERSIST key 移除 key 的過期時間,key 將持久保持。 |
| PTTL key以毫秒為單位返回 key 的剩余的過期時間。 |
| TTL key以秒為單位,返回給定 key 的剩余生存時間(TTL, time to live)。 |
| RANDOMKEY從當前數據庫中隨機返回一個 key 。 |
| RENAME key newkey修改 key 的名稱 |
| RENAMENX key newkey僅當 newkey 不存在時,將 key 改名為 newkey 。 |
| SCAN cursor [MATCH pattern\] [COUNT count] 迭代數據庫中的數據庫鍵。Redis的多種存儲對象根據存儲的量以及所占空間大小的不同可以手動激活或者自動激活其底層數據結構和數據類型的變化來提高性能和減少空間使用量。 |
| TYPE key 返回 key 所儲存的值的類型。 |
#### 18.5 事務
Redis 事務可以一次執行多個命令, 并且帶有以下三個重要的保證:
- 批量操作在發送 EXEC 命令前被放入隊列緩存。
- 收到 EXEC 命令后進入事務執行,事務中任意命令執行失敗,其余的命令依然被執行。
- 在事務執行過程,其他客戶端提交的命令請求不會插入到事務執行命令序列中。
一個事務從開始到執行會經歷以下三個階段:
- 開始事務。
- 命令入隊。
- 執行事務。
注意:
- Redis的事務不會回滾。設計者設計之初希望Redis是純粹的性能強悍的NoSQL數據庫。
- Redis的事務不支持原子性(要么全部執行成功,要么錯一條全部不成功),但是在將命令加入事務隊列時,Redis會檢測事務中每一條命令是否錯誤,如果發現語法錯誤、命令不存在或命令參數不對則會返回錯誤給客戶端并修改客戶端狀態,當輸入事務執行令EXEC時,Redis服務器會拒絕執行此事務。但是Redis不支持檢測程序員自己造成的邏輯錯誤。Redis事務隊列中命令語法正確的前提下,由程序員自己造成的邏輯錯誤,甚至數據與數據類型異常的情況中,會在異常的那條命令會輸出錯誤后執行之后的命令。
下表列出了 redis 事務的相關命令:
| 命令及描述 |
| ------------------------------------------------------------ |
| DISCARD 取消事務,放棄執行事務塊內的所有命令。 |
| EXEC 執行所有事務塊內的命令。 |
| MULTI 標記一個事務塊的開始。 |
| UNWATCH 取消 WATCH 命令對所有 key 的監視。 |
| [WATCH key key ...\] 監視一個(或多個) key ,如果在事務執行之前這個(或這些) key 被其他命令所改動,那么事務將被打斷。 |
例子:
```
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET str1 "1"
QUEUED
redis 127.0.0.1:6379> GET str1
QUEUED
redis 127.0.0.1:6379> SADD tag "1 "2" "3"
QUEUED
redis 127.0.0.1:6379> SMEMBERS tag
QUEUED
redis 127.0.0.1:6379> EXEC
1) OK
2) "1"
3) (integer) 3
4) 1) "2"
2) "1"
3) "3"
```
#### 18.6 Redis 腳本
Redis支持使用Lua語言寫的腳本。有興趣的同學可以自行學習Lua語言。
例子:
```
redis 127.0.0.1:6379> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
```
#### 18.7 Redis 發布訂閱
Redis 發布訂閱 (pub/sub) 是一種消息通信模式:發送者 (pub) 發送消息,訂閱者 (sub) 接收消息。
Redis 客戶端可以訂閱任意數量的頻道。
頻道 channel1與客戶端之間的關系:
1. 首先客戶端控制服務端創建頻道channel1。
2. 一個或者多個客戶端client保持連接并訂閱了(subscribe)一個頻道channel1。
:-: 
?
1. 當有客戶端使用PUBLISH命令發送給頻道channel1時, 這個消息會被發送給訂閱了它的一個或多個客戶端client。
:-: 
例子:
首先在第一個客戶端,我們創建并訂閱一個頻道。
```
redis 127.0.0.1:6379> SUBSCRIBE test1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redisChat"
3) (integer) 1
```
然后我們打開另外一個Redis客戶端向這個頻道發送信息。
```
redis 127.0.0.1:6379> PUBLISH test1 "111"
(integer) 1
redis 127.0.0.1:6379> PUBLISH test1 "222"
(integer) 1
# 訂閱者的客戶端會顯示如下消息
1) "message"
2) "test1"
3) "111"
1) "message"
2) "test1"
3) "222"
```
下表列出了 redis 發布訂閱常用命令:
| 命令及描述 |
| ------------------------------------------------------------ |
| [PSUBSCRIBE pattern pattern ...\] 訂閱一個或多個符合給定模式的頻道。 |
| PUBSUB subcommand [argument [argument ...\]] 查看訂閱與發布系統狀態。 |
| PUBLISH channel message 將信息發送到指定的頻道。 |
| PUNSUBSCRIBE [pattern [pattern ...\]] 退訂所有給定模式的頻道。 |
| [SUBSCRIBE channel channel ...\] 訂閱給定的一個或多個頻道的信息。 |
| UNSUBSCRIBE [channel [channel ...\]] 指退訂給定的頻道。 |
##### PHP利用發布訂閱實現即時通信
這里使用了PHP的動態擴展:Redis擴展。
```
<?php
//發布
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$message='新年快樂';
$ret=$redis->publish('中央廣播電臺',$message);
?>
```
客戶端1向頻道"中央廣播電臺"發送信息"新年快樂"。
```
<?php
//訂閱
ini_set('default_socket_timeout', -1); //不超時
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$result=$redis->subscribe(array('中央廣播電臺'), 'callback');
function callback($instance,$channelName,$message){
echo $message;
}
?>
```
另一個保持連接的客戶端2由于訂閱了頻道"中央廣播電臺"而執行回調函數,echo輸出上一個客戶端發布的消息"新年快樂"。而且不論客戶端1在任何時候在頻道"中央廣播電臺"發布任何消息,客戶端2都會實時接收到頻道傳來的信息并輸出。
#### 18.8 Redis Stream
Redis Stream 是 Redis 5.0 版本新增加的數據結構。
Redis Stream 主要用于消息隊列,Redis 本身是有一個 Redis 發布訂閱 (pub/sub) 來實現消息隊列的功能,但它有個缺點就是消息無法持久化,如果出現網絡斷開、Redis 宕機等,消息就會被丟棄。
簡單來說發布訂閱 (pub/sub) 可以分發消息,但無法記錄歷史消息。
而 Redis Stream 提供了消息的持久化和主備復制功能,可以讓任何客戶端訪問任何時刻的數據,并且能記住每一個客戶端的訪問位置,還能保證消息不丟失。
Redis Stream 的結構如下所示,它有一個消息鏈表,將所有加入的消息都串起來,每個消息都有一個唯一的 ID 和對應的內容:
:-: 
每個 Stream 都有唯一的名稱,它就是 Redis 的 key,在我們首次使用 xadd 指令追加消息時自動創建。
上圖解析:
- **Consumer Group** :消費組,使用 XGROUP CREATE 命令創建,一個消費組有多個消費者(Consumer)。
- **last_delivered_id** :游標,每個消費組會有個游標 last_delivered_id,任意一個消費者讀取了消息都會使游標 last_delivered_id 往前移動。
- **pending_ids** :消費者(Consumer)的狀態變量,作用是維護消費者的未確認的 id。 pending_ids 記錄了當前已經被客戶端讀取的消息,但是還沒有 ack (Acknowledge character:確認字符)。
Redis Stream 區別于RabbitMQ等消息隊列軟件的幾點:
- Redis Stream的每條消息只能由一個消費者讀取,其他消費者不可讀取。而RabbitMQ可以按照策略將一條消息發送給多個消費者。
- Redis Stream是主動讀取。RabbitMQ是由軟件按照策略、算法、機制自動分發。
**消息隊列相關命令:**
- **XADD** - 添加消息到末尾
- **XTRIM** - 對流進行修剪,限制長度
- **XDEL** - 刪除消息
- **XLEN** - 獲取流包含的元素數量,即消息長度
- **XRANGE** - 獲取消息列表,會自動過濾已經刪除的消息
- **XREVRANGE** - 反向獲取消息列表,ID 從大到小
- **XREAD** - 以阻塞或非阻塞方式獲取消息列表
**消費者組相關命令:**
- **XGROUP CREATE** - 創建消費者組
- **XREADGROUP GROUP** - 讀取消費者組中的消息
- **XACK** - 將消息標記為"已處理"
- **XGROUP SETID** - 為消費者組設置新的最后遞送消息ID
- **XGROUP DELCONSUMER** - 刪除消費者
- **XGROUP DESTROY** - 刪除消費者組
- **XPENDING** - 顯示待處理消息的相關信息
- **XCLAIM** - 轉移消息的歸屬權
- **XINFO** - 查看流和消費者組的相關信息;
- **XINFO GROUPS** - 打印消費者組的信息;
- **XINFO STREAM** - 打印流信息
例子:
使用了Redis擴展。
1. 這里先創建一個鏈接,給一個stream里面添加數據。
```
$streamKey = 'test:stream:queue';
$redis = new \Redis();
$redis->connect('127.0.0.1');
for ($i = 0; $i < 100; $i++) {
/**
* 隊列名
* *: 表示由Redis自己生成消息ID:規則為[毫秒時間戳+自增數]
* 存儲的數據
*/
$xAddResult = $redis->xAdd($streamKey, '*', ['field-'.$i => 'value:'.$i*2]);
}
```
1. 刪除隊列中的某一條消息。
```
/**
* 刪除消息
* 隊列名
* 消息ID
*/
$xDelResult = $redis->xDel($streamKey, ['1609131229884-0']);
```
1. 查看隊列中的消息。
```
/**
* 取出所有的消息
* 隊列名
* 消息開始ID: - 不限制開始ID
* 消息結束ID: + 表示不限制
*/
$streamResult = $redis->xRange($streamKey, '-', '+');
```
1. 此時如果要消費隊列中的消息,需要先創建一個group與隊列關聯起來,才可以消費隊列中的消息。
```
/**
* 創建一個消費組
* 操作類型:['HELP', 'SETID', 'DELGROUP', 'CREATE', 'DELCONSUMER']
* 隊列名
* 消費者 : 這個時候自己隨便起名字就可以
* 消息ID : 0 表示從頭開始 $ 表示不接收老的消息
*/
$xGroupResult = $redis->xGroup('CREATE', $streamKey, $streamKey.':group_1', 0);
```
1. 獲取隊列中的消息。
```
/**
* group
* 消費者
* [隊列名 => '>' : 特殊>ID,這意味著使用者只想接收從未傳遞給任何其他使用者的消息。這只是意味著,給我新消息。
* 隊列名 => '0' : 任何其他ID(即0或任何其他有效ID或不完整的ID(僅毫秒時間部分))將具有以下效果:返回正在等待用戶發送的ID大于提供的ID的命令的條目。因此,基本上,如果ID不是>,那么該命令將只允許客戶端訪問其掛起的條目:傳遞給它的消息,但尚未確認。]
* 一次性取多少條消息
*/
$xReadGroupResult = $redis->xReadGroup($streamKey.':group_1', 'consumerA', [$streamKey => '>'], 1);
```
1. 從消費者組內讀取消息并處理完成后,需確認該條消息已處理。
```
/**
* 確認消息已處理
* 隊列名
* 消費者組
* 消息ID
*/
$xAckResult = $redis->xAck($streamKey, $streamKey.':group_1', ['1609131229885-0']);
```
1. 用來獲消費組或消費內消費者的未處理完畢的消息。
```
/**
* 用來獲消費組或消費內消費者的未處理完畢的消息。
* 隊列名
* 消費者組
*/
$pendingResult = $redis->xPending($streamKey, $streamKey.':group_1');
```