golang常用庫包:redis操作庫go-redis使用(03)-高級數據結構和其它特性

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 的命令文檔:

命令介紹

hyperloglog 常用命令:

  1. PFADD:PFADD key element [element …],將任意數量的元素添加到指定 hyperloglog 中
  2. PFCOUNT:PFCOUNT key [key ...],如果是單個鍵,返回給定鍵在hyperloglog中的近似值,不存在則返回 0;如果是多個鍵,返回給定hyperloglog的並集的近似值。
  3. 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 命令文檔:

命令介紹

GEO 常用命令:

  1. GEOADD:將緯度、經度、名字添加到指定的鍵裏
  2. GEORADIUS:以給定的經緯度爲中心,返回鍵包含的位置元素中,與中心的距離不超過給定最大距離的所有位置元素。在Redis6.2.0 廢棄
  3. GEOPOS:GEOPOS key [member [member ...]],從鍵裏返回所有給定位置元素的位置(經度和緯度)
  4. GEODIST:返回兩個位置之間的距離
  5. 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 流的存儲示意圖:

image-20230316233311767

stream命令用法講解

stream 命令文檔:

Stream 是 5.0 才增加的數據類型,所以詳細講解下相關命令的用法。

消息隊列相關的命令:

  1. 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,由毫秒時間和順序編號組成。
  1. 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
  1. XLEN:獲取消息隊列長度
// 語法
XLEN key
  • key: 表示消息隊列名稱
> xlen mystream
  1. XRANGE:獲取範圍隊列長度
XRANGE key start end [COUNT count]
  • key: 消息隊列名稱
  • start:起始消息 ID
  • end:結束消息 ID
  • [COUNT count]:讀取指定消息的數量。COUNT 關鍵字,count 數值

例子:

// - + 表示讀取所有
> xrange mystream - +  
  1. 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 相關命令

  1. XGROUP CREATE:創建消費者組
XGROUP CREATE key group <id | $> [MKSTREAM]
  • CREATE:關鍵字
  • key: 消息隊列名
  • group:要創建的消費者組名稱
  • <id | $>:消費者從哪個 ID 開始消費數據,id - 指定消息id,id爲0表示從第一個獲取,$ - 表示只消費新產生的消息。
  • [MKSTREAM]:可選參數,如果指定消息隊列不存在,則自動創建
  1. XGROUP SETID:設置消費者組中下一條要讀取的命令
XGROUP SETID key group <id | $>
  • SETID:關鍵字
  • key:消息隊列名
  • group:消費者組名
  • <id | $>:指定具體消息id,0 可以表示重新開始處理消費者組中所有消息,$ 表示只處理新產生的消息
  1. XGROUP DESTORY:銷燬消費者組
XGROUP DESTROY key group
  • DESTORY:關鍵字
  • key:消息隊列名
  • group:要銷燬的消費組名
  1. XGROUP CRATECONSUMER:創建消費者
XGROUP CREATECONSUMER key group consumer
  • CREATECONSUMER:關鍵字
  • key:消息隊列名稱
  • group:消費者組名稱
  • consumer:要創建的消費者名稱

10:XGROUP DELCONSUMER:刪除消費者

XGROUP DELCONSUMER key group consumer

XINFO 相關命令

  1. XINFO CONSUMERS:用於監控消費者,https://redis.io/commands/xinfo-consumers/
XINFO CONSUMERS key group
  • CONSUMERS:關鍵字
  • key:消息隊列名
  • group:消費者組名
  1. XIFNO GROUPS:用於監控消費者組
XINFO GROUPS key
  1. XINFO STREAM:監控消息隊列
XINFO STREAM key [FULL [COUNT count]]
  • STREAM:關鍵字
  • key:消息隊列名
  • [FULL [COUNT count]]:FULL 表示所有消息隊列;COUNT 關鍵字,count 表示數值,多少個消息隊列
  1. 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。非阻塞模式下 $ 沒有意義。
  1. 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]:可選參數,表示消費者名稱
  1. XACK:用於進行消息確認
XACK key group id [id ...]
  • key:消息隊列名
  • group:消費者組名
  • id:消息ID,可以傳入多個
  1. 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
  1. 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。
  1. XTRIM:對流進行裁剪,

stream tutorial

官方地址:https://redis.io/docs/data-types/streams-tutorial/

代碼例子

  1. 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)
	}
}
  1. 創建一個消費者組
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)
	}

}
  1. 獲取、讀取消息隊列信息、確認信息、獲取流信息、消費者組信息、消費者信息
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)
}

  1. 刪除相關信息

刪除消息隊列裏的消息

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。

  1. 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())

}
  1. 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 服務器。發佈者將消息發送到某個頻道,訂閱了這條頻道的訂閱者就能收到這條消息。

image-20230318184810184

常用命令

  • 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

七、參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章