欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

利用Redis?lua實(shí)現(xiàn)高效讀寫鎖的代碼實(shí)例

 更新時(shí)間:2024年01月31日 10:40:59   作者:Goland貓  
這篇文章給大家介紹了如何利用Redis?lua實(shí)現(xiàn)高效的讀寫鎖,讀寫鎖的好處就是能幫助客戶讀到的數(shù)據(jù)一定是最新的,寫鎖是排他鎖,而讀鎖是一個(gè)共享鎖,需要的朋友可以參考下

前言

讀寫鎖的好處就是能幫助客戶讀到的數(shù)據(jù)一定是最新的,寫鎖是排他鎖,而讀鎖是一個(gè)共享鎖,如果寫鎖一直存在,那么讀取數(shù)據(jù)就要一直等待,直到寫入數(shù)據(jù)完成才能看到,保證了數(shù)據(jù)的一致性

一、為什么使用Lua

Lua腳本是高并發(fā)、高性能的必備腳本語言, 大部分的開源框架(如:redission)中的分布式鎖組件,都是用純lua腳本實(shí)現(xiàn)的。

那么,為什么要使用Lua語言來實(shí)現(xiàn)分布式鎖呢?我們從一個(gè)案例看起:

所以,只有確保判斷鎖和刪除鎖是一步操作時(shí),才能避免上面的問題,才能確保原子性。

其實(shí)很簡(jiǎn)單,首先獲取鎖對(duì)應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。雖然看似做了兩件事,但是卻只有一個(gè)完整的原子操作。

第一行代碼,我們寫了一個(gè)簡(jiǎn)單的 Lua 腳本代碼; 第二行代碼,我們將Lua代碼傳到 edis.eval()方法里,并使參數(shù) KEYS[1] 賦值為 lockKey,ARGV[1] 賦值為 requestId,eval() 方法是將Lua代碼交給 Redis 服務(wù)端執(zhí)行。

二、執(zhí)行流程

加鎖和刪除鎖的操作,使用純 Lua 進(jìn)行封裝,保障其執(zhí)行時(shí)候的原子性。

基于純Lua腳本實(shí)現(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 存儲(chǔ)到 Redis 中,并設(shè)置過期時(shí)間為 TTL。如果設(shè)置失敗,則進(jìn)入條件內(nèi)部。
  • 在條件內(nèi)部,判斷 ENABLE_LOCK_INTENT 的值。如果為 1,則執(zhí)行 redis.call("SET", KEYS[2], 1, "PX", ARGV[2]),將 LOCK_INTENT 鍵設(shè)置為 1,并設(shè)置與 LOCK_KEY 相同的過期時(shí)間。這是為了表示鎖被占用的意圖。
  • 返回 redis.call("PTTL", KEYS[1]),即 LOCK_KEY 的剩余過期時(shí)間,以毫秒為單位。這是為了告知調(diào)用方鎖已被占用,返回鎖的剩余過期時(shí)間。
  • 若上述條件都不滿足,則執(zhí)行 redis.call("DEL", KEYS[2]),刪除 LOCK_INTENT 鍵。
  • 返回 nil,表示鎖已成功獲取。

它首先嘗試通過 SET 命令將 LOCK_KEY 存儲(chǔ)到 Redis 中,如果設(shè)置失敗,則表示鎖已被其他進(jìn)程占用,返回鎖的剩余過期時(shí)間。如果設(shè)置成功,則刪除 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])
  • 延長鎖的時(shí)間

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é)果存儲(chǔ)在變量 t 中。

  • 使用條件邏輯判斷鎖的狀態(tài):

    • 如果 t 是字符串,則返回 PTTL 命令的結(jié)果,即鎖的剩余過期時(shí)間。
    • 如果 LOCK_INTENT 鍵存在,則返回 PTTL 命令的結(jié)果,即鎖占用意圖的剩余過期時(shí)間。
  • 由于以上條件都不滿足,即鎖未被占用,將鎖 ID (ARGV[1]) 添加到 LOCK_KEY 集合中。

  • 使用 PEXPIRE 命令設(shè)置 LOCK_KEY 的過期時(shí)間為 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)先?

寫鎖會(huì)阻塞讀鎖,所以是寫優(yōu)先

寫鎖是如何阻塞寫鎖的?

如果當(dāng)前的寫鎖已經(jīng)被占用,其他寫鎖的獲取請(qǐng)求會(huì)被阻塞,因?yàn)樵卺尫沛i的邏輯中,會(huì)先判斷鎖的類型,如果是寫鎖,則會(huì)判斷當(dāng)前鎖的值是否符合預(yù)期,從而判斷能否刪除該鎖。

讀鎖與讀鎖之間互斥嗎?

