go-redis Pipeline與事務(wù)的實(shí)現(xiàn)示例
1 背景與動(dòng)機(jī)
在高并發(fā)服務(wù)中,網(wǎng)絡(luò)往返 (RTT) 與 一致性 是兩大核心痛點(diǎn)。
- Pipeline —— 把多條命令打包,一次發(fā)網(wǎng)絡(luò)、一并回包 → 減少 RTT、提高吞吐。
- 事務(wù) (MULTI/EXEC) —— 多條命令串行、原子執(zhí)行 → 保證一致性。
- Watch + Tx —— 給事務(wù)加上 樂觀鎖,并發(fā)安全地修改共享數(shù)據(jù)。
go-redis v9 對(duì)上述三者均提供了優(yōu)雅 API,下面逐一拆解。
2 Pipeline:降低 RTT 的秘密武器
2.1 基礎(chǔ)用法
// 初始化
pipe := rdb.Pipeline()
// 批量寫 seat:0~4
for i := 0; i < 5; i++ {
pipe.Set(ctx, fmt.Sprintf("seat:%d", i), fmt.Sprintf("#%d", i), 0)
}
// 真正發(fā)送
cmds, err := pipe.Exec(ctx)
if err != nil { panic(err) }
for _, c := range cmds {
fmt.Printf("%s; ", c.(*redis.StatusCmd).Val()) // OK;OK;OK;...
}
?? 只有 Exec() 之后,c.Val() 才有結(jié)果;錯(cuò)誤也集中由 Exec 返回。
批量讀寫混用
pipe = rdb.Pipeline() g0 := pipe.Get(ctx, "seat:0") g3 := pipe.Get(ctx, "seat:3") g4 := pipe.Get(ctx, "seat:4") _, _ = pipe.Exec(ctx) fmt.Println(g0.Val(), g3.Val(), g4.Val()) // #0 #3 #4
2.2 自動(dòng)化Pipelined()
var g0, g3, g4 *redis.StringCmd
_, err := rdb.Pipelined(ctx, func(p redis.Pipeliner) error {
g0 = p.Get(ctx, "seat:0")
g3 = p.Get(ctx, "seat:3")
g4 = p.Get(ctx, "seat:4")
return nil
})
if err != nil { panic(err) }
fmt.Println(g0.Val(), g3.Val(), g4.Val())
優(yōu)勢:自動(dòng) Exec、代碼更簡潔,非常適合服務(wù)層一次性批量操作。
2.3 性能實(shí)測 & 調(diào)優(yōu)
| 批量大小 | QPS (單核) | RTT (平均) |
|---|---|---|
| 單命令 | 80 k/s | 0.15 ms |
| 50 條 | 310 k/s | 0.04 ms |
| 200 條 | 340 k/s | 0.05 ms |
| 500 條 | 300 k/s | 0.09 ms |
- 最佳區(qū)間 50-200:吞吐高且單包不至于過大。
- 并發(fā)寫場景可 每個(gè) Goroutine 維護(hù)獨(dú)立 Pipeline。
- 遇到 context.DeadlineExceeded 說明批量過大或超時(shí)過短。
3 事務(wù):一次提交,全部成功
3.1TxPipeline()基礎(chǔ)
tx := rdb.TxPipeline()
tx.IncrBy(ctx, "counter:1", 1)
tx.IncrBy(ctx, "counter:2", 2)
tx.IncrBy(ctx, "counter:3", 3)
cmds, err := tx.Exec(ctx)
if err != nil { panic(err) }
for _, c := range cmds {
fmt.Println(c.(*redis.IntCmd).Val()) // 1 2 3
}
3.2TxPipelined()回調(diào)
var c1, c2, c3 *redis.IntCmd
_, err := rdb.TxPipelined(ctx, func(t redis.Pipeliner) error {
c1 = t.IncrBy(ctx, "counter:1", 1)
c2 = t.IncrBy(ctx, "counter:2", 2)
c3 = t.IncrBy(ctx, "counter:3", 3)
return nil
})
if err != nil { panic(err) }
fmt.Println(c1.Val(), c2.Val(), c3.Val()) // 2 4 6
3.3 事務(wù) vs Lua 腳本
| 特性 | 事務(wù) (MULTI/EXEC) | Lua 腳本 |
|---|---|---|
| 原子性 | ? | ? |
| 復(fù)雜邏輯 | 一般 | 強(qiáng)大 |
| 可讀性 | 高(Go 代碼) | 中 |
| 調(diào)試 & 監(jiān)控 | 簡單 | 略復(fù)雜 |
| 性能 | 好 | 極好(單指令) |
結(jié)論:邏輯簡單 → 事務(wù);多 Key、復(fù)雜判斷 → Lua。
4 樂觀鎖:Watch 機(jī)制剖析
在并發(fā)環(huán)境修改同一 Key,需防止 “讀-改-寫” 期間被別人修改。WATCH 就是解決方案。
4.1 完整重試模型
const maxRetry = 1000
for i := 0; i < maxRetry; i++ {
err := rdb.Watch(ctx, func(tx *redis.Tx) error {
// 1) 讀取
path, err := tx.Get(ctx, "shellpath").Result()
if err != nil && err != redis.Nil { return err }
// 2) 業(yè)務(wù)計(jì)算
newPath := path + ":/usr/mycmds/"
// 3) 嘗試寫入(事務(wù))
_, err = tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
p.Set(ctx, "shellpath", newPath, 0)
return nil
})
return err
}, "shellpath")
if err == nil { break } // 成功
if err == redis.TxFailedErr { continue } // 沖突,重試
panic(err) // 其他錯(cuò)誤
}
4.2 常見坑與最佳實(shí)踐
| 坑 | 現(xiàn)象 | 解決方案 |
|---|---|---|
| Watch 區(qū)間耗時(shí)過長 | 沖突率飆升 | 減少業(yè)務(wù)邏輯 / 降重 |
| 忘記重試 | 數(shù)據(jù)丟失或未更新 | 封裝通用 RetryTx |
| 批量 Watch 多 Key | 死鎖概率增大 | 拆分 Key 或 Lua |
5 生產(chǎn)級(jí) Checklist
- Pipeline 批量:50-200 條最優(yōu);阻塞命令 (BLPOP) 另開連接。
- 事務(wù)重試:封裝帶退避 (exponential back-off) 的 Retry。
- 連接池:PoolSize = CPU*10,MinIdleConns ≈ 20% PoolSize。
- 超時(shí):DialTimeout 100ms、Read/WriteTimeout 200ms 典型值。
- 可觀測:redisotel.InstrumentTracing/Metrics 接入 OTel。
- 冪等命令:重試需確保無副作用。
- Lua 腳本:庫存扣減、搶紅包等使用腳本更穩(wěn)。
- RESP3:如 Redis ≥ 6.0,可設(shè)置 Protocol: 3 享受 Map/Push 類型。
6 結(jié)語
- Pipeline 帶來吞吐提升,適合大量寫入與批量讀寫。
- 事務(wù) 提供原子操作,確保數(shù)據(jù)一致。
- Watch 則在并發(fā)場景下守護(hù)一致性。
合理組合三者,配合連接池調(diào)優(yōu)與可觀測監(jiān)控,你就能構(gòu)建 既快又穩(wěn) 的 Redis 訪問層。祝編碼愉快,TPS 飆升!
到此這篇關(guān)于go-redis Pipeline與事務(wù)的實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)go-redis Pipeline與事務(wù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
基于Golang實(shí)現(xiàn)統(tǒng)一加載資源的入口
當(dāng)我們需要在?main?函數(shù)中做一些初始化的工作,比如初始化日志,初始化配置文件,都需要統(tǒng)一初始化入口函數(shù),所以本文就來編寫一個(gè)統(tǒng)一加載資源的入口吧2023-05-05
Golang timer可能造成的內(nèi)存泄漏問題分析
本文探討了Golang中timer可能造成的內(nèi)存泄漏問題,通過分析一段代碼,解釋了為什么協(xié)程在調(diào)用timer.Stop()后無法正常退出,文章指出,timer.Stop()并不關(guān)閉Channel,導(dǎo)致協(xié)程無法繼續(xù)執(zhí)行,最后,提出了一種修復(fù)方法,并呼吁大家關(guān)注和分享2024-12-12
Go計(jì)算某段代碼運(yùn)行所耗時(shí)間簡單實(shí)例
這篇文章主要給大家介紹了關(guān)于Go計(jì)算某段代碼運(yùn)行所耗時(shí)間的相關(guān)資料,主要介紹了Golang記錄計(jì)算函數(shù)執(zhí)行耗時(shí)、運(yùn)行時(shí)間的一個(gè)簡單方法,文中給出了詳細(xì)的代碼示例,需要的朋友可以參考下2023-11-11
golang微服務(wù)框架kratos實(shí)現(xiàn)Socket.IO服務(wù)的方法
本文主要介紹了golang微服務(wù)框架kratos實(shí)現(xiàn)Socket.IO服務(wù)的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
Go和Java算法詳析之分?jǐn)?shù)到小數(shù)
這篇文章主要給大家介紹了關(guān)于Go和Java算法詳析之分?jǐn)?shù)到小數(shù)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-08-08

