利用Redis?lua實現(xiàn)高效讀寫鎖的代碼實例
前言
讀寫鎖的好處就是能幫助客戶讀到的數(shù)據(jù)一定是最新的,寫鎖是排他鎖,而讀鎖是一個共享鎖,如果寫鎖一直存在,那么讀取數(shù)據(jù)就要一直等待,直到寫入數(shù)據(jù)完成才能看到,保證了數(shù)據(jù)的一致性
一、為什么使用Lua
Lua腳本是高并發(fā)、高性能的必備腳本語言, 大部分的開源框架(如:redission)中的分布式鎖組件,都是用純lua腳本實現(xiàn)的。
那么,為什么要使用Lua語言來實現(xiàn)分布式鎖呢?我們從一個案例看起:
所以,只有確保判斷鎖和刪除鎖是一步操作時,才能避免上面的問題,才能確保原子性。
其實很簡單,首先獲取鎖對應的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。雖然看似做了兩件事,但是卻只有一個完整的原子操作。
第一行代碼,我們寫了一個簡單的 Lua 腳本代碼; 第二行代碼,我們將Lua代碼傳到 edis.eval()方法里,并使參數(shù) KEYS[1] 賦值為 lockKey,ARGV[1] 賦值為 requestId,eval() 方法是將Lua代碼交給 Redis 服務端執(zhí)行。
二、執(zhí)行流程
加鎖和刪除鎖的操作,使用純 Lua 進行封裝,保障其執(zhí)行時候的原子性。
基于純Lua腳本實現(xiàn)分布式鎖的執(zhí)行流程,大致如下:
三、代碼詳解
lua\lock.lua
-- KEYS = [LOCK_KEY, LOCK_INTENT] -- ARGV = [LOCK_ID, TTL] local t = redis.call('TYPE', KEYS[1])["ok"] if t == "string" then return redis.call('PTTL', KEYS[1]) end if redis.call("EXISTS", KEYS[2]) == 1 then return redis.call('PTTL', KEYS[2]) end redis.call('SADD', KEYS[1], ARGV[1]) redis.call('PEXPIRE', KEYS[1], ARGV[2]) return nil
-- KEYS = [LOCK_KEY, LOCK_INTENT]
和-- ARGV = [LOCK_ID, TTL, ENABLE_LOCK_INTENT]
if not redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2], "NX") then
行首使用了redis.call
函數(shù)調(diào)用,將 LOCK_KEY 和 LOCK_ID 存儲到 Redis 中,并設置過期時間為 TTL。如果設置失敗,則進入條件內(nèi)部。- 在條件內(nèi)部,判斷 ENABLE_LOCK_INTENT 的值。如果為 1,則執(zhí)行
redis.call("SET", KEYS[2], 1, "PX", ARGV[2])
,將 LOCK_INTENT 鍵設置為 1,并設置與 LOCK_KEY 相同的過期時間。這是為了表示鎖被占用的意圖。 - 返回
redis.call("PTTL", KEYS[1])
,即 LOCK_KEY 的剩余過期時間,以毫秒為單位。這是為了告知調(diào)用方鎖已被占用,返回鎖的剩余過期時間。 - 若上述條件都不滿足,則執(zhí)行
redis.call("DEL", KEYS[2])
,刪除 LOCK_INTENT 鍵。 - 返回
nil
,表示鎖已成功獲取。
它首先嘗試通過 SET 命令將 LOCK_KEY 存儲到 Redis 中,如果設置失敗,則表示鎖已被其他進程占用,返回鎖的剩余過期時間。如果設置成功,則刪除 LOCK_INTENT 鍵,表示鎖已成功獲取
lua\refresh.lua
-- KEYS = [LOCK_KEY] -- ARGV = [LOCK_ID, TTL] local t = redis.call('TYPE', KEYS[1])["ok"] if (t == "string" and redis.call('GET', KEYS[1]) ~= ARGV[1]) or (t == "set" and redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 0) or (t == "none") then return 0 end return redis.call('PEXPIRE', KEYS[1], ARGV[2])
- 延長鎖的時間
lua\rlock.lua
-- KEYS = [LOCK_KEY, LOCK_INTENT] -- ARGV = [LOCK_ID, TTL] local t = redis.call('TYPE', KEYS[1])["ok"] if t == "string" then return redis.call('PTTL', KEYS[1]) end if redis.call("EXISTS", KEYS[2]) == 1 then return redis.call('PTTL', KEYS[2]) end redis.call('SADD', KEYS[1], ARGV[1]) redis.call('PEXPIRE', KEYS[1], ARGV[2]) return nil
local t = redis.call('TYPE', KEYS[1])["ok"]
通過TYPE
命令獲取鍵的類型,并將結(jié)果存儲在變量t
中。使用條件邏輯判斷鎖的狀態(tài):
- 如果
t
是字符串,則返回PTTL
命令的結(jié)果,即鎖的剩余過期時間。 - 如果
LOCK_INTENT
鍵存在,則返回PTTL
命令的結(jié)果,即鎖占用意圖的剩余過期時間。
- 如果
由于以上條件都不滿足,即鎖未被占用,將鎖 ID (
ARGV[1]
) 添加到LOCK_KEY
集合中。使用
PEXPIRE
命令設置LOCK_KEY
的過期時間為ARGV[2]
(以毫秒為單位)。返回
nil
,表示鎖已成功獲取。
lua\unlock.lua
-- KEYS = [LOCK_KEY] -- ARGV = [LOCK_ID] local t = redis.call('TYPE', KEYS[1])["ok"] if t == "string" and redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) elseif t == "set" and redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 then redis.call('SREM', KEYS[1], ARGV[1]) if redis.call('SCARD', KEYS[1]) == 0 then return redis.call('DEL', KEYS[1]) end end return 1
- 檢查指定鍵的類型,如果是字符串并且鍵的值等于給定的
ARGV
值,則刪除該鍵。 - 如果指定鍵的類型是集合,并且集合中包含給定的
ARGV
值,則將該值從集合中移除。隨后,如果集合中不再包含任何元素,則刪除該鍵。
寫優(yōu)先還是讀優(yōu)先?
寫鎖會阻塞讀鎖,所以是寫優(yōu)先
寫鎖是如何阻塞寫鎖的?
如果當前的寫鎖已經(jīng)被占用,其他寫鎖的獲取請求會被阻塞,因為在釋放鎖的邏輯中,會先判斷鎖的類型,如果是寫鎖,則會判斷當前鎖的值是否符合預期,從而判斷能否刪除該鎖。
讀鎖與讀鎖之間互斥嗎?
對于讀鎖而言,多個讀鎖之間是可以并發(fā)持有的,因此讀鎖之間默認是不會互斥的,可以同時執(zhí)行讀操作。
寫鎖會有被餓死的情況嗎?
寫優(yōu)先鎖可以保證寫線程不會餓死,但是如果一直有寫線程獲取寫鎖,讀線程也會被「餓死」。
既然不管優(yōu)先讀鎖還是寫鎖,對方可能會出現(xiàn)餓死問題,那么我們就不偏袒任何一方,搞個「公平讀寫鎖」。
公平讀寫鎖比較簡單的一種方式是:用隊列把獲取鎖的線程排隊,不管是寫線程還是讀線程都按照先進先出的原則加鎖即可,這樣讀線程仍然可以并發(fā),也不會出現(xiàn)「饑餓」的現(xiàn)象。
抽象lock類
import ( "context" "errors" "time" "github.com/redis/go-redis/v9" ) var _ context.Context = (*Lock)(nil) // Lock represents a lock with context. type Lock struct { redis redis.Scripter id string ttl time.Duration key string log LogFunc ctx context.Context cancel context.CancelFunc } // ID returns the id value set by the lock. func (l *Lock) ID() string { return l.id } // Key returns the key value set by the lock. func (l *Lock) Key() string { return l.key } func (l *Lock) Deadline() (deadline time.Time, ok bool) { return l.ctx.Deadline() } func (l *Lock) Done() <-chan struct{} { return l.ctx.Done() } func (l *Lock) Err() error { return l.ctx.Err() } func (l *Lock) Value(key any) any { return l.ctx.Value(key) } // Unlock unlocks. func (l *Lock) Unlock() { l.cancel() _, err := scriptUnlock.Run(context.Background(), l.redis, []string{l.key}, l.id).Result() if err != nil { l.log("[ERROR] unlock %q %s: %v", l.key, l.id, err) } } func (l *Lock) refreshTTL(left time.Time) { defer l.cancel() refresh := l.updateTTL() for { diff := time.Since(left) select { case <-l.ctx.Done(): return case <-time.After(-diff): // cant refresh return case <-time.After(refresh): status, err := scriptRefresh.Run(l.ctx, l.redis, []string{l.key}, l.id, l.ttl.Milliseconds()).Int() if err != nil { if errors.Is(err, context.Canceled) { return } refresh = refreshTimeout l.log("[ERROR] refresh key %q %s: %v", l.key, l.id, err) continue } left = l.leftTTL() refresh = l.updateTTL() if status == 0 { l.log("[ERROR] refresh key %q %s already expired", l.key, l.id) return } } } } func (l *Lock) leftTTL() time.Time { return time.Now().Add(l.ttl) } func (l *Lock) updateTTL() time.Duration { return l.ttl / 2 }
ID()
:返回鎖的ID。Key()
:返回鎖的鍵名。Deadline()
:返回鎖的截止時間和標志,如果沒有設置則返回零值。Done()
:返回一個通道,在鎖的上下文被取消或者鎖過期后會被關(guān)閉。Err()
:返回鎖的錯誤狀態(tài)。Value(key any) any
:返回一個鍵關(guān)聯(lián)的值,用于傳遞上下文相關(guān)的數(shù)據(jù)。Unlock()
:解鎖操作,會取消鎖的上下文,并調(diào)用Redis的腳本解鎖操作。refreshTTL(left time.Time)
:刷新鎖的過期時間,定期更新Redis中鎖的過期時間,直到鎖的上下文被取消、鎖過期或無法繼續(xù)刷新為止。leftTTL()
:返回鎖的剩余過期時間。updateTTL()
:更新刷新鎖的間隔時間。每次減少一半
為什么需要為什么l.ttl / 2
這是為了實現(xiàn)鎖的自動續(xù)約。通過定期刷新鎖的過期時間,可以確保鎖在使用過程中不會過期而被意外釋放。
這種做法可以在以下情況下帶來一些好處:
- 減少鎖的續(xù)約操作對Redis的壓力:由于續(xù)約操作是相對較昂貴的,通過將過期時間縮短為原來的一半,可以降低續(xù)約的頻率,從而減少對Redis的請求,減少了網(wǎng)絡和計算資源的消耗。
- 避免長時間持有鎖帶來的問題:如果某個持有鎖的進程/線程發(fā)生故障或延遲,導致無法及時釋放鎖,那么其他進程可能會長時間等待獲取該鎖,造成資源浪費。通過定期刷新鎖的過期時間,可以在鎖即將過期之前及時釋放鎖,降低該問題的風險。
Options
package redismutex import ( "context" "log" "os" "sync" "time" ) const ( lenBytesID = 16 refreshTimeout = time.Millisecond * 500 defaultKeyTTL = time.Second * 4 ) var ( globalMx sync.RWMutex globalLog = func() LogFunc { l := log.New(os.Stderr, "redismutex: ", log.LstdFlags) return func(format string, v ...any) { l.Printf(format, v...) } }() ) // LogFunc type is an adapter to allow the use of ordinary functions as LogFunc. type LogFunc func(format string, v ...any) // NopLog logger does nothing var NopLog = LogFunc(func(string, ...any) {}) // SetLog sets the logger. func SetLog(l LogFunc) { globalMx.Lock() defer globalMx.Unlock() if l != nil { globalLog = l } } // MutexOption is the option for the mutex. type MutexOption func(*mutexOptions) type mutexOptions struct { name string ttl time.Duration lockIntent bool log LogFunc } // WithTTL sets the TTL of the mutex. func WithTTL(ttl time.Duration) MutexOption { return func(o *mutexOptions) { if ttl >= time.Second*2 { o.ttl = ttl } } } // WithLockIntent sets the lock intent. func WithLockIntent() MutexOption { return func(o *mutexOptions) { o.lockIntent = true } } // LockOption is the option for the lock. type LockOption func(*lockOptions) type lockOptions struct { ctx context.Context key string lockIntentKey string enableLockIntent int ttl time.Duration log LogFunc } func newLockOptions(m mutexOptions, opt ...LockOption) lockOptions { opts := lockOptions{ ctx: context.Background(), key: m.name, enableLockIntent: boolToInt(m.lockIntent), ttl: m.ttl, log: m.log, } for _, o := range opt { o(&opts) } opts.lockIntentKey = lockIntentKey(opts.key) return opts } // WithKey sets the key of the lock. func WithKey(key string) LockOption { return func(o *lockOptions) { if key != "" { o.key += ":" + key } } } // WithContext sets the context of the lock. func WithContext(ctx context.Context) LockOption { return func(o *lockOptions) { if ctx != nil { o.ctx = ctx } } } func boolToInt(b bool) int { if b { return 1 } return 0 } func lockIntentKey(key string) string { return key + ":lock-intent" }
SetLog(l LogFunc)
:設置日志記錄器。WithTTL(ttl time.Duration)
:設置互斥鎖的生存時間(TTL)選項。WithLockIntent()
:設置鎖意圖選項。newLockOptions(m mutexOptions, opt ...LockOption)
:創(chuàng)建鎖的選項。WithKey(key string)
:設置鎖的鍵選項。WithContext(ctx context.Context)
:設置鎖的上下文選項。lockIntentKey(key string)
:為給定的鎖鍵生成鎖意圖鍵。
可以通過設置選項來控制互斥鎖的行為和屬性,如生存時間、鎖意圖、上下文等。還提供了一些實用函數(shù)和類型,用于管理互斥鎖和生成選項
redismutex
// Package redismutex provides a distributed rw mutex. package redismutex import ( "context" "crypto/rand" "embed" "encoding/hex" "errors" "sync" "time" "github.com/redis/go-redis/v9" ) var ErrLock = errors.New("redismutex: lock not obtained") var ( //go:embed lua lua embed.FS scriptRLock *redis.Script scriptLock *redis.Script scriptRefresh *redis.Script scriptUnlock *redis.Script ) func init() { scriptRLock = redis.NewScript(mustReadFile("rlock.lua")) scriptLock = redis.NewScript(mustReadFile("lock.lua")) scriptRefresh = redis.NewScript(mustReadFile("refresh.lua")) scriptUnlock = redis.NewScript(mustReadFile("unlock.lua")) } // A RWMutex is a distributed mutual exclusion lock. type RWMutex struct { redis redis.Scripter opts mutexOptions id struct { sync.Mutex buf []byte } } // NewMutex creates a new distributed mutex. func NewMutex(rc redis.Scripter, name string, opt ...MutexOption) *RWMutex { globalMx.RLock() defer globalMx.RUnlock() opts := mutexOptions{ name: name, ttl: defaultKeyTTL, log: globalLog, } for _, o := range opt { o(&opts) } rw := &RWMutex{ redis: rc, opts: opts, } rw.id.buf = make([]byte, lenBytesID) return rw } // TryRLock tries to lock for reading and reports whether it succeeded. func (m *RWMutex) TryRLock(opt ...LockOption) (*Lock, bool) { opts := newLockOptions(m.opts, opt...) ctx, _, err := m.rlock(opts) if err != nil { if !errors.Is(err, ErrLock) { m.opts.log("[ERROR] try-read-lock key %q: %v", opts.key, err) } return nil, false } return ctx, true } // RLock locks for reading. func (m *RWMutex) RLock(opt ...LockOption) (*Lock, bool) { opts := newLockOptions(m.opts, opt...) ctx, ttl, err := m.rlock(opts) if err == nil { return ctx, true } if !errors.Is(err, ErrLock) { m.opts.log("[ERROR] read-lock key %q: %v", opts.key, err) return nil, false } for { select { case <-opts.ctx.Done(): m.opts.log("[ERROR] read-lock key %q: %v", opts.key, opts.ctx.Err()) return nil, false case <-time.After(ttl): ctx, ttl, err = m.rlock(opts) if err == nil { return ctx, true } if !errors.Is(err, ErrLock) { m.opts.log("[ERROR] read-lock key %q: %v", opts.key, err) return nil, false } continue } } } // TryLock tries to lock for writing and reports whether it succeeded. func (m *RWMutex) TryLock(opt ...LockOption) (*Lock, bool) { opts := newLockOptions(m.opts, opt...) opts.enableLockIntent = 0 // force disable lock intent ctx, _, err := m.lock(opts) if err != nil { if !errors.Is(err, ErrLock) { m.opts.log("[ERROR] try-lock key %q: %v", opts.key, err) } return nil, false } return ctx, true } // Lock locks for writing. func (m *RWMutex) Lock(opt ...LockOption) (*Lock, bool) { opts := newLockOptions(m.opts, opt...) ctx, ttl, err := m.lock(opts) if err == nil { return ctx, true } if !errors.Is(err, ErrLock) { m.opts.log("[ERROR] lock key %q: %v", opts.key, err) return nil, false } for { select { case <-opts.ctx.Done(): m.opts.log("[ERROR] lock key %q: %v", opts.key, opts.ctx.Err()) return nil, false case <-time.After(ttl): ctx, ttl, err = m.lock(opts) if err == nil { return ctx, true } if !errors.Is(err, ErrLock) { m.opts.log("[ERROR] lock key %q: %v", opts.key, err) return nil, false } continue } } } func (m *RWMutex) lock(opts lockOptions) (*Lock, time.Duration, error) { id, err := m.randomID() if err != nil { return nil, 0, err } pTTL, err := scriptLock.Run(opts.ctx, m.redis, []string{opts.key, opts.lockIntentKey}, id, opts.ttl.Milliseconds(), opts.enableLockIntent).Result() leftTTL := time.Now().Add(opts.ttl) if err == nil { return nil, time.Duration(pTTL.(int64)) * time.Millisecond, ErrLock } if err != redis.Nil { return nil, 0, err } ctx, cancel := context.WithCancel(opts.ctx) lock := &Lock{ redis: m.redis, id: id, ttl: opts.ttl, key: opts.key, log: opts.log, ctx: ctx, cancel: cancel, } go lock.refreshTTL(leftTTL) return lock, 0, nil } func (m *RWMutex) rlock(opts lockOptions) (*Lock, time.Duration, error) { id, err := m.randomID() if err != nil { return nil, 0, err } pTTL, err := scriptRLock.Run(opts.ctx, m.redis, []string{opts.key, opts.lockIntentKey}, id, opts.ttl.Milliseconds()).Result() leftTTL := time.Now().Add(opts.ttl) if err == nil { return nil, time.Duration(pTTL.(int64)) * time.Millisecond, ErrLock } if err != redis.Nil { return nil, 0, err } ctx, cancel := context.WithCancel(opts.ctx) lock := &Lock{ redis: m.redis, id: id, ttl: opts.ttl, key: opts.key, log: opts.log, ctx: ctx, cancel: cancel, } go lock.refreshTTL(leftTTL) return lock, 0, nil } // randomID generates a random hex string with 16 bytes. func (m *RWMutex) randomID() (string, error) { m.id.Lock() defer m.id.Unlock() _, err := rand.Read(m.id.buf) if err != nil { return "", err } return hex.EncodeToString(m.id.buf), nil } func mustReadFile(filename string) string { b, err := lua.ReadFile("lua/" + filename) if err != nil { panic(err) } return string(b) }
- 通過
NewMutex
函數(shù)創(chuàng)建一個新的分布式互斥鎖。該函數(shù)接受 Redis 客戶端、鎖的名稱和一系列選項作為參數(shù),返回一個 RWMutex 結(jié)構(gòu)體實例。 - 通過
RLock
和Lock
方法來獲取讀鎖和寫鎖。如果無法立即獲取鎖,則會阻塞等待,直到獲取成功或者上下文取消。 - 通過
TryRLock
和TryLock
方法來嘗試獲取讀鎖和寫鎖,如果無法立即獲取鎖則立即返回失敗,不會阻塞。 - 該包實現(xiàn)了一個
Lock
結(jié)構(gòu)體,包含了鎖相關(guān)的信息和操作方法,比如刷新鎖的過期時間。 - 使用
redis.Script
來執(zhí)行 Lua 腳本,通過 Redis 客戶端執(zhí)行相應的 Redis 命令。 - 使用了
crypto/rand
包來生成隨機的鎖標識符。 - 最終的
mustReadFile
函數(shù)用于讀取嵌入的 Lua 腳本文件。
測試用例
package redismutex import ( "context" "errors" "log" "strings" "testing" "time" "github.com/redis/go-redis/v9" ) func init() { SetLog(func(format string, a ...any) { if strings.HasPrefix(format, "[ERROR]") { log.Fatalf(format, a...) } }) } func TestMutex(t *testing.T) { t.Parallel() const lockKey = "mutex" rc := redis.NewClient(redisOpts()) prep(t, rc, lockKey) mx := NewMutex(rc, lockKey) lock, ok := mx.Lock() if exp, got := true, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } defer lock.Unlock() assertTTL(t, rc, lockKey, defaultKeyTTL) // try again _, ok = mx.TryLock() if exp, got := false, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } _, ok = mx.TryRLock() if exp, got := false, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } // manually unlock lock.Unlock() // lock again lock, ok = mx.Lock() if exp, got := true, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } defer lock.Unlock() } func TestRWMutex(t *testing.T) { t.Parallel() const lockKey = "rw_mutex" rc := redis.NewClient(redisOpts()) prep(t, rc, lockKey) mx := NewMutex(rc, lockKey) lock, ok := mx.RLock() if exp, got := true, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } defer lock.Unlock() assertTTL(t, rc, lockKey, defaultKeyTTL) // try again _, ok = mx.TryLock() if exp, got := false, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } // try rlock rlock, ok := mx.TryRLock() if exp, got := true, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } rlock.Unlock() // manually unlock lock.Unlock() // lock again lock, ok = mx.Lock() if exp, got := true, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } defer lock.Unlock() } func TestRWMutex_LockIntent(t *testing.T) { t.Parallel() const lockKey = "lock_intent_mutex" rc := redis.NewClient(redisOpts()) prep(t, rc, lockKey) mx := NewMutex(rc, lockKey, WithLockIntent()) lock, ok := mx.RLock() if exp, got := true, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } defer lock.Unlock() // mark lock intent _, _, err := mx.lock(newLockOptions(mx.opts)) if exp, got := ErrLock, err; !errors.Is(got, exp) { t.Fatalf("exp %v, got %v", exp, got) } // try rlock _, ok = mx.TryRLock() if exp, got := false, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } // manually unlock lock.Unlock() // lock write lock, ok = mx.Lock() if exp, got := true, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } lock.Unlock() // remove lock intent // lock again lock, ok = mx.RLock() if exp, got := true, ok; exp != got { t.Fatalf("exp %v, got %v", exp, got) } defer lock.Unlock() } func TestRWMutex_ID(t *testing.T) { t.Parallel() rw := &RWMutex{} rw.id.buf = make([]byte, lenBytesID) id, _ := rw.randomID() if exp, got := 32, len(id); exp != got { t.Fatalf("exp %v, got %v", exp, got) } } func prep(t *testing.T, rc *redis.Client, key string) { t.Cleanup(func() { for _, v := range []string{key, lockIntentKey(key)} { if err := rc.Del(context.Background(), v).Err(); err != nil { t.Fatal(err) } } if err := rc.Close(); err != nil { t.Fatal(err) } }) } func assertTTL(t *testing.T, rc *redis.Client, key string, exp time.Duration) { t.Helper() got, err := rc.TTL(context.Background(), key).Result() if exp, got := (any)(nil), err; exp != got { t.Fatalf("exp %v, got %v", exp, got) } delta := got - exp if delta < 0 { delta = 1 - delta } if delta > time.Second { t.Fatalf("exp ~%v, got %v", exp, got) } } func redisOpts() *redis.Options { return &redis.Options{ Network: "tcp", Addr: "0.0.0.0:6379", DB: 9, } }
以上就是利用Redis lua實現(xiàn)高效讀寫鎖的代碼實例的詳細內(nèi)容,更多關(guān)于Redis lua讀寫鎖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
kubernetes環(huán)境部署單節(jié)點redis數(shù)據(jù)庫的方法
這篇文章主要介紹了kubernetes環(huán)境部署單節(jié)點redis數(shù)據(jù)庫的方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01Redis中有序集合的內(nèi)部實現(xiàn)方式的詳細介紹
本文主要介紹了Redis中有序集合的內(nèi)部實現(xiàn)方式的詳細介紹,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03Redis中的數(shù)據(jù)結(jié)構(gòu)跳表詳解
跳表是一種基于并聯(lián)的鏈表結(jié)構(gòu),用于在有序元素序列中快速查找元素的數(shù)據(jù)結(jié)構(gòu),本文給大家介紹Redis中的數(shù)據(jù)結(jié)構(gòu)跳表,感興趣的朋友跟隨小編一起看看吧2024-06-06Redis高級數(shù)據(jù)類型Hyperloglog、Bitmap的使用
很多小伙伴在面試中都會被問道 Redis的常用數(shù)據(jù)結(jié)構(gòu)有哪些?可能很大一部分回答都是 string、hash、list、set、zset,但其實還有Hyperloglog和Bitmap,本文就來介紹一下2021-05-05