Redis
在項目開發中redis的使用也比較頻繁,本文介紹了Go語言中go-redis
庫的基本使用。
Redis介紹
Redis是一個開源的內存數據庫,Redis提供了多種不同類型的數據結構,很多業務場景下的問題都可以很自然地映射到這些數據結構上。除此之外,通過復制、持久化和客戶端分片等特性,我們可以很方便地將Redis擴展成一個能夠包含數百GB數據、每秒處理上百萬次請求的系統。
Redis支持的數據結構
Redis支持諸如字符串(string)、哈希(hashe)、列表(list)、集合(set)、帶范圍查詢的排序集合(sorted set)、bitmap、hyperloglog、帶半徑查詢的地理空間索引(geospatial index)和流(stream)等數據結構。
Redis應用場景
- 緩存系統,減輕主數據庫(MySQL)的壓力。
- 計數場景,比如微博、抖音中的關注數和粉絲數。
- 熱門排行榜,需要排序的場景特別適合使用ZSET。
- 利用 LIST 可以實現隊列的功能。
- 利用 HyperLogLog 統計UV、PV等數據。
- 使用 geospatial index 進行地理位置相關查詢。
準備Redis環境
讀者可以選擇在本機安裝 redis 或使用云數據庫,這里直接使用Docker啟動一個 redis 環境,方便學習使用。
使用下面的命令啟動一個名為 redis507 的 5.0.7 版本的 redis server環境。
docker run --name redis507 -p 6379:6379 -d redis:5.0.7
注意:?此處的版本、容器名和端口號可以根據自己需要設置。
啟動一個 redis-cli 連接上面的 redis server。
docker run -it --network host --rm redis:5.0.7 redis-cli
go-redis庫
安裝
Go 社區中目前有很多成熟的 redis client 庫,比如GitHub - gomodule/redigo: Go client for Redis?和GitHub - redis/go-redis: Redis Go client,讀者可以自行選擇適合自己的庫。本文使用 go-redis 這個庫來操作 Redis 數據庫。
使用以下命令下安裝 go-redis 庫。
安裝v8
版本:
go get github.com/redis/go-redis/v8
安裝v9
版本:
go get github.com/redis/go-redis/v9
連接
在項目中導入?go-redis
庫(請根據實際情況導入自己需要的版本)。
import "github.com/redis/go-redis/v9"
SetNX
方法僅在鍵不存在時設置值:
err := rdb.SetNX(ctx, "key1", "value", 0).Err()
if err != nil {panic(err)
}
2.5.1 添加和刪除
ZAdd用于添加或更新元素,ZRem用于刪除元素:
err := rdb.ZAdd(ctx, "key", &redis.Z{Score: 2.5, Member: "zhangsan"}).Err()
if err != nil {
?? ?panic(err)
}
rdb.ZRem(ctx, "key", "zhangsan")
2.5.2 查詢操作
ZRange和ZRevRange用于按分數排序返回元素,ZScore用于查詢元素的分數:
vals, err := rdb.ZRange(ctx, "key", 0, -1).Result()
if err != nil {
?? ?panic(err)
}
fmt.Println(vals)
score, err := rdb.ZScore(ctx, "key", "zhangsan").Result()
if err != nil {
?? ?panic(err)
}
fmt.Println(score)
? ? ? ? ? ? ? ? ? ? ? ??
原文鏈接:https://blog.csdn.net/weixin_73833086/article/details/146127620
普通連接模式
注意 如果 redis?的值不存的情況下:
if err == redis.Nil? 的情況判斷
go-redis 庫中使用 redis.NewClient 函數連接 Redis 服務器。
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379",Password: "", // 密碼DB: 0, // 數據庫PoolSize: 20, // 連接池大小
})
除此之外,還可以使用 redis.ParseURL 函數從表示數據源的字符串中解析得到 Redis 服務器的配置信息。
opt, err := redis.ParseURL("redis://<user>:<pass>@localhost:6379/<db>")
if err != nil {panic(err)
}rdb := redis.NewClient(opt)
TLS連接模式
如果使用的是 TLS 連接方式,則需要使用 tls.Config 配置。
rdb := redis.NewClient(&redis.Options{TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12,// Certificates: []tls.Certificate{cert},// ServerName: "your.domain.com",},
})
Redis Sentinel模式? 哨兵模式
使用下面的命令連接到由 Redis Sentinel 管理的 Redis 服務器。
rdb := redis.NewFailoverClient(&redis.FailoverOptions{MasterName: "master-name",SentinelAddrs: []string{":9126", ":9127", ":9128"},
})
Redis Cluster模式 集群模式
使用下面的命令連接到 Redis Cluster,go-redis 支持按延遲或隨機路由命令。
rdb := redis.NewClusterClient(&redis.ClusterOptions{Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},// 若要根據延遲或隨機路由命令,請啟用以下命令之一// RouteByLatency: true,// RouteRandomly: true,
})
基本使用
執行命令
下面的示例代碼演示了 go-redis 庫的基本使用。
// doCommand go-redis基本使用示例
func doCommand() {ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)defer cancel()// 執行命令獲取結果val, err := rdb.Get(ctx, "key").Result()fmt.Println(val, err)// 先獲取到命令對象cmder := rdb.Get(ctx, "key")fmt.Println(cmder.Val()) // 獲取值fmt.Println(cmder.Err()) // 獲取錯誤// 直接執行命令獲取錯誤err = rdb.Set(ctx, "key", 10, time.Hour).Err()// 直接執行命令獲取值value := rdb.Get(ctx, "key").Val()fmt.Println(value)
}
執行任意命令
go-redis 還提供了一個執行任意命令或自定義命令的 Do 方法,特別是一些 go-redis 庫暫時不支持的命令都可以使用該方法執行。具體使用方法如下。
// doDemo rdb.Do 方法使用示例
func doDemo() {ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)defer cancel()// 直接執行命令獲取錯誤err := rdb.Do(ctx, "set", "key", 10, "EX", 3600).Err()fmt.Println(err)// 執行命令獲取結果val, err := rdb.Do(ctx, "get", "key").Result()fmt.Println(val, err)
}
redis.Nil
go-redis 庫提供了一個 redis.Nil 錯誤來表示 Key 不存在的錯誤。因此在使用 go-redis 時需要注意對返回錯誤的判斷。在某些場景下我們應該區別處理 redis.Nil 和其他不為 nil 的錯誤。
// getValueFromRedis redis.Nil判斷
func getValueFromRedis(key, defaultValue string) (string, error) {ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)defer cancel()val, err := rdb.Get(ctx, key).Result()if err != nil {// 如果返回的錯誤是key不存在if errors.Is(err, redis.Nil) {return defaultValue, nil}// 出其他錯了return "", err}return val, nil
}
其他示例
zset示例
下面的示例代碼演示了如何使用 go-redis 庫操作 zset。
// zsetDemo 操作zset示例
func zsetDemo() {// keyzsetKey := "language_rank"// value// 注意:v8版本使用[]*redis.Z;此處為v9版本使用[]redis.Zlanguages := []redis.Z{{Score: 90.0, Member: "Golang"},{Score: 98.0, Member: "Java"},{Score: 95.0, Member: "Python"},{Score: 97.0, Member: "JavaScript"},{Score: 99.0, Member: "C/C++"},}ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)defer cancel()// ZADDerr := rdb.ZAdd(ctx, zsetKey, languages...).Err()if err != nil {fmt.Printf("zadd failed, err:%v\n", err)return}fmt.Println("zadd success")// 把Golang的分數加10newScore, err := rdb.ZIncrBy(ctx, zsetKey, 10.0, "Golang").Result()if err != nil {fmt.Printf("zincrby failed, err:%v\n", err)return}fmt.Printf("Golang's score is %f now.\n", newScore)// 取分數最高的3個ret := rdb.ZRevRangeWithScores(ctx, zsetKey, 0, 2).Val()for _, z := range ret {fmt.Println(z.Member, z.Score)}// 取95~100分的op := &redis.ZRangeBy{Min: "95",Max: "100",}ret, err = rdb.ZRangeByScoreWithScores(ctx, zsetKey, op).Result()if err != nil {fmt.Printf("zrangebyscore failed, err:%v\n", err)return}for _, z := range ret {fmt.Println(z.Member, z.Score)}
}
執行上面的函數將得到如下輸出結果。
zadd success
Golang's score is 100.000000 now.
Golang 100
C/C++ 99
Java 98
Python 95
JavaScript 97
Java 98
C/C++ 99
Golang 100
掃描或遍歷所有key
在Redis中可以使用KEYS prefix*?命令按前綴查詢所有符合條件的 key,go-redis
庫中提供了Keys
方法實現類似查詢key的功能。
例如使用以下命令查詢以user:
為前綴的所有key(user:cart:00
、user:order:2023
等)。
vals, err := rdb.Keys(ctx, "user:*").Result()
但是如果需要掃描數百萬的 key ,那速度就會比較慢。這種場景下你可以使用Scan
命令來遍歷所有符合要求的 key。
// scanKeysDemo1 按前綴查找所有key示例
func scanKeysDemo1() {ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)defer cancel()var cursor uint64for {var keys []stringvar err error// 將redis中所有以prefix:為前綴的key都掃描出來keys, cursor, err = rdb.Scan(ctx, cursor, "prefix:*", 0).Result()if err != nil {panic(err)}for _, key := range keys {fmt.Println("key", key)}if cursor == 0 { // no more keysbreak}}
}
針對這種需要遍歷大量key的場景,go-redis
中提供了一個簡化方法——Iterator
,其使用示例如下。
// scanKeysDemo2 按前綴掃描key示例
func scanKeysDemo2() {ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)defer cancel()// 按前綴掃描keyiter := rdb.Scan(ctx, 0, "prefix:*", 0).Iterator()for iter.Next(ctx) {fmt.Println("keys", iter.Val())}if err := iter.Err(); err != nil {panic(err)}
}
例如,我們可以寫出一個將所有匹配指定模式的 key 刪除的示例。
// delKeysByMatch 按match格式掃描所有key并刪除
func delKeysByMatch(match string, timeout time.Duration) {ctx, cancel := context.WithTimeout(context.Background(), timeout)defer cancel()iter := rdb.Scan(ctx, 0, match, 0).Iterator()for iter.Next(ctx) {err := rdb.Del(ctx, iter.Val()).Err()if err != nil {panic(err)}}if err := iter.Err(); err != nil {panic(err)}
}
此外,對于 Redis 中的 set、hash、zset 數據類型,go-redis
?也支持類似的遍歷方法。
iter := rdb.SScan(ctx, "set-key", 0, "prefix:*", 0).Iterator()
iter := rdb.HScan(ctx, "hash-key", 0, "prefix:*", 0).Iterator()
iter := rdb.ZScan(ctx, "sorted-hash-key", 0, "prefix:*", 0).Iterator(
Pipeline
Redis Pipeline 允許通過使用單個 client-server-client 往返執行多個命令來提高性能。區別于一個接一個地執行100個命令,你可以將這些命令放入 pipeline 中,然后使用1次讀寫操作像執行單個命令一樣執行它們。這樣做的好處是節省了執行命令的網絡往返時間(RTT)。
y在下面的示例代碼中演示了使用 pipeline 通過一個 write + read 操作來執行多個命令。
pipe := rdb.Pipeline()incr := pipe.Incr(ctx, "pipeline_counter")
pipe.Expire(ctx, "pipeline_counter", time.Hour)cmds, err := pipe.Exec(ctx)
if err != nil {panic(err)
}// 在執行pipe.Exec之后才能獲取到結果
fmt.Println(incr.Val())
上面的代碼相當于將以下兩個命令一次發給 Redis Server 端執行,與不使用 Pipeline 相比能減少一次RTT。
INCR pipeline_counter
EXPIRE pipeline_counts 3600
或者,你也可以使用Pipelined
?方法,它會在函數退出時調用 Exec。
var incr *redis.IntCmdcmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {incr = pipe.Incr(ctx, "pipelined_counter")pipe.Expire(ctx, "pipelined_counter", time.Hour)return nil
})
if err != nil {panic(err)
}// 在pipeline執行后獲取到結果
fmt.Println(incr.Val())
我們可以遍歷 pipeline 命令的返回值依次獲取每個命令的結果。下方的示例代碼中使用pipiline一次執行了100個 Get 命令,在pipeline 執行后遍歷取出100個命令的執行結果。
cmds, err := rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {for i := 0; i < 100; i++ {pipe.Get(ctx, fmt.Sprintf("key%d", i))}return nil
})
if err != nil {panic(err)
}for _, cmd := range cmds {fmt.Println(cmd.(*redis.StringCmd).Val())
}
在那些我們需要一次性執行多個命令的場景下,就可以考慮使用 pipeline 來優化。
事務
Redis 是單線程執行命令的,因此單個命令始終是原子的,但是來自不同客戶端的兩個給定命令可以依次執行,例如在它們之間交替執行。但是,Multi/exec
能夠確保在multi/exec
兩個語句之間的命令之間沒有其他客戶端正在執行命令。
在這種場景我們需要使用 TxPipeline 或 TxPipelined 方法將 pipeline 命令使用?MULTI
?和EXEC
包裹起來。
// TxPipeline demo
pipe := rdb.TxPipeline()
incr := pipe.Incr(ctx, "tx_pipeline_counter")
pipe.Expire(ctx, "tx_pipeline_counter", time.Hour)
_, err := pipe.Exec(ctx)
fmt.Println(incr.Val(), err)// TxPipelined demo
var incr2 *redis.IntCmd
_, err = rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {incr2 = pipe.Incr(ctx, "tx_pipeline_counter")pipe.Expire(ctx, "tx_pipeline_counter", time.Hour)return nil
})
fmt.Println(incr2.Val(), err)
上面代碼相當于在一個RTT下執行了下面的redis命令:
MULTI
INCR pipeline_counter
EXPIRE pipeline_counts 3600
EXEC
Watch
我們通常搭配?WATCH
命令來執行事務操作。從使用WATCH
命令監視某個 key 開始,直到執行EXEC
命令的這段時間里,如果有其他用戶搶先對被監視的 key 進行了替換、更新、刪除等操作,那么當用戶嘗試執行EXEC
的時候,事務將失敗并返回一個錯誤,用戶可以根據這個錯誤選擇重試事務或者放棄事務。
Watch方法接收一個函數和一個或多個key作為參數。
Watch(fn func(*Tx) error, keys ...string) error
下面的代碼片段演示了 Watch 方法搭配 TxPipelined 的使用示例。
// watchDemo 在key值不變的情況下將其值+1
func watchDemo(ctx context.Context, key string) error {return rdb.Watch(ctx, func(tx *redis.Tx) error {n, err := tx.Get(ctx, key).Int()if err != nil && err != redis.Nil {return err}// 假設操作耗時5秒// 5秒內我們通過其他的客戶端修改key,當前事務就會失敗time.Sleep(5 * time.Second)_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {pipe.Set(ctx, key, n+1, time.Hour)return nil})return err}, key)
}
將上面的函數執行并打印其返回值,如果我們在程序運行后的5秒內修改了被 watch 的 key 的值,那么該事務操作失敗,返回redis: transaction failed
錯誤。
最后我們來看一個 go-redis 官方文檔中使用?GET
?、SET
和WATCH
命令實現一個 INCR 命令的完整示例。
// 此處rdb為初始化的redis連接客戶端
const routineCount = 100// 設置5秒超時
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()// increment 是一個自定義對key進行遞增(+1)的函數
// 使用 GET + SET + WATCH 實現,類似 INCR
increment := func(key string) error {txf := func(tx *redis.Tx) error {// 獲得當前值或零值n, err := tx.Get(ctx, key).Int()if err != nil && err != redis.Nil {return err}// 實際操作(樂觀鎖定中的本地操作)n++// 僅在監視的Key保持不變的情況下運行_, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {// pipe 處理錯誤情況pipe.Set(ctx, key, n, 0)return nil})return err}// 最多重試100次for retries := routineCount; retries > 0; retries-- {err := rdb.Watch(ctx, txf, key)if err != redis.TxFailedErr {return err}// 樂觀鎖丟失}return errors.New("increment reached maximum number of retries")
}// 開啟100個goroutine并發調用increment
// 相當于對key執行100次遞增
var wg sync.WaitGroup
wg.Add(routineCount)
for i := 0; i < routineCount; i++ {go func() {defer wg.Done()if err := increment("counter3"); err != nil {fmt.Println("increment error:", err)}}()
}
wg.Wait()n, err := rdb.Get(ctx, "counter3").Int()
fmt.Println("最終結果:", n, err)
copy
在這個示例中使用了?redis.TxFailedErr
?來檢查事務是否失敗。
更多詳情請查閱官方文檔。