對(duì)于讀鎖而言,多個(gè)讀鎖之間是可以并發(fā)持有的,因此讀鎖之間默認(rèn)是不會(huì)互斥的,可以同時(shí)執(zhí)行讀操作。

寫鎖會(huì)有被餓死的情況嗎?

寫優(yōu)先鎖可以保證寫線程不會(huì)餓死,但是如果一直有寫線程獲取寫鎖,讀線程也會(huì)被「餓死」。

既然不管優(yōu)先讀鎖還是寫鎖,對(duì)方可能會(huì)出現(xiàn)餓死問題,那么我們就不偏袒任何一方,搞個(gè)「公平讀寫鎖」。

公平讀寫鎖比較簡(jiǎn)單的一種方式是:用隊(duì)列把獲取鎖的線程排隊(duì),不管是寫線程還是讀線程都按照先進(jìn)先出的原則加鎖即可,這樣讀線程仍然可以并發(fā),也不會(huì)出現(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():返回鎖的截止時(shí)間和標(biāo)志,如果沒有設(shè)置則返回零值。
  • Done():返回一個(gè)通道,在鎖的上下文被取消或者鎖過期后會(huì)被關(guān)閉。
  • Err():返回鎖的錯(cuò)誤狀態(tài)。
  • Value(key any) any:返回一個(gè)鍵關(guān)聯(lián)的值,用于傳遞上下文相關(guān)的數(shù)據(jù)。
  • Unlock():解鎖操作,會(huì)取消鎖的上下文,并調(diào)用Redis的腳本解鎖操作。
  • refreshTTL(left time.Time):刷新鎖的過期時(shí)間,定期更新Redis中鎖的過期時(shí)間,直到鎖的上下文被取消、鎖過期或無法繼續(xù)刷新為止。
  • leftTTL():返回鎖的剩余過期時(shí)間。
  • updateTTL():更新刷新鎖的間隔時(shí)間。每次減少一半

為什么需要為什么l.ttl / 2

這是為了實(shí)現(xiàn)鎖的自動(dòng)續(xù)約。通過定期刷新鎖的過期時(shí)間,可以確保鎖在使用過程中不會(huì)過期而被意外釋放。

這種做法可以在以下情況下帶來一些好處:

  • 減少鎖的續(xù)約操作對(duì)Redis的壓力:由于續(xù)約操作是相對(duì)較昂貴的,通過將過期時(shí)間縮短為原來的一半,可以降低續(xù)約的頻率,從而減少對(duì)Redis的請(qǐng)求,減少了網(wǎng)絡(luò)和計(jì)算資源的消耗。
  • 避免長時(shí)間持有鎖帶來的問題:如果某個(gè)持有鎖的進(jìn)程/線程發(fā)生故障或延遲,導(dǎo)致無法及時(shí)釋放鎖,那么其他進(jìn)程可能會(huì)長時(shí)間等待獲取該鎖,造成資源浪費(fèi)。通過定期刷新鎖的過期時(shí)間,可以在鎖即將過期之前及時(shí)釋放鎖,降低該問題的風(fēng)險(xiǎn)。

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):設(shè)置日志記錄器。
  • WithTTL(ttl time.Duration):設(shè)置互斥鎖的生存時(shí)間(TTL)選項(xiàng)。
  • WithLockIntent():設(shè)置鎖意圖選項(xiàng)。
  • newLockOptions(m mutexOptions, opt ...LockOption):創(chuàng)建鎖的選項(xiàng)。
  • WithKey(key string):設(shè)置鎖的鍵選項(xiàng)。
  • WithContext(ctx context.Context):設(shè)置鎖的上下文選項(xiàng)。
  • lockIntentKey(key string):為給定的鎖鍵生成鎖意圖鍵。

可以通過設(shè)置選項(xiàng)來控制互斥鎖的行為和屬性,如生存時(shí)間、鎖意圖、上下文等。還提供了一些實(shí)用函數(shù)和類型,用于管理互斥鎖和生成選項(xiàng)

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)建一個(gè)新的分布式互斥鎖。該函數(shù)接受 Redis 客戶端、鎖的名稱和一系列選項(xiàng)作為參數(shù),返回一個(gè) RWMutex 結(jié)構(gòu)體實(shí)例。
  • 通過 RLock 和 Lock 方法來獲取讀鎖和寫鎖。如果無法立即獲取鎖,則會(huì)阻塞等待,直到獲取成功或者上下文取消。
  • 通過 TryRLock 和 TryLock 方法來嘗試獲取讀鎖和寫鎖,如果無法立即獲取鎖則立即返回失敗,不會(huì)阻塞。
  • 該包實(shí)現(xiàn)了一個(gè) Lock 結(jié)構(gòu)體,包含了鎖相關(guān)的信息和操作方法,比如刷新鎖的過期時(shí)間。
  • 使用 redis.Script 來執(zhí)行 Lua 腳本,通過 Redis 客戶端執(zhí)行相應(yīng)的 Redis 命令。
  • 使用了 crypto/rand 包來生成隨機(jī)的鎖標(biāo)識(shí)符。
  • 最終的 mustReadFile 函數(shù)用于讀取嵌入的 Lua 腳本文件。

