Redis 高級數據結構操作和其它特性
第一篇:go-redis使用,介紹Redis基本數據結構和其他特性,以及 go-redis 連接到Redis
https://www.cnblogs.com/jiujuan/p/17207166.html
第二篇:go-redis使用,Redis5種基本數據類型操作
https://www.cnblogs.com/jiujuan/p/17215125.html
第三篇:go-redis使用,Redis高級數據結構和其它特性
https://www.cnblogs.com/jiujuan/p/17231723.html
一、Redis的新數據類型
在redis中,後天添加了幾個比較高級的數據類型 hyperloglog基數統計、GEO存儲地理位置、bitmap位圖、stream爲消息隊列設計的數據類型 這 4 種數據類型。
HyperLogLog類型
HyperLogLog簡介
HyperLogLog 是一種用於數據統計的集合類型,叫基數統計。它有點類似布隆過濾器的算法。
比如說 Google 要計算用戶執行不同搜索的數量,這種統計量肯定很大,精確計算話需要消耗大量內存空間來計算。但是如果我們不要求計算精確的數量,而是大致的數量,就可以用 HyperLogLog 這種近似算法來計算集合中的不同元素數量,它可以去重。雖然這種算法不能算出精確數量值,但計算的值也是八九不離十,且佔用內存空間少很多。
統計大量數據的 UV、PV 就可以用這個數據類型。
hyperloglog 的命令文檔:
- https://redis.io/commands/?group=hyperloglog,官方文檔
- https://redis.io/docs/data-types/hyperloglogs/
- http://redisdoc.com/hyperloglog/index.html,參考文檔
命令介紹
hyperloglog 常用命令:
- PFADD:PFADD key element [element …],將任意數量的元素添加到指定 hyperloglog 中
- PFCOUNT:PFCOUNT key [key ...],如果是單個鍵,返回給定鍵在hyperloglog中的近似值,不存在則返回 0;如果是多個鍵,返回給定hyperloglog的並集的近似值。
- PFMERGE:PFMERGE destkey sourcekey [sourcekey …],將多個hyperloglog合併爲一個hyperloglog,合併後近似值爲並集
代碼例子
代碼例子,官方的一個例子,hll/main.go,改一點:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
// 設置hyperloglog的鍵myset
for i := 0; i < 10; i++ {
if err := rdb.PFAdd(ctx, "myset", fmt.Sprint(i)).Err(); err != nil {
panic(err)
}
}
ctx = context.Background()
//PFCount, 返回hyperloglog的近似值
card, err := rdb.PFCount(ctx, "myset").Result()
if err != nil {
panic(err)
}
fmt.Println("PFCount: ", card)
// PFMerge,合併2個hyperloglog
for i := 0; i < 10; i++ {
if err = rdb.PFAdd(ctx, "myset2", fmt.Sprintf("val%d", i)).Err(); err != nil {
panic(err)
}
}
rdb.PFMerge(ctx, "mergeset", "myset", "myset2")
card, _ = rdb.PFCount(ctx, "mergeset").Result()
fmt.Println("merge: ", card)
}
/*output:
PFCount: 10
merge: 20
*/
GEO地理位置空間索引
GEO簡介
GEO(Geospatial) 主要用於存儲地理位置信息,存儲地理位置經緯度信息。我們點餐用的 APP 就會用到地理位置信息服務。這些都是 屬於 LBS(Location-Based Service) 地理位置信息服務。
geospatial index 地理位置空間索引,可以用經緯度查詢彼此之間距離,範圍大小等。
GEO 命令文檔:
- https://redis.io/commands/?group=geo,官方文檔
- https://redis.io/docs/data-types/geospatial/
- http://redisdoc.com/geo/index.html,參考文檔
命令介紹
GEO 常用命令:
- GEOADD:將緯度、經度、名字添加到指定的鍵裏
- GEORADIUS:以給定的經緯度爲中心,返回鍵包含的位置元素中,與中心的距離不超過給定最大距離的所有位置元素。在Redis6.2.0 廢棄
- GEOPOS:GEOPOS key [member [member ...]],從鍵裏返回所有給定位置元素的位置(經度和緯度)
- GEODIST:返回兩個位置之間的距離
- GEOHASH:返回一個或多個位置元素的 Geohash 表示
代碼例子
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
ctx = context.Background()
// GEOADD,添加一個
val, err := rdb.GeoAdd(ctx, "town-geo-key", &redis.GeoLocation{
Longitude: 113.2442,
Latitude: 23.12592,
Name: "niwan-town",
}).Result()
if err != nil {
panic(err)
}
fmt.Println("GeoAdd: ", val)
// GEOADD,添加多個
val, _ = rdb.GeoAdd(ctx, "town-geo-key",
&redis.GeoLocation{Longitude: 113.2442, Latitude: 23.12592, Name: "niwan-town"},
&redis.GeoLocation{Longitude: 113.38397, Latitude: 22.93599, Name: "panyu-town"},
&redis.GeoLocation{Longitude: 113.60845, Latitude: 22.77144, Name: "nansha-town"},
&redis.GeoLocation{Longitude: 113.829579, Latitude: 23.290497, Name: "zengcheng-town"},
).Result()
fmt.Println("Mulit GeoAdd : ", val)
// GEOPOS,根據名字獲取經緯度
lonlats, err := rdb.GeoPos(ctx, "town-geo-key", "zengcheng-town", "panyu-town").Result()
if err != nil {
panic(err)
}
for _, lonlat := range lonlats {
fmt.Println("GeoPos, ", "Longitude: ", lonlat.Longitude, "Latitude: ", lonlat.Latitude)
}
// GEODIST , 計算兩地距離
distance, err := rdb.GeoDist(ctx, "town-geo-key", "niwan-town", "nansha-town", "m").Result() // m-米,km-千米,mi-英里
if err != nil {
panic(err)
}
fmt.Println("GeoDist: ", distance, " m")
// GEOHASH,計算hash值
hash, _ := rdb.GeoHash(ctx, "town-geo-key", "zengcheng-town").Result()
fmt.Println("zengcheng-town geohash: ", hash)
// GEORADIUS,計算範圍內包含的經緯度位置
radius, _ := rdb.GeoRadius(ctx, "town-geo-key", 113.829579, 23.290497, &redis.GeoRadiusQuery{
Radius: 800,
Unit: "km",
WithCoord: true, // WITHCOORD參數,返回結果會帶上匹配位置的經緯度
WithDist: true, // WITHDIST參數,返回結果會帶上匹配位置與給定地理位置的距離。
WithGeoHash: true, // WITHHASH參數,返回結果會帶上匹配位置的hash值。
Count: 4, // COUNT參數,可以返回指定數量的結果。
Sort: "ASC", // 傳入ASC爲從近到遠排序,傳入DESC爲從遠到近排序。
}).Result()
for _, v := range radius {
fmt.Println("GeoRadius: ", v)
}
// 上面式子裏參數更多詳情請看這裏:http://redisdoc.com/geo/georadius.html
}
/*
GeoAdd: 0
Mulit GeoAdd : 0
GeoPos, Longitude: 113.8295790553093 Latitude: 23.290497021802757
GeoPos, Longitude: 113.3839675784111 Latitude: 22.935990920457606
GeoDist: 54280.9773 m
zengcheng-town geohash: [ws0uqrbhvr0]
GeoRadius: {zengcheng-town 113.8295790553093 23.290497021802757 0 4046592114973855}
GeoRadius: {panyu-town 113.3839675784111 22.935990920457606 60.2724 4046531372960175}
*/
Stream作爲消息隊列
stream簡介
Redis5.0 增加了 Stream 數據類型,stream 類型可以支持消息隊列,因爲它具備消息隊列的很多特性。
Stream 是什麼呢?
stream 是一種數據結構,它類似於一種追加日誌。你能夠使用 stream 實時記錄並關聯相關事件。stream 使用場景:
- Event sourcing 事件碩源(比如跟蹤用戶操作,點擊事件等)
- Sensor monitoring 傳感器監控(比如從設備中讀取數據)
- Notifications 通知(比如將每個用戶的通知信息單獨記錄在 stream 中)
stream 是一個包含零個或任意多個元素數據的有序隊列,隊列中的每個元素都包含一個 ID 和任意多個鍵值對,這些元素根據 ID 大小在流中有序排列。ID 是由毫秒和順序數組成,比如 10000000000000-0。
stream 流中的每個元素可以包含一個或任意多個鍵值對,同一流中不同元素可以包含不同數量的鍵值對。
來一張 stream 流的存儲示意圖:
stream命令用法講解
stream 命令文檔:
Stream 是 5.0 才增加的數據類型,所以詳細講解下相關命令的用法。
消息隊列相關的命令:
- XADD:向某個消息隊列中添加消息,添加到流尾部,https://redis.io/commands/xadd/
// 語法 XADD key [NOMKSTREAM] [<MAXLEN | MINID> [= | ~] threshold [LIMIT count]] <* | id> field value [field value ...]
key:表示消息隊列名稱
[NOMKSTREAM]:可選參數,表示 key 不存在新建
[<MAXLEN | MINID> [= | ~] threshold [LIMIT count]:可選參數,<MAXLEN | MINID> 表示消息隊列中消息的最大長度或消息ID的最小值;
[= | ~] 設置精確的值或大約值;
threshold 表示具體設置的值,超過 threshold 值後會將舊的值刪除;
[LIMIT count] (Redis6.2後加入的參數) 限制數量;
<* | id> 表示消息 ID,* 表示由 Redis 生成,id 則是自定義生成;
field value [field value ...] 具體消息內容的鍵值對,可以傳入多個。
redis-cli 下示例:
> XADD mystream * sensor-id 1234 temperature 19.8
1518951480106-0
// 用 XADD 命令增加了2組數據 sensor-id:1234 和 temperature:19.8,消息隊列的 key 是 mystream,* 表示Redis自動生成id。
// 1518951480106-0,返回redis生成的id,由毫秒時間和順序編號組成。
- XREAD:從某一消息隊列中讀取消息
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id[id ...]
- [COUNT count]:可選參數,表示讀取消息的數量,COUNT 爲關鍵字,小寫count表示具體的數值
- [BLOCK milliseconds]:可選參數,BLOCK 爲關鍵字,表示設置 XREAD 爲阻塞模式,默認非阻塞,milliseconds 表示阻塞具體時間
- STREAMS key [key ...]:STREAMS 爲關鍵字,key 表示消息隊列名稱,可以傳入多個消息隊列名稱
- id[id ...]:表示從哪個消息ID讀取,與上面的 key 一一對應。id爲0表示從第一條開始讀取。阻塞模式可以使用$表示最新消息ID
redis-cli 下示例:
> xread COUNT 2 STREAMS mystream 0-0
// 讀取 2 個消息隊列stream
> xadd myapple * applekeyone apple1
> xread COUNT 2 STREAMS mystream myapple 0-0 0-0
- XLEN:獲取消息隊列長度
// 語法 XLEN key
- key: 表示消息隊列名稱
> xlen mystream
- XRANGE:獲取範圍隊列長度
XRANGE key start end [COUNT count]
- key: 消息隊列名稱
- start:起始消息 ID
- end:結束消息 ID
- [COUNT count]:讀取指定消息的數量。COUNT 關鍵字,count 數值
例子:
// - + 表示讀取所有
> xrange mystream - +
- XDEL:消息隊列刪除,https://redis.io/commands/xdel/
XDEL key id [id ...]
- key:消息隊列名稱
- id:消息隊列ID
例子:
> xadd myapple * applekeyone apple1
"1678952235530-0"
> xadd myapple * two apple2
"1678953271739-0"
> xadd myapple * three apple3
"1678953284915-0"
> xdel myapple 1678953271739-0
XGROUP 相關命令
- XGROUP CREATE:創建消費者組
XGROUP CREATE key group <id | $> [MKSTREAM]
- CREATE:關鍵字
- key: 消息隊列名
- group:要創建的消費者組名稱
- <id | $>:消費者從哪個 ID 開始消費數據,id - 指定消息id,id爲0表示從第一個獲取,$ - 表示只消費新產生的消息。
- [MKSTREAM]:可選參數,如果指定消息隊列不存在,則自動創建
- XGROUP SETID:設置消費者組中下一條要讀取的命令
XGROUP SETID key group <id | $>
- SETID:關鍵字
- key:消息隊列名
- group:消費者組名
- <id | $>:指定具體消息id,0 可以表示重新開始處理消費者組中所有消息,$ 表示只處理新產生的消息
- XGROUP DESTORY:銷燬消費者組
XGROUP DESTROY key group
- DESTORY:關鍵字
- key:消息隊列名
- group:要銷燬的消費組名
- XGROUP CRATECONSUMER:創建消費者
XGROUP CREATECONSUMER key group consumer
- CREATECONSUMER:關鍵字
- key:消息隊列名稱
- group:消費者組名稱
- consumer:要創建的消費者名稱
10:XGROUP DELCONSUMER:刪除消費者
XGROUP DELCONSUMER key group consumer
XINFO 相關命令
- XINFO CONSUMERS:用於監控消費者,https://redis.io/commands/xinfo-consumers/
XINFO CONSUMERS key group
- CONSUMERS:關鍵字
- key:消息隊列名
- group:消費者組名
- XIFNO GROUPS:用於監控消費者組
XINFO GROUPS key
- XINFO STREAM:監控消息隊列
XINFO STREAM key [FULL [COUNT count]]
- STREAM:關鍵字
- key:消息隊列名
- [FULL [COUNT count]]:FULL 表示所有消息隊列;COUNT 關鍵字,count 表示數值,多少個消息隊列
- XREADGROUP:分組消費
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] id [id ...]
- GROUP:關鍵字
- group:消費者組名
- [COUNT count]:可選參數,指定讀取消息的數量。COUNT 爲關鍵字,count 讀取具體的數值
- [BLOCK milliseconds]:可選參數,設置爲阻塞讀取。BLOCK 爲關鍵字,milliseconds 設置阻塞時間。默認爲非阻塞
- [NOACK]:可選參數,表示不要將消息加入Pending等待隊列中,相當於消息讀取時進行消息確認
- STREAMS:關鍵字
- key [key ...]:消息隊列的名稱,可以傳入多個名稱。
- id [id ...]:從哪個消息ID開始讀,與上面的 key 一一對應。如果爲 0 表示從第一條消息開始讀。在阻塞模式中,可以用 $ 表示最新的消息ID。非阻塞模式下 $ 沒有意義。
- XPENDING:用於獲取等待隊列。等待隊列中保存了消費者組內被讀取但還未完成處理的消息,也就是還沒被ACK的消息
XPENDING key group [[IDLE min-idle-time] start end count [consumer]]
- key:消息隊列名
- group:消費者組的名稱
- [IDLE min-idle-time]:可選參數,IDLE 關鍵字,表示指定消息已讀取時長,min-idle-time 表示具體數值
- start:起始消息ID
- end:結束消息ID
- count:讀取消息的條數
- [consumer]:可選參數,表示消費者名稱
- XACK:用於進行消息確認
XACK key group id [id ...]
- key:消息隊列名
- group:消費者組名
- id:消息ID,可以傳入多個
- XCLAIM:消息轉移。當某個等待隊列中的消息長時間沒有被處理(沒被ACK)時,可以用這個命令將其轉移到其它消費者等待列表中。
XCLAIM key group consumer min-idle-time id [id ...] [IDLE ms] [TIME unix-time-milliseconds] [RETRYCOUNT count] [FORCE] [JUSTID] [LASTID lastid]
- key:消息隊列名
- group:消費者組名
- consumer:消費者名
- min-idle-time:表示消息空閒時長(表示消息已經讀取,但還未處理)
- id [id...]:可選參數,要轉移的消息ID,可傳入多個ID
- [IDLE ms]:可選參數,設置消息空閒時間
- [TIME unix-time-milliseconds]:可選參數,它將空閒時間設置爲特定的UNIX時間,以毫秒爲單位。
- [RETRYCOUNT count]:可選參數,設置重試計數器的值,每次消息讀取時,計數器的值都會遞增。一般不需要修改這個值。
- [FORCE]:可選參數,強制將消息ID加入到執行消費者的等待列表中
- [JUSTID]:可選參數,僅返回要轉移消息的ID,使用此參數意味着重試計數器不會遞增
- [LASTID lastid]:可選參數,返回最後一個消息ID
- XREADGROUP:對消息組進行讀取
XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] id [id ...]
- GROUP:關鍵字
- group:消費者組名
- consumer:消費者名
- [COUNT count]:可選參數,每個流讀取的數量,COUNT 爲關鍵字,count爲讀取數值
- [BLOCK milliseconds]:可選參數,用阻塞的方式執行。BLOCK 爲關鍵字,milliseconds爲阻塞毫秒數。如果爲 0 表示阻塞直到出現可返回的元素爲止。
- [NOACK]:可選參數,讀取消息時是否確認。true-需要確認,false-不需要確認
- STREAMS key [key ...] id [id ...]:STREAMS 關鍵字,key 消息隊列明,id 消息ID。
- XTRIM:對流進行裁剪,
stream tutorial
官方地址:https://redis.io/docs/data-types/streams-tutorial/
代碼例子
- xadd 添加一個消息隊列:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
ctx = context.Background()
// XADD,添加消息到對尾(這個代碼每運行一次就增加一次內容)
err = rdb.XAdd(ctx, &redis.XAddArgs{
Stream: "mystreamone", // 設置流stream的 key,消息隊列名
NoMkStream: false, //爲false,key不存在會新建
MaxLen: 10000, //消息隊列最大長度,隊列長度超過設置最大長度後,舊消息會被刪除
Approx: false, //默認false,設爲true時,模糊指定stram的長度
ID: "*", //消息ID,* 表示由Redis自動生成
Values: []interface{}{ //消息隊列的內容,鍵值對形式
"apple", "12.0",
"orange", "5.6",
"banana", "7.6",
},
// MinID: "id",//超過設置長度值,丟棄小於MinID消息id
// Limit: 1000, //限制長度,基本不用
}).Err()
if err != nil {
panic(err)
}
}
- 創建一個消費者組
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
ctx = context.Background()
// XGroupCreate,創建一個消費者組
err = rdb.XGroupCreate(ctx, "mystreamone", "test_group1", "0").Err() // 0-從第一個獲取,$-從最新獲取
if err != nil {
panic(err)
}
}
- 獲取、讀取消息隊列信息、確認信息、獲取流信息、消費者組信息、消費者信息
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
ctx = context.Background()
//XLEN,獲取stream中元素數量,也就是消息隊列長度
len, err := rdb.XLen(ctx, "mystreamone").Result()
if err != nil {
panic(err)
}
fmt.Println("XLen: ", len)
// XRead,從消息隊列獲取數據,阻塞或非阻塞
val, err := rdb.XRead(ctx, &redis.XReadArgs{
Block: time.Second * 10, // 如果Block設置爲0,表示一直阻塞,默認非阻塞。這裏設置阻塞10s
Count: 2, // 讀取消息的數量
Streams: []string{"mystreamone", "0-0"}, // 消息隊列名稱,從哪個ID開始讀起,0-0 表示從mystreamone的第一個ID開始讀
}).Result()
if err != nil {
panic(err)
}
fmt.Println("XRead: ", val)
// XRANGE,從隊列左邊獲取值,ID 從小到大
vals, err := rdb.XRange(ctx, "mystreamone", "-", "+").Result() //- + 表示讀取所有
if err != nil {
panic(err)
}
fmt.Println("XRange: ", vals)
// XRangeN,從隊列左邊獲取N個值,ID 從小到大
vals, _ = rdb.XRangeN(ctx, "mystreamone", "-", "+", 2).Result() //順序獲取隊列前2個值
fmt.Println("XRangeN: ", vals)
// XRevRange,從隊列右邊獲取值,ID 從大到小,與XRANGE相反
vals, _ = rdb.XRevRange(ctx, "mystreamone", "+", "-").Result()
fmt.Println("XRevRange: ", vals)
// XRevRangeN,從隊列右邊獲取N個值,ID 從大到小
// rdb.XRevRangeN(ctx, "mystreamone", "+", "-", 2).Result()
//XDEL - 刪除消息
//err = rdb.XDel(ctx, "mystreamone", "1678984704869-0").Err()
// ========= 消費者組相關操作 API ===========
// XGroupCreate,創建一個消費者組
/*
err = rdb.XGroupCreate(ctx, "mystreamone", "test_group1", "0").Err() // 0-從第一個獲取,$-從最新獲取
if err != nil {
panic(err)
}
*/
// XReadGroup,讀取消費者中消息
readgroupval, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
// Streams第二個參數爲ID,list of streams and ids, e.g. stream1 stream2 id1 id2
// id爲 >,表示最新未讀消息ID,也是未被分配給其他消費者的最新消息
// id爲 0 或其他,表示可以獲取已讀但未確認的消息。這種情況下BLOCK和NOACK都會忽略
// id爲具體ID,表示獲取這個消費者組的pending的歷史消息,而不是新消息
Streams: []string{"mystreamone", ">"},
Group: "test_group1", //消費者組名
Consumer: "test_consumer1", // 消費者名
Count: 1,
Block: 0, // 是否阻塞,=0 表示阻塞且沒有超時限制。只要大於1條消息就立即返回
NoAck: true, // true-表示讀取消息時確認消息
}).Result()
if err != nil {
panic(err)
}
fmt.Println("XReadGroup: ", readgroupval)
// XPending,獲取待處理的消息
count, err := rdb.XPending(ctx, "mystreamone", "test_group1").Result()
if err != nil {
panic(err)
}
fmt.Println("XPending: ", count)
// XAck , 將消息標記爲已處理
err = rdb.XAck(ctx, "mystreamone", "test_group1", "1678984704869-0").Err()
// XClaim , 轉移消息的歸屬權
claiminfo, err := rdb.XClaim(ctx, &redis.XClaimArgs{
Stream: "mystreamone",
Group: "test_group1",
Consumer: "test_consumer2",
MinIdle: time.Second * 10, // 表示要轉移的消息需要最少空閒 10s 才能轉移
Messages: []string{"1678984704869-0"},
}).Result()
if err != nil {
panic(err)
}
fmt.Println("XClaim: ", claiminfo)
// XInfoStream , 獲取流的消息
info, err := rdb.XInfoStream(ctx, "mystreamone").Result()
if err != nil {
panic(err)
}
fmt.Println("XInfoStream: ", info)
// XInfoGroups , 獲取消費者組消息
groupinfo, _ := rdb.XInfoGroups(ctx, "mystreamone").Result()
fmt.Println("XInfoGroups: ", groupinfo)
// XInfoConsumer ,獲取消費者信息
consumerinfo, _ := rdb.XInfoConsumers(ctx, "mystreamone", "test_group1").Result()
fmt.Println("XInfoConsumers: ", consumerinfo)
}
- 刪除相關信息
刪除消息隊列裏的消息
ctx = context.Background()
//XDEL - 刪除消息
count, _ := rdb.XDel(ctx, "mystreamone", "1678984704869-0", "1678984915646-0", "1678985389693-0", "1678985099142-0").Result()
fmt.Println("XDel: ", count)
刪除消費者信息和消費者組信息
ctx = context.Background()
//XGroupDelConsumer,刪除消費者
count, _ := rdb.XGroupDelConsumer(ctx, "mystreamone", "test_group1", "test_consumer1").Result()
fmt.Println("XGroupDelConsumer: ", count)
// XGroupDestroy , 刪除消費者組
count, _ = rdb.XGroupDestroy(ctx, "mystreamone", "test_group1").Result()
fmt.Println("XGroupDestroy: ", count)
二、Pipelining管道化
Redis 中的 pipelining 是一種通過一次發送多個命令而不必等待對每個命令響應,用這種方式來提高性能的一種技術。
下面的大部分代碼來自 go-redis 官方文檔:https://redis.uptrace.dev/guide/go-redis-pipelines.html。
- pipeline 基礎使用, 用 pipeline 一次執行多條命令:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
ctx = context.Background()
// 一次執行 2 個刪除命令
rdb.Set(ctx, "setkey1", "value1", 0).Err()
rdb.Set(ctx, "setkey2", "value2", 0).Err()
pipe := rdb.Pipeline()
pipe.Del(ctx, "setkey1")
pipe.Del(ctx, "setkey2")
cmds, err := pipe.Exec(ctx)
if err != nil {
panic(err)
}
fmt.Println("Pipeline: ", cmds)
// 一次執行寫和加上過期時間命令,用 pipeline 一次執行這2條命令
incr := pipe.Incr(ctx, "pipeline_counter") // Incr 相當於寫入
pipe.Expire(ctx, "pipeline_counter", time.Second*60) // 加上過期時間
cmds, err = pipe.Exec(ctx) // 執行 pipeline
if err != nil {
panic(err)
}
// 執行 pipe.Exec() 後獲取結果
fmt.Println("Pipeline: ", incr.Val())
}
- Pipelined 方法
- 它可以把 pipeline 執行多條命令作爲一個函數整體來執行,看着像省略 Exec() 執行方法,其實這個 Pipelined 函數裏就包含了 Exec()。執行後返回結果。代碼示例如下:
// Pipelined, 另外一種方法 Pipelined
var incr2 *redis.IntCmd
cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
incr2 = pipe.Incr(ctx, "pipeline_counter2")
pipe.Expire(ctx, "pipeline_counter2", time.Second*60)
return nil
})
if err != nil {
panic(err)
}
fmt.Println("Pipelined: ", incr2.Val())
- 批量結果,Pipelined 執行後批量返回結果,返回結果都存儲在類似於 *redis.XXXCmd 的指針中,
// 遍歷 pipeline 命令執行後的返回值
cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
for i := 0; i < 5; i++ {
pipe.Set(ctx, fmt.Sprintf("key%d", i), fmt.Sprintf("val%d", i), 0)
}
return nil
})
if err != nil {
panic(err)
}
for _, cmd := range cmds {
fmt.Println(cmd.(*redis.StatusCmd).Val())
}
- Pipelined() 方法簡析,源碼如下:
// https://github.com/redis/go-redis/blob/v8/pipeline.go#L128
func (c *Pipeline) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
if err := fn(c); err != nil {
return nil, err
}
cmds, err := c.Exec(ctx) // 看見沒,這裏會執行 Exec() 方法
_ = c.Close()
return cmds, err
}
// Pipeliner 是一個接口,接口就可以調用實現了它的方法了
type Pipeliner interface {
StatefulCmdable
Len() int
Do(ctx context.Context, args ...interface{}) *Cmd
Process(ctx context.Context, cmd Cmder) error
Close() error
Discard() error
Exec(ctx context.Context) ([]Cmder, error)
}
三、Transaction事務
事務命令介紹
Redis 事務允許在單個步驟中執行一組命令。
事務中所有命令都被序列化並按照順序執行。另外一個客戶端發送的請求永遠不會在Redis事務執行過程中得到處理,這保證了命令作爲單個命令原子執行。
在 Redis 中使用事務時,有幾個相關命令,EXEC、MULTI、WATCH、UNWATCH
,還有一個 DISCARD
。
- MULTI:標記一個事務開始。在一個事務內有多條命令會按照先後順序放進一個隊列中,最後由 EXEC 命令原子的執行。
multi 命令例子:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR count1
QUEUED
127.0.0.1:6379> INCR count2
QUEUED
127.0.0.1:6379> PING
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 1
2) (integer) 2
3) PONG
127.0.0.1:6379>
- EXEC:觸發執行事務內的所有命令,如上面的例子。
如果某個或某些 key 正處於 WATCH 命令監視之下,並且事務中有和這些 key 相關的命令,那麼 EXEC 命令只有在這個或這些key 沒有被其他命令所改動的情況下執行並生效,否則該事務會被打斷。
- WATCH:監視一個key或多個key,如果事務在執行之前,這個key或多個key被其他命令改動,那麼事務將被打斷
WATCH key [key …]
- UNWATCH:取消 WATCH 命令對所有key的監視。它沒有任何參數。
127.0.0.1:6379> WATCH lockone locktwo
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET lockone "testwatch"
QUEUED
127.0.0.1:6379> INCR locktwo
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 1
如果你又開另外一個客戶端:
127.0.0.1:6379> WATCH lockone locktwo
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET lockone "testwatch2"
QUEUED
127.0.0.1:6379> INCR locktwo
QUEUED
127.0.0.1:6379> EXEC # locktwo 這時被另外一個客戶端修改了,testwatch2 執行也失敗
(nil)
UNWATCH 命令:
127.0.0.1:6379> UNWATCH
- DISCARD:取消事務,放棄執行事務內的所有命令
DISCARD 命令例子:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> PING
QUEUED
127.0.0.1:6379> SET helloworld "hello"
QUEUED
127.0.0.1:6379> DISCARD
OK
TxPipeline和TxPipelined包裝MULTI和EXEC
TxPipeline 和 TxPipelined 是把 Redis 中的 2 個事務命令 MULTI 和 EXEC 包裝起來,然後用 pipeline 來執行命令。
這 2 個命令和 pipeline 和 pipelined 用法幾乎相同。
代碼例子
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
ctx = context.Background()
// 一次執行 2 個刪除命令
rdb.Set(ctx, "setkey1", "value1", 0).Err()
rdb.Set(ctx, "setkey2", "value2", 0).Err()
//TxPipeline
txpipe := rdb.TxPipeline()
txpipe.Del(ctx, "setkey1")
txpipe.Del(ctx, "setkey2")
cmds, err := txpipe.Exec(ctx) // 執行 TxPipeline 裏的命令
if err != nil {
panic(err)
}
fmt.Println("TxPipeline: ", cmds)
// TxPipelined
var incr2 *redis.IntCmd
cmds, err = rdb.TxPipelined(ctx, func(txpipe redis.Pipeliner) error {
txpipe.Set(ctx, "txpipeline_counter2", 30, time.Second*120)
incr2 = txpipe.Incr(ctx, "txpipeline_counter2")
txpipe.Expire(ctx, "txpipeline_counter2", time.Second*300)
return nil
})
if err != nil {
panic(err)
}
fmt.Println("TxPipelined: ", incr2.Val())
fmt.Println("cmds: ", cmds)
}
/*
TxPipeline: [del setkey1: 1 del setkey2: 1]
TxPipelined: 31
cmds: [set txpipeline_counter2 30 ex 120: OK incr txpipeline_counter2: 31 expire txpipeline_counter2 300: true]
*/
Watch監視
watch 監視一個key或多個key。如果事務在執行之前,這個key或多個key被其他命令改動,那麼事務將被打斷。
你使用 watch 在一個客戶端上監視一些操作命令,然後另外開一個客戶端執行相同的命令,那麼 watch 會監視這種變動從而取消事務操作。
代碼例子
這是官方文檔的一個例子,改一下:
package main
import (
"context"
"fmt"
"strconv"
"sync"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
ctx = context.Background()
var incr func(string) error
incr = func(key string) error {
err = rdb.Watch(ctx, func(tx *redis.Tx) error { //Watch 監控函數
n, err := tx.Get(ctx, key).Int64() // 先查詢下當前watch監聽的key的值
if err != nil && err != redis.Nil {
return err
}
// 如果key的值沒有改變的話,pipe 函數纔會調用成功
_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.Set(ctx, key, strconv.FormatInt(n+1, 10), 0)
return nil
})
return err
}, key)
if err == redis.TxFailedErr {
return incr(key)
}
return err
}
keyname := "keynameone"
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
err := incr(keyname)
fmt.Println("[for] err: ", err)
}()
}
wg.Wait()
n, err := rdb.Get(ctx, keyname).Int64()
if err != nil {
panic(err)
}
fmt.Println("last key val: ", n)
}
四、pub/sub 發佈/訂閱
簡介
Redis 的發佈訂閱功能,有三大部分:發佈者、訂閱者和 Channel 頻道。發佈者和訂閱者都是 Redis 客戶端,Channel 頻道是Redis 服務器。發佈者將消息發送到某個頻道,訂閱了這條頻道的訂閱者就能收到這條消息。
常用命令
PUBLISH :
publish channel message
,向channel頻道發佈消息SUBSCRIBE:
subscribe channel [channel ...]
,訂閱頻道PSUBSCRIBE:
psubscribe pattern [pattern …]
,訂閱多個符合模式的頻道UNSUBSCRIBE:
unsubscribe [channel [channel …]]
,客戶端退訂頻道,可以退訂多個頻道PUNSUBSCRIBE:
punsubscribe [pattern [pattern …]]
,指定客戶端退訂多個符合模式的頻道PUBSUB:查看訂閱和發佈系統狀態的命令,它由數個不同格式子命令組成
代碼例子
publish 頻道發佈消息:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
ctx = context.Background()
// 向頻道 mychannel1 發佈消息 payload1
err = rdb.Publish(ctx, "mychannel1", "payload1").Err()
if err != nil {
panic(err)
}
val, err := rdb.Publish(ctx, "mychannel1", "hello").Result()
if err != nil {
panic(err)
}
fmt.Println(val)
rdb.Publish(ctx, "mychannel2", "hello2").Err()
}
subscribe 訂閱頻道消息:
// Subscribe,訂閱頻道接收消息
// pubsub := rdb.Subscribe(ctx, "mychannel1")
pubsub := rdb.Subscribe(ctx, "mychannel1", "mychannel2")
defer pubsub.Close()
// 第一種接收消息方法
// ch := pubsub.Channel()
// for msg := range ch {
// fmt.Println(msg.Channel, msg.Payload)
// }
// 第二種接收消息方法
for {
msg, err := pubsub.ReceiveMessage(ctx)
if err != nil {
panic(err)
}
fmt.Println(msg.Channel, msg.Payload)
}
PSubscribe 模式匹配訂閱頻道消息:
pubsub := rdb.PSubscribe(ctx, "mychannel*")
defer pubsub.Close()
// 第一種接收消息方法
ch := pubsub.Channel()
for msg := range ch {
fmt.Println(msg.Channel, msg.Payload)
}
Unsubscribe 退訂頻道:
// Subscribe,訂閱頻道
pubsub := rdb.Subscribe(ctx, "mychannel1", "mychanne2")
defer pubsub.Close()
// 退訂具體頻道
unsub := pubsub.Unsubscribe(ctx, "mychannel1", "mychannel2")
// 按照模式匹配退訂
pubsub.PUnsubscribe(ctx, "mychannel*")
查詢頻道相關信息、訂閱者信息:
ps := rdb.Subscribe(ctx, "mychannel*")
defer ps.Close()
// PubSubChannels,查詢活躍的頻道
fmt.Println("====PubSubChannels====")
channels, _ := rdb.PubSubChannels(ctx, "").Result() //"" 爲空,查詢所有活躍的channel頻道
for ch, v := range channels {
fmt.Println(ch, v)
}
// 指定匹配模式
channels, _ = rdb.PubSubChannels(ctx, "mychannel*").Result()
for ch, v := range channels {
fmt.Println("PubSubChannels* :", ch, v)
}
fmt.Println("====PubSubNumSub====")
// PubSubNumSub,具體的channel有多少個訂閱者
numsub, _ := rdb.PubSubNumSub(ctx, "mychannel1", "mychannel2").Result()
for ch, count := range numsub {
fmt.Println(ch, ",", count) // ch-channel名字,count-channel的訂閱者數量
}
// PubSubNumPat, 模式匹配
pubsub := rdb.PSubscribe(ctx, "mychannel*")
defer pubsub.Close()
numsubpat, _ := rdb.PubSubNumPat(ctx).Result()
fmt.Println("PubSubNumPat: ", numsubpat)
五、Lua腳本
介紹
從Redis2.6開始,通過內置的 Lua 解釋器,可以使用 EVAL 命令對 lua 腳本進行求值。
- EVAL:語法
EVAL script numkeys key [key …] arg [arg …]
- script:一段 lua5.1腳本程序,它會被運行在redis服務器下
- numkeys:用於指定健名參數的個數
- key [key …]:從 EVAL 的第三個參數開始算起,表示在腳本中所用到的那些 Redis 鍵(key)。這些鍵可以通過lua的全局變量KEYS數組獲取,用 1 訪問形式 KEYS[1], 2 是 KEYS[2]
- arg [arg …]:附加參數,可以在lua中用全局變量 ARGV 數組訪問,訪問形式 ARGV[1],ARGC[2] 等。
命令例子:
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
// first second 都是附加參數
在 Lua 腳本中,可以用 redis.call() 和 redis.pcall() 來還行 Redis 命令。
代碼例子
直接拿官網的例子來看,
// https://github.com/redis/go-redis/blob/master/example/lua-scripting/main.go
package main
import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
)
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: ":6379",
})
_ = rdb.FlushDB(ctx).Err()
fmt.Printf("# INCR BY\n")
for _, change := range []int{+1, +5, 0} {
num, err := incrBy.Run(ctx, rdb, []string{"my_counter"}, change).Int()
if err != nil {
panic(err)
}
fmt.Printf("incr by %d: %d\n", change, num)
}
fmt.Printf("\n# SUM\n")
sum, err := sum.Run(ctx, rdb, []string{"my_sum"}, 1, 2, 3).Int()
if err != nil {
panic(err)
}
fmt.Printf("sum is: %d\n", sum)
}
var incrBy = redis.NewScript(`
local key = KEYS[1]
local change = ARGV[1]
local value = redis.call("GET", key)
if not value then
value = 0
end
value = value + change
redis.call("SET", key, value)
return value
`)
var sum = redis.NewScript(`
local key = KEYS[1]
local sum = redis.call("GET", key)
if not sum then
sum = 0
end
local num_arg = #ARGV
for i = 1, num_arg do
sum = sum + ARGV[i]
end
redis.call("SET", key, sum)
return sum
`)
六、Do任意命令和用戶自定義命令
go-redis 中提供了一個 Do 方法,它可以執行任意方法.
Do方法例子:
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
IdleTimeout: 350,
PoolSize: 50, // 連接池連接數量
})
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, err := rdb.Ping(ctx).Result() // 檢查連接redis是否成功
if err != nil {
fmt.Println("Connect Failed: %v \n", err)
panic(err)
}
v := rdb.Do(ctx, "get", "key_does_not_exist").String()
fmt.Printf("%q \n", v)
err = rdb.Do(ctx, "set", "set-key", "set-val", "EX", time.Second*120).Err()
fmt.Println("Do set: ", err)
v = rdb.Do(ctx, "get", "set-key").String()
fmt.Println("Do get: ", v)
}
Do 源碼:
// https://github.com/redis/go-redis/blob/v8.2.0/redis.go#LL592C2-L592C2
func (c *Client) Do(ctx context.Context, args ...interface{}) *Cmd {
cmd := NewCmd(ctx, args...) // 構造 Cmd struct
_ = c.Process(ctx, cmd) // 執行 cmd
return cmd
}
//https://github.com/redis/go-redis/blob/v8.2.0/command.go#L196
func NewCmd(ctx context.Context, args ...interface{}) *Cmd {
return &Cmd{
baseCmd: baseCmd{
ctx: ctx,
args: args,
},
}
}
// https://github.com/redis/go-redis/blob/v8.2.0/command.go#L183
type Cmd struct {
baseCmd
val interface{}
}
//https://github.com/redis/go-redis/blob/v8.2.0/command.go#L111
type baseCmd struct {
ctx context.Context
args []interface{}
err error
keyPos int8
_readTimeout *time.Duration
}
完整代碼請查看 github:https://github.com/jiujuan/go-exercises/tree/main/redis/go-redis/v8
七、參考
- https://redis.io/docs/ redis doc
- https://redis.io/docs/data-types/tutorial/ redis 各種類型用法
- https://redis.io/commands/ redis command doc
- https://redis.uptrace.dev/guide/ go-redis guide
- https://pkg.go.dev/github.com/redis/go-redis go-redis doc
- https://redis.io/docs/data-types/streams/ stream
- https://redis.io/docs/data-types/streams-tutorial/ stream tutorial
- https://redis.io/docs/data-types/hyperloglogs/ hyperloglogs
- https://redis.uptrace.dev/guide/go-redis-pipelines.html pipeline
- https://redis.io/docs/manual/pipelining/ redis pipelining
- https://redis.uptrace.dev/guide/lua-scripting.html lua scripting