測(cè)試用例

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實(shí)現(xiàn)高效讀寫鎖的代碼實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于Redis lua讀寫鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Redis 分片集群的實(shí)現(xiàn)

    Redis 分片集群的實(shí)現(xiàn)

    本文主要介紹了Redis 分片集群的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-01-01
  • kubernetes環(huán)境部署單節(jié)點(diǎn)redis數(shù)據(jù)庫的方法

    kubernetes環(huán)境部署單節(jié)點(diǎn)redis數(shù)據(jù)庫的方法

    這篇文章主要介紹了kubernetes環(huán)境部署單節(jié)點(diǎn)redis數(shù)據(jù)庫的方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-01-01
  • 深入理解redis刪除策略和淘汰策略

    深入理解redis刪除策略和淘汰策略

    每隔一段時(shí)間就掃描一定數(shù)據(jù)的設(shè)置了過期時(shí)間的key,并清除其中已過期的keys,本文主要介紹了深入理解redis刪除策略和淘汰策略,感興趣的可以了解一下
    2024-08-08
  • Redis中有序集合的內(nèi)部實(shí)現(xiàn)方式的詳細(xì)介紹

    Redis中有序集合的內(nèi)部實(shí)現(xiàn)方式的詳細(xì)介紹

    本文主要介紹了Redis中有序集合的內(nèi)部實(shí)現(xiàn)方式的詳細(xì)介紹,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • Redis緩存過期的實(shí)現(xiàn)示例

    Redis緩存過期的實(shí)現(xiàn)示例

    Redis緩存的過期策略是保證緩存可靠性和性能的關(guān)鍵之一,本文主要介紹了Redis緩存過期的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-12-12
  • Redis中的數(shù)據(jù)結(jié)構(gòu)跳表詳解

    Redis中的數(shù)據(jù)結(jié)構(gòu)跳表詳解

    跳表是一種基于并聯(lián)的鏈表結(jié)構(gòu),用于在有序元素序列中快速查找元素的數(shù)據(jù)結(jié)構(gòu),本文給大家介紹Redis中的數(shù)據(jù)結(jié)構(gòu)跳表,感興趣的朋友跟隨小編一起看看吧
    2024-06-06
  • Redis高級(jí)數(shù)據(jù)類型Hyperloglog、Bitmap的使用

    Redis高級(jí)數(shù)據(jù)類型Hyperloglog、Bitmap的使用

    很多小伙伴在面試中都會(huì)被問道 Redis的常用數(shù)據(jù)結(jié)構(gòu)有哪些?可能很大一部分回答都是 string、hash、list、set、zset,但其實(shí)還有Hyperloglog和Bitmap,本文就來介紹一下
    2021-05-05
  • Redis操作命令總結(jié)

    Redis操作命令總結(jié)

    這篇文章主要介紹了Redis操作命令總結(jié),本文講解了key pattern 查詢相應(yīng)的key、字符串類型的操作、鏈表操作、hashes類型及操作、集合結(jié)構(gòu)操作、有序集合、服務(wù)器相關(guān)命令等內(nèi)容,需要的朋友可以參考下
    2015-03-03
  • Redis 布隆過濾器的原理和實(shí)踐教程

    Redis 布隆過濾器的原理和實(shí)踐教程

    布隆過濾器適用于需要快速判斷一個(gè)元素是否可能存在于集合中的場(chǎng)景,例如網(wǎng)絡(luò)爬蟲中的去重、緩存中的數(shù)據(jù)判斷等,這篇文章主要介紹了Redis 布隆過濾器的原理和實(shí)踐,需要的朋友可以參考下
    2024-02-02
  • Redis壓縮列表的設(shè)計(jì)與實(shí)現(xiàn)

    Redis壓縮列表的設(shè)計(jì)與實(shí)現(xiàn)

    壓縮列表(Ziplist)是 Redis 為了節(jié)省內(nèi)存而設(shè)計(jì)的一種緊湊型數(shù)據(jù)結(jié)構(gòu),主要用于存儲(chǔ)長度較短且數(shù)量較少的元素集合,本文給大家介紹了Redis壓縮列表的設(shè)計(jì)與實(shí)現(xiàn),文中通過代碼示例講解的非常詳細(xì),需要的朋友可以參考下
    2024-08-08

最新評(píng)論