├── adapter ├── go-redis │ ├── V7 │ │ ├── adapter_test.go │ │ ├── go.mod │ │ ├── adapter.go │ │ └── go.sum │ ├── V8 │ │ ├── adapter_test.go │ │ ├── go.mod │ │ ├── adapter.go │ │ └── go.sum │ └── V9 │ │ ├── go.mod │ │ ├── adapter.go │ │ ├── go.sum │ │ └── adapter_test.go ├── go-zero │ └── V1 │ │ ├── adapter_test.go │ │ ├── adapter.go │ │ ├── go.mod │ │ └── go.sum └── README.md ├── .codecov.yml ├── lua ├── multiLock.lua ├── multiUnLock.lua ├── multiRenew.lua ├── writeRenew.lua ├── readRenew.lua ├── fairRenew.lua ├── writeUnLock.lua ├── readUnLock.lua ├── fairUnlock.lua ├── readLock.lua ├── writeLock.lua ├── reentrantRenew.lua ├── reentrantUnLock.lua ├── reentrantLock.lua └── fairLock.lua ├── go.mod ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── 02.feature_request.yaml │ ├── 03.docs_improvement.yaml │ ├── 04.ask_question.yaml │ └── 01.bug_report.yaml ├── dependabot.yml └── workflows │ ├── go.yml │ └── codeql.yml ├── examples ├── demo │ ├── go.mod │ ├── main.go │ └── go.sum └── usage │ ├── reentrant │ └── order.go │ └── fair │ └── ticket.go ├── .gitignore ├── tests ├── go.mod ├── rdb_test.go ├── go.sum ├── lock_write_test.go ├── lock_read_test.go ├── lock_fair_test.go └── lock_reentrant_test.go ├── vars.go ├── lock_multi.go ├── Makefile ├── LICENSE ├── docs ├── fair.md ├── 注意事项.md ├── good_bad.md ├── 读锁升级为写锁.md └── 读写锁.md ├── go.sum ├── lock_read.go ├── lock_write.go ├── lock_fair.go ├── lock_reentrant.go ├── lock.go ├── CHANGELOG.cn.md ├── README.md ├── CHANGELOG.md ├── README.en.md └── mocks └── lock.go /adapter/go-redis/V7/adapter_test.go: -------------------------------------------------------------------------------- 1 | package v7 2 | -------------------------------------------------------------------------------- /adapter/go-redis/V8/adapter_test.go: -------------------------------------------------------------------------------- 1 | package v8 2 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: true 3 | 4 | coverage: 5 | ignore: 6 | - "mocks/**" 7 | - "examples/**" -------------------------------------------------------------------------------- /lua/multiLock.lua: -------------------------------------------------------------------------------- 1 | -- 锁 key 和 value 2 | local lock_key = '{' .. KEYS[1] .. '}' 3 | local lock_value = ARGV[1] 4 | local lock_ttl = tonumber(ARGV[2]) 5 | 6 | -- 尝试创建锁 7 | if redis.call('SET', lock_key, lock_value, 'NX', 'PX', lock_ttl) then 8 | return 1 9 | end 10 | 11 | -- 获取失败 12 | return 0 13 | -------------------------------------------------------------------------------- /lua/multiUnLock.lua: -------------------------------------------------------------------------------- 1 | -- 锁 key 和持有者标识 2 | local lock_key = '{' .. KEYS[1] .. '}' 3 | local lock_value = ARGV[1] 4 | 5 | -- 只有持有锁的客户端才能释放 6 | if redis.call('GET', lock_key) == lock_value then 7 | redis.call('DEL', lock_key) 8 | return 1 9 | end 10 | 11 | -- 解锁失败(锁不存在或不是持有者) 12 | return 0 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jefferyjob/go-redislock 2 | 3 | go 1.21 4 | 5 | retract ( 6 | v1.6.0 // adapter package name is invalid 7 | v1.5.0 // adapter package name is invalid 8 | v1.0.0 // package name error 9 | ) 10 | 11 | require ( 12 | github.com/golang/mock v1.6.0 13 | github.com/google/uuid v1.6.0 14 | ) 15 | -------------------------------------------------------------------------------- /adapter/go-redis/V7/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jefferyjob/go-redislock/adapter/go-redis/V7 2 | 3 | go 1.21 4 | 5 | replace github.com/jefferyjob/go-redislock => ../../.. 6 | 7 | require ( 8 | github.com/go-redis/redis/v7 v7.4.1 9 | github.com/jefferyjob/go-redislock v1.7.0-beta 10 | ) 11 | 12 | require github.com/google/uuid v1.6.0 // indirect 13 | -------------------------------------------------------------------------------- /lua/multiRenew.lua: -------------------------------------------------------------------------------- 1 | -- 锁 key 和持有者标识 2 | local lock_key = '{' .. KEYS[1] .. '}' 3 | local lock_value = ARGV[1] 4 | local lock_ttl = tonumber(ARGV[2]) 5 | 6 | -- 只有持有锁的客户端才能续期 7 | if redis.call('GET', lock_key) == lock_value then 8 | redis.call('PEXPIRE', lock_key, lock_ttl) 9 | return 1 10 | end 11 | 12 | -- 续期失败(锁不存在或不是持有者) 13 | return 0 14 | -------------------------------------------------------------------------------- /lua/writeRenew.lua: -------------------------------------------------------------------------------- 1 | local local_key = KEYS[1] 2 | local lock_value = ARGV[1] 3 | local lock_ttl = tonumber(ARGV[2]) or 0 4 | 5 | -- 验证写锁持有者 6 | local writer = redis.call('HGET', local_key, 'writer') 7 | if writer ~= lock_value then 8 | -- 非写锁持有者,续期失败 9 | return 0 10 | end 11 | 12 | -- 刷新 TTL 13 | redis.call('PEXPIRE', local_key, lock_ttl) 14 | return 1 -------------------------------------------------------------------------------- /lua/readRenew.lua: -------------------------------------------------------------------------------- 1 | local local_key = KEYS[1] 2 | local lock_value = ARGV[1] -- 当前请求续期的持有者标识(owner) 3 | local lock_ttl = tonumber(ARGV[2]) or 0 4 | 5 | -- 获取自身读锁计数,判断是否持有读锁 6 | local self_cnt = tonumber(redis.call('HGET', local_key, 'r:' .. lock_value) or '0') 7 | 8 | -- 如果当前线程没有持有读锁,则续期失败 9 | if self_cnt <= 0 then 10 | return 0 11 | end 12 | 13 | -- 刷新锁的 TTL,延长锁有效期,避免锁过期被其他线程抢占 14 | redis.call('PEXPIRE', local_key, lock_ttl) 15 | return 1 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "Open Source: go-redislock" 4 | url: "https://github.com/jefferyjob/go-redislock" 5 | about: "About High-performance Redis distributed lock service based on Go language. " 6 | - name: "Open Source: go-easy-util" 7 | url: "https://github.com/jefferyjob/go-easy-utils" 8 | about: "Quick toolbox for common data processing developed by Go language。" 9 | -------------------------------------------------------------------------------- /examples/demo/go.mod: -------------------------------------------------------------------------------- 1 | module demo 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/jefferyjob/go-redislock v1.7.0-beta.1 7 | github.com/jefferyjob/go-redislock/adapter/go-redis/V9 v0.0.0-20251210045550-d6fd703b525b 8 | github.com/redis/go-redis/v9 v9.17.2 9 | ) 10 | 11 | require ( 12 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 13 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 14 | github.com/google/uuid v1.6.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /adapter/go-redis/V8/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jefferyjob/go-redislock/adapter/go-redis/V8 2 | 3 | go 1.21 4 | 5 | replace github.com/jefferyjob/go-redislock => ../../.. 6 | 7 | require ( 8 | github.com/go-redis/redis/v8 v8.11.5 9 | github.com/jefferyjob/go-redislock v1.7.0-beta 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | github.com/google/uuid v1.6.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /adapter/go-redis/V9/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jefferyjob/go-redislock/adapter/go-redis/V9 2 | 3 | go 1.21 4 | 5 | replace github.com/jefferyjob/go-redislock => ../../.. 6 | 7 | require ( 8 | github.com/jefferyjob/go-redislock v1.7.0-beta 9 | github.com/redis/go-redis/v9 v9.17.0 10 | ) 11 | 12 | require ( 13 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 14 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 15 | github.com/google/uuid v1.6.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | open-pull-requests-limit: 10 13 | 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | coverage.out 14 | coverage.txt 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | vendor/ 21 | 22 | # Go workspace file 23 | go.work 24 | 25 | # Edit Dir 26 | .idea 27 | .project 28 | .DS_Store 29 | .vscode 30 | .fleet -------------------------------------------------------------------------------- /lua/fairRenew.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Fair Lock Renew TTL (公平锁锁续期脚本) 3 | 4 | 适用场景: 5 | - 客户端在业务处理中需要延长持锁时间,防止锁因 TTL 到期提前释放。 6 | 7 | KEYS[1] - 锁的 key(与加锁脚本保持一致) 8 | ARGV[1] - 请求 ID(必须与加锁时写入的完全一致) 9 | ARGV[2] - 续期的锁 TTL(毫秒,lock_ttl) 10 | 11 | 返回: 12 | 1 续期成功(确实持有该锁并已刷新 TTL) 13 | 0 续期失败(锁不存在,或锁已被其他请求持有) 14 | --]] 15 | 16 | 17 | local lock_key = '{' .. KEYS[1] .. '}' 18 | local request_id = ARGV[1] 19 | local lock_ttl = tonumber(ARGV[2]) 20 | 21 | -- 只允许当前持锁者续期 22 | if redis.call('GET', lock_key) == request_id then 23 | redis.call('PEXPIRE', lock_key, lock_ttl) 24 | return 1 -- 续期成功 25 | end 26 | 27 | return 0 -- 续期失败:要么锁不存在,要么不是你的 -------------------------------------------------------------------------------- /tests/go.mod: -------------------------------------------------------------------------------- 1 | module tests 2 | 3 | go 1.21 4 | 5 | replace github.com/jefferyjob/go-redislock => .. 6 | 7 | require ( 8 | github.com/jefferyjob/go-redislock v1.7.0-beta.1 9 | github.com/jefferyjob/go-redislock/adapter/go-redis/V9 v0.0.0-20251210060753-5b2e8d62842e 10 | github.com/redis/go-redis/v9 v9.17.2 11 | github.com/stretchr/testify v1.11.1 12 | ) 13 | 14 | require ( 15 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 18 | github.com/google/uuid v1.6.0 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /examples/usage/reentrant/order.go: -------------------------------------------------------------------------------- 1 | package reentrant 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | redislock "github.com/jefferyjob/go-redislock" 9 | ) 10 | 11 | type Order struct { 12 | rdb redislock.RedisInter 13 | } 14 | 15 | // CreateOrder 防止重复下单 16 | func (o *Order) CreateOrder(ctx context.Context, userId int64, productId int64) error { 17 | lockKey := fmt.Sprintf("order_lock:%d:%d", userId, productId) 18 | lock := redislock.New( 19 | o.rdb, 20 | lockKey, 21 | redislock.WithTimeout(10*time.Second), 22 | ) 23 | 24 | if err := lock.Lock(ctx); err != nil { 25 | return fmt.Errorf("操作过于频繁,请稍后再试") 26 | } 27 | defer lock.UnLock(ctx) 28 | 29 | // 检查是否已存在订单 30 | // 创建新订单 31 | // ... 32 | 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /vars.go: -------------------------------------------------------------------------------- 1 | package go_redislock 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | const ( 9 | // 默认锁超时时间 10 | lockTime = 5 * time.Second 11 | // 默认请求超时时间 12 | requestTimeout = lockTime 13 | ) 14 | 15 | var ( 16 | // ErrLockFailed 加锁失败 17 | ErrLockFailed = errors.New("lock failed") 18 | // ErrUnLockFailed 解锁失败 19 | ErrUnLockFailed = errors.New("unLock failed") 20 | // ErrSpinLockTimeout 自旋锁加锁超时 21 | ErrSpinLockTimeout = errors.New("spin lock timeout") 22 | // ErrSpinLockDone 自旋锁加锁超时 23 | ErrSpinLockDone = errors.New("spin lock context done") 24 | // ErrLockRenewFailed 锁续期失败 25 | ErrLockRenewFailed = errors.New("lock renew failed") 26 | // ErrException 内部异常 27 | ErrException = errors.New("go redis lock internal exception") 28 | ) 29 | -------------------------------------------------------------------------------- /adapter/go-redis/V7/adapter.go: -------------------------------------------------------------------------------- 1 | package v7 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v7" 6 | redislock "github.com/jefferyjob/go-redislock" 7 | ) 8 | 9 | type RedisAdapter struct { 10 | client redis.UniversalClient 11 | } 12 | 13 | func New(client redis.UniversalClient) redislock.RedisInter { 14 | return &RedisAdapter{client: client} 15 | } 16 | 17 | func (r *RedisAdapter) Eval(ctx context.Context, script string, keys []string, args ...interface{}) redislock.RedisCmd { 18 | cmd := r.client.Eval(script, keys, args...) 19 | return &RedisCmdWrapper{cmd: cmd} 20 | } 21 | 22 | type RedisCmdWrapper struct { 23 | cmd *redis.Cmd 24 | } 25 | 26 | func (w *RedisCmdWrapper) Result() (interface{}, error) { 27 | return w.cmd.Result() 28 | } 29 | func (w *RedisCmdWrapper) Int64() (int64, error) { 30 | return w.cmd.Int64() 31 | } 32 | -------------------------------------------------------------------------------- /adapter/go-redis/V8/adapter.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "context" 5 | "github.com/go-redis/redis/v8" 6 | redislock "github.com/jefferyjob/go-redislock" 7 | ) 8 | 9 | type RedisAdapter struct { 10 | client redis.UniversalClient 11 | } 12 | 13 | func New(client redis.UniversalClient) redislock.RedisInter { 14 | return &RedisAdapter{client: client} 15 | } 16 | 17 | func (r *RedisAdapter) Eval(ctx context.Context, script string, keys []string, args ...interface{}) redislock.RedisCmd { 18 | cmd := r.client.Eval(ctx, script, keys, args...) 19 | return &RedisCmdWrapper{cmd: cmd} 20 | } 21 | 22 | type RedisCmdWrapper struct { 23 | cmd *redis.Cmd 24 | } 25 | 26 | func (w *RedisCmdWrapper) Result() (interface{}, error) { 27 | return w.cmd.Result() 28 | } 29 | func (w *RedisCmdWrapper) Int64() (int64, error) { 30 | return w.cmd.Int64() 31 | } 32 | -------------------------------------------------------------------------------- /adapter/go-redis/V9/adapter.go: -------------------------------------------------------------------------------- 1 | package v9 2 | 3 | import ( 4 | "context" 5 | 6 | redislock "github.com/jefferyjob/go-redislock" 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | type RedisAdapter struct { 11 | client redis.UniversalClient 12 | } 13 | 14 | func New(client redis.UniversalClient) redislock.RedisInter { 15 | return &RedisAdapter{client: client} 16 | } 17 | 18 | func (r *RedisAdapter) Eval(ctx context.Context, script string, keys []string, args ...interface{}) redislock.RedisCmd { 19 | cmd := r.client.Eval(ctx, script, keys, args...) 20 | return &RedisCmdWrapper{cmd: cmd} 21 | } 22 | 23 | type RedisCmdWrapper struct { 24 | cmd *redis.Cmd 25 | } 26 | 27 | func (w *RedisCmdWrapper) Result() (interface{}, error) { 28 | return w.cmd.Result() 29 | } 30 | func (w *RedisCmdWrapper) Int64() (int64, error) { 31 | return w.cmd.Int64() 32 | } 33 | -------------------------------------------------------------------------------- /lua/writeUnLock.lua: -------------------------------------------------------------------------------- 1 | local local_key = KEYS[1] 2 | local lock_value = ARGV[1] -- 当前请求解锁的持有者标识(owner) 3 | 4 | -- 获取当前写锁持有者 5 | local writer = redis.call('HGET', local_key, 'writer') 6 | if writer ~= lock_value then 7 | -- 如果当前线程不是写锁持有者,则解锁失败 8 | return 0 9 | end 10 | 11 | -- 减少写锁计数(支持可重入锁) 12 | local wcount = tonumber(redis.call('HINCRBY', local_key, 'wcount', -1)) 13 | if wcount > 0 then 14 | -- 写锁仍然持有(可重入计数 > 0),无需释放锁,直接返回 15 | return 1 16 | end 17 | 18 | -- 写锁计数归零,释放写锁 19 | redis.call('HDEL', local_key, 'writer') 20 | redis.call('HDEL', local_key, 'wcount') 21 | 22 | -- 检查是否存在读锁 23 | local rcount = tonumber(redis.call('HGET', local_key, 'rcount') or '0') 24 | if rcount > 0 then 25 | -- 仍有读锁,切回读模式 26 | redis.call('HSET', local_key, 'mode', 'read') 27 | else 28 | -- 无锁持有者,删除键 29 | redis.call('DEL', local_key) 30 | end 31 | 32 | return 1 -------------------------------------------------------------------------------- /examples/demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | redislock "github.com/jefferyjob/go-redislock" 8 | adapter "github.com/jefferyjob/go-redislock/adapter/go-redis/V9" 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | func main() { 13 | // Create a Redis client adapter 14 | rdbAdapter := adapter.New(redis.NewClient(&redis.Options{ 15 | Addr: "localhost:6379", 16 | })) 17 | 18 | // Create a context for canceling lock operations 19 | ctx := context.Background() 20 | 21 | // Create a RedisLock object 22 | lock := redislock.New(rdbAdapter, "test_key") 23 | 24 | // acquire lock 25 | err := lock.Lock(ctx) 26 | if err != nil { 27 | fmt.Println("lock acquisition failed:", err) 28 | return 29 | } 30 | defer lock.UnLock(ctx) // unlock 31 | 32 | // Perform tasks during lockdown 33 | // ... 34 | fmt.Println("task execution completed") 35 | } 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02.feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: "🚀 Feature Request" 2 | description: "Make creative suggestions for this package." 3 | title: "[Feature]: " 4 | labels: [feature] 5 | # assignees: '' 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | We'd love to hear your idea! Please provide as much detail as possible. 12 | 13 | - type: textarea 14 | id: details 15 | attributes: 16 | label: "Detailed Description" 17 | description: "Explain the feature in detail. Why is it needed? How should it work?" 18 | placeholder: "Feature details go here..." 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | id: alternatives 24 | attributes: 25 | label: "Alternatives" 26 | description: "Have you considered any alternatives or workarounds?" 27 | placeholder: "Alternative solutions or workarounds" 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03.docs_improvement.yaml: -------------------------------------------------------------------------------- 1 | name: "📚 Documentation Improvement" 2 | description: "Suggest improvements to the documentation." 3 | title: "[Docs]: " 4 | labels: [documentation] 5 | # assignees: '' 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Help us improve our documentation by providing suggestions. 12 | 13 | - type: input 14 | id: page 15 | attributes: 16 | label: "Documentation Page" 17 | description: "Which page or section are you suggesting improvements for?" 18 | placeholder: "URL or section name" 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | id: improvement 24 | attributes: 25 | label: "Suggested Improvement" 26 | description: "What improvements would you like to see?" 27 | placeholder: "Suggestion details here..." 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /lock_multi.go: -------------------------------------------------------------------------------- 1 | package go_redislock 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "time" 7 | ) 8 | 9 | var ( 10 | //go:embed lua/multiLock.lua 11 | multiLockScript string 12 | //go:embed lua/multiUnLock.lua 13 | multiUnLockScript string 14 | //go:embed lua/multiRenew.lua 15 | multiRenewScript string 16 | ) 17 | 18 | func (l *RedisLock) MultiLock(ctx context.Context, locks []RedisLockInter) error { 19 | // TODO implement me 20 | panic("implement me") 21 | } 22 | 23 | func (l *RedisLock) MultiUnLock(ctx context.Context, locks []RedisLockInter) error { 24 | // TODO implement me 25 | panic("implement me") 26 | } 27 | 28 | func (l *RedisLock) SpinMultiLock(ctx context.Context, locks []RedisLockInter, timeout time.Duration) error { 29 | // TODO implement me 30 | panic("implement me") 31 | } 32 | 33 | func (l *RedisLock) MultiRenew(ctx context.Context, locks []RedisLockInter) error { 34 | // TODO implement me 35 | panic("implement me") 36 | } 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/04.ask_question.yaml: -------------------------------------------------------------------------------- 1 | name: "🤔 Ask a Question" 2 | description: "Ask any question about the project." 3 | title: "[Question]: " 4 | labels: [question] 5 | # assignees: '' 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Have a question about the project? We'd love to help! Please provide details below. 12 | 13 | - type: textarea 14 | id: question_details 15 | attributes: 16 | label: "Question Description" 17 | description: "Explain your question in detail. Provide any context or background." 18 | placeholder: "Details go here..." 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | id: code_configuration 24 | attributes: 25 | label: "Relevant code or configuration" 26 | description: "Please provide code snippets or configuration file contents related to the problem." 27 | placeholder: "Code or configuration go here..." 28 | validations: 29 | required: false -------------------------------------------------------------------------------- /lua/readUnLock.lua: -------------------------------------------------------------------------------- 1 | local local_key = KEYS[1] 2 | local lock_value = ARGV[1] -- 当前请求解锁的持有者标识(owner) 3 | 4 | -- 获取当前持有者的读锁计数 5 | local self_cnt = tonumber(redis.call('HGET', local_key, 'r:' .. lock_value) or '0') 6 | 7 | -- 如果自身没有持有读锁,则解锁失败 8 | if self_cnt <= 0 then 9 | return 0 10 | end 11 | 12 | -- 减少自身读锁计数 13 | self_cnt = redis.call('HINCRBY', local_key, 'r:' .. lock_value, -1) 14 | -- 自身读锁减 1 15 | redis.call('HINCRBY', local_key, 'rcount', -1) 16 | if self_cnt == 0 then 17 | -- 如果自身读锁计数归零,删除自身读锁字段 18 | redis.call('HDEL', local_key, 'r:' .. lock_value) 19 | end 20 | 21 | -- 获取总读者数 22 | local total = tonumber(redis.call('HGET', local_key, 'rcount') or '0') 23 | if total <= 0 then 24 | -- 当没有其他读者时,需要根据模式判断是否清理键 25 | local mode = redis.call('HGET', local_key, 'mode') -- 当前锁模式 26 | if mode == 'read' then 27 | -- 如果当前模式是读锁,且总读者数为 0,则删除整个锁键 28 | redis.call('DEL', local_key) 29 | else 30 | -- 如果模式是写锁,说明还有写锁存在,只删除读锁计数字段 31 | redis.call('HDEL', local_key, 'rcount') 32 | end 33 | end 34 | 35 | return 1 -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 2 | .PHONY:build 3 | build: ## 编译项目 4 | go build -v ./... 5 | 6 | .PHONY:test 7 | test: ## 运行测试 8 | go test -v ./... 9 | 10 | .PHONY:lint 11 | lint: ## 执行代码静态分析 12 | golangci-lint run 13 | 14 | .PHONY:bench 15 | bench: ## 运行基准测试 16 | go test -benchmem -bench . 17 | 18 | .PHONY:doc 19 | doc: ## 启动文档服务器 20 | godoc -http=:6060 -play -index 21 | 22 | .PHONY:cover 23 | cover: ## 生成测试覆盖率报告 24 | #go tool cover -func=coverage.out 25 | go test -race -coverprofile=coverage.txt -covermode=atomic ./... 26 | 27 | .PHONY:run-redis 28 | run-redis: ## Docker启动redis服务 29 | docker run -itd -p 63790:6379 --name example_redislock redis:5.0.3-alpine 30 | 31 | .PHONY:mocks 32 | mocks: ## 基于Interface生成Mock代码 33 | mockgen -source=lock.go -destination=mocks/lock.go -package=mocks 34 | 35 | .PHONY:help 36 | .DEFAULT_GOAL:=help 37 | help: 38 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 libin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /adapter/go-redis/V9/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM= 12 | github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 13 | -------------------------------------------------------------------------------- /tests/rdb_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | redislock "github.com/jefferyjob/go-redislock" 9 | adapter "github.com/jefferyjob/go-redislock/adapter/go-redis/V9" 10 | "github.com/redis/go-redis/v9" 11 | ) 12 | 13 | const ( 14 | // 默认锁超时时间 15 | lockTime = 5 * time.Second 16 | // 默认请求超时时间 17 | requestTimeout = lockTime 18 | ) 19 | 20 | var ( 21 | addr = "127.0.0.1" 22 | port = "63790" 23 | // luaSetScript = `return redis.call("SET", KEYS[1], ARGV[1])` 24 | // luaGetScript = `return redis.call("GET", KEYS[1])` 25 | // luaDelScript = `return redis.call("DEL", KEYS[1])` 26 | 27 | once sync.Once 28 | redisInter redislock.RedisInter 29 | ) 30 | 31 | // Redis 服务器集成测试 32 | // 33 | // 本测试依赖实际的 Redis 服务,用于验证分布式锁在真实环境下的行为。 34 | // 你可以通过以下命令快速启动一个本地 Redis 容器: 35 | // 36 | // docker run -d -p 63790:6379 --name go_redis_lock redis 37 | // 38 | // 运行后,测试代码将自动连接到该容器的 Redis 实例, 39 | // 以便更方便地调试和定位服务中的潜在问题。 40 | func getRedisClient() redislock.RedisInter { 41 | once.Do(func() { 42 | rdb := redis.NewClient(&redis.Options{ 43 | Addr: fmt.Sprintf("%s:%s", addr, port), 44 | }) 45 | redisInter = adapter.New(rdb) 46 | }) 47 | return redisInter 48 | } 49 | -------------------------------------------------------------------------------- /lua/fairUnlock.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Fair Queue Distributed Unlock Script (基于排队公平锁的解锁脚本) 3 | 4 | 功能描述: 5 | 用于释放通过公平排队机制获取的分布式锁。确保只有当前请求者才有权限释放锁, 6 | 并从 Redis ZSET 队列中移除自己的请求 ID,避免长时间占用排队资源。 7 | 8 | 使用场景: 9 | 搭配基于 ZSET 实现的公平分布式锁脚本使用,客户端释放锁时调用。 10 | 11 | 输入参数: 12 | KEYS[1] - 业务锁 key(如 "my-lock") 13 | ARGV[1] - 请求 ID(客户端持锁标识,建议与加锁时传入一致) 14 | 15 | Redis 数据结构说明: 16 | 1. 主锁键({KEYS[1]}):存储当前持锁请求 ID; 17 | 2. 排队键({KEYS[1]}:queue):ZSET,记录所有等待请求,score 为时间戳。 18 | 19 | 执行逻辑: 20 | 1. 若当前请求 ID 与锁键中的值一致(是锁的持有者),则删除锁键; 21 | 2. 无论是否持有锁,统一从 ZSET 排队队列中移除该请求 ID; 22 | 3. 返回 "OK" 表示执行成功。 23 | 24 | 返回值: 25 | - "OK":无论是否实际持有锁,解锁请求都被成功处理(幂等) 26 | 27 | 注意事项: 28 | - 使用 `GET lock_key == request_id` 判断是否是锁的持有者; 29 | - 解锁时必须确保 request_id 和加锁时保持一致; 30 | - 该脚本是幂等的,多次调用不会产生副作用; 31 | - 与基于 ZSET 的加锁脚本配套使用效果最佳。 32 | 33 | --]] 34 | 35 | 36 | local lock_key = '{' .. KEYS[1] .. '}' 37 | local queue_key = lock_key .. ':queue' 38 | local request_id = ARGV[1] 39 | 40 | -- 删除锁键(只删除自己持有的锁) 41 | if redis.call('GET', lock_key) == request_id then 42 | redis.call('DEL', lock_key) 43 | end 44 | 45 | -- 从队列中删除请求ID 46 | redis.call('ZREM', queue_key, request_id) 47 | 48 | return 1 49 | -------------------------------------------------------------------------------- /adapter/go-redis/V9/adapter_test.go: -------------------------------------------------------------------------------- 1 | package v9 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "testing" 8 | "time" 9 | 10 | redislock "github.com/jefferyjob/go-redislock" 11 | "github.com/redis/go-redis/v9" 12 | ) 13 | 14 | var ( 15 | addr = "127.0.0.1" 16 | port = "63790" 17 | ) 18 | 19 | func getRedisClient() redislock.RedisInter { 20 | rdb := redis.NewClient(&redis.Options{ 21 | Addr: fmt.Sprintf("%s:%s", addr, port), 22 | }) 23 | return New(rdb) 24 | } 25 | 26 | // 适配器测试 27 | func TestAdapter(t *testing.T) { 28 | adapter := getRedisClient() 29 | 30 | ctx := context.Background() 31 | key := "test_key" 32 | 33 | // 线程2抢占锁资源-预期失败 34 | go func() { 35 | time.Sleep(time.Second * 1) 36 | lock := redislock.New(adapter, key) 37 | err := lock.Lock(ctx) 38 | if err == nil { 39 | t.Errorf("Lock() returned unexpected success: %v", err) 40 | return 41 | } 42 | log.Println("线程2:抢占锁失败,锁已被其他线程占用") 43 | }() 44 | 45 | // 线程1加锁-预期成功 46 | lock := redislock.New(adapter, key) 47 | err := lock.Lock(ctx) 48 | if err != nil { 49 | t.Errorf("Lock() returned unexpected error: %v", err) 50 | return 51 | } 52 | defer lock.UnLock(ctx) 53 | 54 | // 模拟业务处理 55 | log.Println("线程1:锁已获取,开始执行任务") 56 | time.Sleep(time.Second * 5) 57 | } 58 | -------------------------------------------------------------------------------- /adapter/go-zero/V1/adapter_test.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "testing" 8 | "time" 9 | 10 | redislock "github.com/jefferyjob/go-redislock" 11 | "github.com/zeromicro/go-zero/core/stores/redis" 12 | ) 13 | 14 | var ( 15 | addr = "127.0.0.1" 16 | port = "63790" 17 | ) 18 | 19 | func getRedisClient() redislock.RedisInter { 20 | rdb := redis.MustNewRedis(redis.RedisConf{ 21 | Host: fmt.Sprintf("%s:%s", addr, port), 22 | Type: "node", 23 | }) 24 | return New(rdb) 25 | } 26 | 27 | func TestAdapter(t *testing.T) { 28 | adapter := getRedisClient() 29 | 30 | ctx := context.Background() 31 | key := "test_key" 32 | 33 | // 线程2抢占锁资源-预期失败 34 | go func() { 35 | time.Sleep(time.Second * 1) 36 | lock := redislock.New(adapter, key) 37 | err := lock.Lock(ctx) 38 | if err == nil { 39 | t.Errorf("Lock() returned unexpected success: %v", err) 40 | return 41 | } 42 | log.Println("线程2:抢占锁失败,锁已被其他线程占用") 43 | }() 44 | 45 | // 线程1加锁-预期成功 46 | lock := redislock.New(adapter, key) 47 | err := lock.Lock(ctx) 48 | if err != nil { 49 | t.Errorf("Lock() returned unexpected error: %v", err) 50 | return 51 | } 52 | defer lock.UnLock(ctx) 53 | 54 | // 模拟业务处理 55 | log.Println("线程1:锁已获取,开始执行任务") 56 | time.Sleep(time.Second * 5) 57 | } 58 | -------------------------------------------------------------------------------- /examples/usage/fair/ticket.go: -------------------------------------------------------------------------------- 1 | package fair 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | redislock "github.com/jefferyjob/go-redislock" 12 | ) 13 | 14 | type Ticket struct { 15 | rdb redislock.RedisInter 16 | } 17 | 18 | // 并发模拟 50 位用户抢票 19 | func (t *Ticket) buy(ctx context.Context) { 20 | lockKey := "fair:lock" 21 | 22 | lock := redislock.New( 23 | t.rdb, 24 | lockKey, 25 | redislock.WithTimeout(30*time.Second), // 锁 TTL 26 | redislock.WithRequestTimeout(10*time.Second), // 10s无法获取锁则放弃 27 | ) 28 | 29 | var wg sync.WaitGroup 30 | userCount := 50 31 | wg.Add(userCount) 32 | 33 | for i := 0; i < userCount; i++ { 34 | go func(userId int) { 35 | defer wg.Done() 36 | requestId := fmt.Sprintf("user:%d:%s", userId, uuid.New().String()) 37 | 38 | // 自旋公平锁 —— 在 N 秒内一直尝试 39 | if err := lock.SpinFairLock(ctx, requestId, 10*time.Second); err != nil { 40 | log.Printf("[%s] 排队超时,未抢到锁: %v", requestId, err) 41 | return 42 | } 43 | 44 | // 下面开始处于临界区,只有队首线程能执行 45 | defer lock.FairUnLock(ctx, requestId) 46 | 47 | // 检查剩余票数 48 | // 扣减库存 / 为该用户锁定票资源 49 | 50 | // 抢票成功 51 | log.Printf("[%s] 抢票成功!", requestId) 52 | }(i + 1) 53 | } 54 | 55 | wg.Wait() 56 | 57 | log.Printf("抢票结束") 58 | } 59 | -------------------------------------------------------------------------------- /adapter/go-zero/V1/adapter.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | redislock "github.com/jefferyjob/go-redislock" 8 | "github.com/zeromicro/go-zero/core/stores/redis" 9 | ) 10 | 11 | type RdbAdapter struct { 12 | client *redis.Redis 13 | } 14 | 15 | func New(client *redis.Redis) redislock.RedisInter { 16 | return &RdbAdapter{client: client} 17 | } 18 | 19 | // Eval 通过 go-zero 的 EvalCtx 执行 Lua 脚本,结果由 GoZeroRdbCmdWrapper 封装 20 | func (r *RdbAdapter) Eval(ctx context.Context, script string, keys []string, args ...interface{}) redislock.RedisCmd { 21 | cmd, err := r.client.EvalCtx(ctx, script, keys, args...) 22 | return &RdbCmdWrapper{ 23 | cmd: cmd, 24 | err: err, 25 | } 26 | } 27 | 28 | type RdbCmdWrapper struct { 29 | cmd interface{} 30 | err error 31 | } 32 | 33 | func (w *RdbCmdWrapper) Result() (interface{}, error) { 34 | if w.err != nil { 35 | return nil, w.err 36 | } 37 | return w.cmd, nil 38 | } 39 | func (w *RdbCmdWrapper) Int64() (int64, error) { 40 | if w.err != nil { 41 | return 0, w.err 42 | } 43 | 44 | switch v := w.cmd.(type) { 45 | case int64: 46 | return v, nil 47 | case int: 48 | return int64(v), nil 49 | case string: 50 | var i int64 51 | _, err := fmt.Sscanf(v, "%d", &i) 52 | return i, err 53 | default: 54 | return 0, fmt.Errorf("cannot convert result to int: %T", w.cmd) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01.bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: "🐞 Bug Report" 2 | description: "Submit a bug report to help us improve." 3 | title: "[Bug]: " 4 | labels: [bug] 5 | # assignees: '' 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to submit a bug report! Please fill out the details below. 12 | 13 | - type: textarea 14 | id: environment 15 | attributes: 16 | label: "Environment" 17 | placeholder: "e.g., Go:1.21, go-redislock version:v2.1.0" 18 | value: | 19 | - Go version: 20 | - go-redislock version: 21 | description: | 22 | examples: 23 | - **Go version**: 1.21 24 | - **go-redislock version**: v2.1.0 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: description 30 | attributes: 31 | label: Bug description 32 | description: Detailed steps to reproduce the bug. 33 | placeholder: Steps to reproduce the bug... 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | id: logs 39 | attributes: 40 | label: "Log or error message" 41 | description: "Please paste any relevant logs or error messages." 42 | render: text 43 | validations: 44 | required: false 45 | 46 | - type: markdown 47 | attributes: 48 | value: | 49 | Thanks for taking the time to fill out this bug! -------------------------------------------------------------------------------- /lua/readLock.lua: -------------------------------------------------------------------------------- 1 | local local_key = KEYS[1] 2 | local lock_value = ARGV[1] -- 当前请求锁的持有者标识(owner) 3 | local lock_ttl = tonumber(ARGV[2]) or 0 4 | 5 | -- 获取当前锁模式 6 | local mode = redis.call('HGET', local_key, 'mode') 7 | 8 | if not mode then 9 | -- 如果锁不存在(空闲状态),直接加读锁 10 | -- 初始化锁信息: 11 | -- mode: 'read' 表示读锁模式 12 | -- rcount: 当前读锁总数 13 | -- r:{owner}: 当前持有者的读锁计数(可重入) 14 | redis.call('HSET', local_key, 15 | 'mode', 'read', 16 | 'rcount', 1, 17 | 'r:' .. lock_value, 1 18 | ) 19 | redis.call('PEXPIRE', local_key, lock_ttl) 20 | return 1 21 | end 22 | 23 | -- 如果当前锁模式是读锁 24 | if mode == 'read' then 25 | -- 读读并发:累加本 lock_value 的读计数与总读者数 26 | 27 | -- 多个读者可并发获取锁 28 | -- 增加当前持有者的读锁计数 29 | redis.call('HINCRBY', local_key, 'r:' .. lock_value, 1) 30 | -- 增加读的计数 31 | redis.call('HINCRBY', local_key, 'rcount', 1) 32 | -- 刷新 TTL 33 | redis.call('PEXPIRE', local_key, lock_ttl) 34 | return 1 35 | end 36 | 37 | 38 | -- 如果当前锁模式是写锁 39 | if mode == 'write' then 40 | local writerLockValue = redis.call('HGET', local_key, 'writer') 41 | if writerLockValue == lock_value then 42 | -- 自己持有写锁:允许同时持有读锁(读写可重入) 43 | 44 | -- 如果当前持有者就是写锁的持有者,则允许读写可重入 45 | -- 增加当前持有者的读锁计数 46 | redis.call('HINCRBY', local_key, 'r:' .. lock_value, 1) 47 | -- 增加总读者数 48 | redis.call('HINCRBY', local_key, 'rcount', 1) 49 | -- 刷新 TTL 50 | redis.call('PEXPIRE', local_key, lock_ttl) 51 | return 1 52 | end 53 | end 54 | 55 | -- 如果锁是写锁且持有者不是自己,则无法获取读锁 56 | -- 他人持有写锁:失败 57 | return 0 -------------------------------------------------------------------------------- /examples/demo/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 8 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/jefferyjob/go-redislock v1.7.0-beta.1 h1:UoWg5wP8+Wml0dxlN6oONfAdcB0/ak6gEFuEZYvgT54= 12 | github.com/jefferyjob/go-redislock v1.7.0-beta.1/go.mod h1:PU8b2eTZnQFWNczgDTKponxnbnk/uQHGLmy4a72aLUI= 13 | github.com/jefferyjob/go-redislock/adapter/go-redis/V9 v0.0.0-20251210045550-d6fd703b525b h1:tkS37WJenWmJrL9s+jfQviqrWyKogTMom8qZbYG1Xak= 14 | github.com/jefferyjob/go-redislock/adapter/go-redis/V9 v0.0.0-20251210045550-d6fd703b525b/go.mod h1:sjKrjzdWpfKRjR6l9XeMuI+WpMK/BgcebpX4HBHRMhU= 15 | github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= 16 | github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 17 | -------------------------------------------------------------------------------- /docs/fair.md: -------------------------------------------------------------------------------- 1 | # 公平锁 2 | 3 | 公平锁(Fair Lock)是一种加锁机制,确保所有请求按顺序排队获取锁。谁先来,谁先拿到锁,遵循 FIFO(先进先出)原则。 4 | 与之相对的是非公平锁(Unfair Lock),谁能抢到谁就先用,可能导致部分请求长期得不到锁,造成“饥饿”现象。 5 | 6 | **举个例子** 7 | 8 | 假设有 3 个线程同时竞争一个锁,如果调度策略是非公平的,线程 A 每次都能抢先获得锁,而线程 C 每次都晚一步被抢走机会。长期如此,C 可能一直得不到执行机会,导致“饥饿”。 9 | 10 | 想象你在医院排队挂号: 11 | - 公平锁就像是取了号排队:前面排的人没处理完,你永远轮不到。 12 | - 非公平锁就像是你每隔几分钟去窗口碰碰运气,如果你运气好、手快,就插队成功了。 13 | 14 | ## 公平锁是如何避免饥饿现象的 15 | 公平锁(Fair Lock)的关键在于**遵循“先来先服务”(FIFO)原则**:谁先请求锁,谁就先获得锁。这样可以保证每个请求最终都会排到前面,**不会永远被其他后来的请求插队抢占**。 16 | 17 | ## 公平锁 VS 非公平锁 18 | | **特性** | **公平锁** | **非公平锁** | 19 | | ------------ | ---------------------------- | ---------------------------------- | 20 | | 排队顺序 | FIFO,按请求顺序 | 不保证顺序,谁先抢到谁拿到 | 21 | | 是否可能饥饿 | 否(理论上不会) | 是,有可能某些请求长时间得不到锁 | 22 | | 性能 | 性能略差(排队/调度有成本) | 性能较好,适合低冲突、高吞吐场景 | 23 | | 适用场景 | 抢票、限流等强调“公平”的场景 | 高并发数据库连接池、线程池任务调度 | 24 | 25 | 26 | ## Redis公平锁的实现原理 27 | ### 数据结构设计 28 | 基于 Redis 的两个关键特性 29 | 30 | | **Redis Key** | **用途** | 31 | | ---------------- | -------------------------------------- | 32 | | lock:{key} | 真正的锁标识(存放持有锁的 requestId) | 33 | | lock:{key}:queue | ZSET 有序集合,用于排队请求者 | 34 | 35 | 36 | ### 加锁流程(FairLock) 37 | ``` 38 | -- 核心逻辑(伪代码) 39 | ZADD queue 当前时间 requestId // 加入队列 40 | ZRANK 判断是否是队首 41 | SET lock_key requestId NX PX ttl // 只有队首才尝试抢锁 42 | ``` 43 | 44 | ### 步骤解析 45 | 1. 排队:请求到来时,加入一个 ZSET 队列,分数为当前时间(毫秒)。 46 | 2. 超时清理:在每次加锁时清除超时队列成员,防止队列堆积。 47 | 3. 判断是否是队首: 48 | - 是队首 → 尝试加锁。 49 | - 否 → 返回失败,由调用方决定是否重试。 50 | 4. 抢锁成功:设置 Redis 的键值(SET NX PX),将 lock:{key} 设置为自己的 requestId。 51 | 5. 启动自动续期(可选):后台协程定期续约,防止锁被自动过期。 52 | 53 | -------------------------------------------------------------------------------- /lua/writeLock.lua: -------------------------------------------------------------------------------- 1 | local local_key = KEYS[1] 2 | local lock_value = ARGV[1] -- 当前请求锁的持有者标识(owner) 3 | local lock_ttl = tonumber(ARGV[2]) or 0 4 | 5 | -- 获取当前锁模式 6 | local mode = redis.call('HGET', local_key, 'mode') 7 | 8 | if not mode then 9 | -- 如果锁不存在(空闲状态),直接加写锁 10 | -- 初始化锁信息: 11 | -- mode: 'write' 表示写锁模式 12 | -- writer: 当前写锁持有者 13 | -- wcount: 写锁可重入计数 14 | redis.call('HSET', local_key, 15 | 'mode', 'write', 16 | 'writer', lock_value, 17 | 'wcount', 1) 18 | -- 设置锁过期时间,避免死锁 19 | redis.call('PEXPIRE', local_key, lock_ttl) 20 | return 1 21 | end 22 | 23 | -- 如果当前锁模式是写锁 24 | if mode == 'write' then 25 | -- 获取写锁持有者 26 | local writer = redis.call('HGET', local_key, 'writer') 27 | if writer == lock_value then 28 | -- 可重入写锁 29 | -- 当前持有者再次请求写锁,可重入 30 | redis.call('HINCRBY', local_key, 'wcount', 1) 31 | -- 刷新 TTL 32 | redis.call('PEXPIRE', local_key, lock_ttl) 33 | return 1 34 | else 35 | -- 他人持有写锁,获取失败 36 | return 0 37 | end 38 | end 39 | 40 | -- 如果当前锁模式是读锁 41 | if mode == 'read' then 42 | -- 总读者数 43 | local total = tonumber(redis.call('HGET', local_key, 'rcount') or '0') 44 | -- 自己的读锁计数 45 | local self_cnt = tonumber(redis.call('HGET', local_key, 'r:' .. lock_value) or '0') 46 | if total == self_cnt then 47 | -- 仅自己持有读锁,可以升级为写锁 48 | redis.call('HSET', local_key, 49 | 'mode', 'write', 50 | 'writer', lock_value, 51 | 'wcount', 1) 52 | redis.call('PEXPIRE', local_key, lock_ttl) 53 | return 1 54 | end 55 | end 56 | 57 | 58 | -- 其他情况无法获取写锁(存在其他读者或写锁被他人占用) 59 | return 0 -------------------------------------------------------------------------------- /lua/reentrantRenew.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Reentrant Distributed Lock TTL Renewal Script (可重入分布式锁续期脚本) 3 | 4 | 功能描述: 5 | 该 Lua 脚本用于对 Redis 中的分布式锁进行续期(刷新 TTL), 6 | 支持可重入场景和普通锁场景。仅当当前客户端仍然持有锁时,才会执行续期操作。 7 | 8 | 使用场景: 9 | 在持锁期间业务执行时间较长,需要在锁到期前定期刷新锁的 TTL, 10 | 防止锁因过期被其他客户端抢占导致数据竞争或并发问题。 11 | 12 | 输入参数: 13 | KEYS[1] - 锁的业务 key(如 "my-lock") 14 | ARGV[1] - 当前客户端标识(如 UUID,作为 lock_value) 15 | ARGV[2] - 续期的 TTL(单位:毫秒) 16 | 17 | Redis 数据结构: 18 | 1. 主锁 key: 19 | 格式:{KEYS[1]} 20 | 值:ARGV[1](客户端标识) 21 | 2. 可重入计数器 key: 22 | 格式:{KEYS[1]}:count:{ARGV[1]} 23 | 值:整数,表示当前客户端的重入次数 24 | 25 | 执行逻辑: 26 | 1. 读取当前客户端对应的可重入计数器(reentrant_key); 27 | 2. 满足以下任意一个条件即认为客户端持有锁,可以续期: 28 | - 可重入计数器存在(reentrant_count > 0) 29 | - 主锁值等于当前客户端的标识(redis.call('GET', lock_key) == lock_value) 30 | 3. 若满足续期条件: 31 | - 刷新主锁和重入计数器的过期时间(PEXPIRE); 32 | - 返回 1 表示续期成功; 33 | 4. 否则(锁不存在或不是当前客户端持有),返回 nil。 34 | 35 | 返回值: 36 | - 1:续期成功(当前客户端仍持有锁) 37 | - 0 :续期失败(锁不存在或非本客户端持有) 38 | 39 | 注意事项: 40 | - 客户端应定时调用该脚本以实现“自动续租”功能; 41 | - 续期操作需要和加锁、解锁脚本配套使用,并保持客户端 lock_value 一致; 42 | - 锁 key 和重入计数器 key 使用 Redis hash tag `{}` 包裹,确保在 Redis Cluster 下分布在同一 slot。 43 | 44 | --]] 45 | 46 | 47 | local lock_key = '{' .. KEYS[1] .. '}' 48 | local lock_value = ARGV[1] 49 | local lock_ttl = tonumber(ARGV[2]) 50 | local reentrant_key = lock_key .. ':count:' .. lock_value 51 | local reentrant_count = tonumber(redis.call('GET', reentrant_key) or '0') 52 | 53 | -- 锁续期 54 | -- 重入锁的场景(reentrant_count > 0) 55 | -- 普通锁的场景(redis.call('GET', lock_key) == lock_value) 56 | if reentrant_count > 0 or redis.call('GET', lock_key) == lock_value then 57 | redis.call('PEXPIRE', lock_key, lock_ttl) 58 | redis.call('PEXPIRE', reentrant_key, lock_ttl) 59 | return 1 60 | end 61 | 62 | return 0 63 | -------------------------------------------------------------------------------- /lua/reentrantUnLock.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Reentrant Distributed Unlock Script (可重入分布式锁释放脚本) 3 | 4 | 功能描述: 5 | 该 Lua 脚本用于在 Redis 中释放一个支持可重入的分布式锁。 6 | 如果客户端持有锁多次(重入),则需要多次调用此脚本释放;只有最后一次释放时,才会真正删除主锁 key。 7 | 8 | 使用场景: 9 | 与加锁脚本配合,用于支持分布式场景下的可重入加锁/解锁逻辑。 10 | 确保只有加锁的客户端才能解锁,并正确处理重入计数。 11 | 12 | 输入参数: 13 | KEYS[1] - 锁的业务 key(如 "my-lock") 14 | ARGV[1] - 当前客户端标识(如 UUID,作为 lock_value) 15 | 16 | Redis 数据结构: 17 | 1. 主锁 key: 18 | 格式:{KEYS[1]} 19 | 值:ARGV[1](客户端 ID) 20 | 2. 可重入计数器 key: 21 | 格式:{KEYS[1]}:count:{ARGV[1]} 22 | 值:整数,表示该客户端的持锁次数 23 | 24 | 执行逻辑: 25 | 1. 构造锁名 lock_key 和可重入计数器名 reentrant_key; 26 | 2. 如果 reentrant_count > 1: 27 | - 表示客户端还持有多次锁,仅减 1 并返回 1; 28 | 3. 如果 reentrant_count == 1: 29 | - 删除计数器; 30 | - 如果主锁的值等于客户端标识,则删除主锁; 31 | - 返回 1; 32 | 4. 如果计数器不存在或为 0: 33 | - 尝试作为普通非重入锁解锁; 34 | - 如果主锁的值等于客户端标识,则删除主锁,返回 1; 35 | 5. 以上均不满足(如不是持有者),返回 0 表示解锁失败。 36 | 37 | 返回值: 38 | - 1:解锁成功(无论是否重入) 39 | - 0 :解锁失败(锁不存在或当前客户端不是持有者) 40 | 41 | 注意事项: 42 | - 加锁和解锁脚本必须搭配使用,并保持客户端 lock_value 一致; 43 | - lock_key 使用 Redis hash tag `{}` 包裹,确保与重入计数器位于同一 slot(用于 Redis Cluster); 44 | - 客户端需要确保在业务完成后调用解锁脚本,否则会造成死锁。 45 | 46 | --]] 47 | 48 | 49 | local lock_key = '{' .. KEYS[1] .. '}' 50 | local lock_value = ARGV[1] 51 | local reentrant_key = lock_key .. ':count:' .. lock_value 52 | local reentrant_count = tonumber(redis.call('GET', reentrant_key) or '0') 53 | 54 | --可重入锁解锁 55 | if reentrant_count > 1 then 56 | redis.call('DECR', reentrant_key) 57 | return 1 58 | elseif reentrant_count == 1 then 59 | redis.call('DEL', reentrant_key) 60 | 61 | -- 如果锁的值相等,删除锁 62 | if redis.call('GET', lock_key) == lock_value then 63 | redis.call('DEL', lock_key) 64 | return 1 65 | end 66 | end 67 | 68 | --非可重入锁解锁 69 | if redis.call('GET', lock_key) == lock_value then 70 | redis.call('DEL', lock_key) 71 | return 1 72 | end 73 | 74 | return 0 75 | -------------------------------------------------------------------------------- /lua/reentrantLock.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Reentrant Distributed Lock Script (可重入分布式锁脚本) 3 | 4 | 功能描述: 5 | 该 Lua 脚本用于在 Redis 中实现一个支持可重入的分布式锁机制。 6 | 支持同一个客户端(以 lock_value 标识)在持有锁的情况下再次获取锁而不会阻塞或失败。 7 | 8 | 使用场景: 9 | 在分布式系统中,需要确保某段关键逻辑同一时间只能由一个客户端执行; 10 | 且客户端可能递归或多次尝试加锁,需要支持可重入。 11 | 12 | 输入参数: 13 | KEYS[1] - 业务锁的主键名(如 "my-lock") 14 | ARGV[1] - 当前客户端标识(如 UUID,作为锁值 lock_value) 15 | ARGV[2] - 锁的过期时间(单位:毫秒,lock_ttl) 16 | 17 | Redis 数据结构: 18 | 1. 主锁 key: 19 | 格式:{KEYS[1]} 20 | 值:ARGV[1](客户端 ID) 21 | 设置:SET NX PX lock_ttl 22 | 2. 可重入计数器 key: 23 | 格式:{KEYS[1]}:count:{ARGV[1]} 24 | 值:整数,表示客户端当前持有锁的重入次数 25 | 26 | 执行逻辑: 27 | 1. 首先尝试读取客户端自己的可重入计数器; 28 | - 如果大于 0,说明该客户端已经持有锁: 29 | - 将重入计数加 1; 30 | - 刷新主锁和计数器的过期时间; 31 | - 返回 1,表示加锁成功。 32 | 2. 如果计数器不存在或为 0,说明该客户端未持有锁: 33 | - 尝试使用 SET NX PX 加锁; 34 | - 如果成功,设置可重入计数器为 1,并设置过期时间; 35 | - 返回 1,表示加锁成功。 36 | 3. 如果 SET NX 加锁失败,表示已有其他客户端持有锁: 37 | - 返回 0,表示加锁失败。 38 | 39 | 返回值: 40 | - 1:加锁成功(首次或可重入) 41 | - 0 :加锁失败(被其他客户端持有) 42 | 43 | 注意事项: 44 | - 锁名(KEYS[1])应使用 Redis 的 hash tag `{}` 包裹,确保主锁和重入计数器落在同一 slot(用于 Redis Cluster)。 45 | - 客户端释放锁时,需正确管理可重入次数递减并在计数为 0 时删除主锁和计数器。 46 | --]] 47 | 48 | 49 | local lock_key = '{' .. KEYS[1] .. '}' 50 | local lock_value = ARGV[1] 51 | local lock_ttl = tonumber(ARGV[2]) 52 | local reentrant_key = lock_key .. ':count:' .. lock_value 53 | local reentrant_count = tonumber(redis.call('GET', reentrant_key) or '0') 54 | 55 | -- 可重入锁计数器 56 | if reentrant_count > 0 then 57 | redis.call('INCR', reentrant_key) 58 | redis.call('PEXPIRE', lock_key, lock_ttl) 59 | redis.call('PEXPIRE', reentrant_key, lock_ttl) 60 | return 1 61 | end 62 | 63 | -- 创建锁 64 | if redis.call('SET', lock_key, lock_value, 'NX', 'PX', lock_ttl) then 65 | redis.call('SET', reentrant_key, 1) 66 | redis.call('PEXPIRE', reentrant_key, lock_ttl) 67 | return 1 68 | end 69 | 70 | return 0 71 | -------------------------------------------------------------------------------- /docs/注意事项.md: -------------------------------------------------------------------------------- 1 | # 注意事项 2 | 3 | ## 使用注意事项(建议仔细阅读) 4 | 使用 `go-redislock` 时,为确保锁机制稳定可靠,请注意以下要点: 5 | 6 | ### 🔑 锁使用原则 7 | * 每次加锁操作应使用新的 `RedisLock` / `ReadWriteLock` / `MultiLock` 实例,**避免复用导致状态混乱**。 8 | * 加锁与解锁必须使用**完全相同的 key 和 token**,确保一致性。 9 | * 使用 `defer lock.UnLock(ctx)` 是推荐的释放锁方式,**确保异常情况下也能自动释放**。 10 | 11 | ### ⏱ 锁超时与续期 12 | * 默认 TTL 为 5 秒,**请根据业务实际耗时设置合理 TTL**,避免锁提前失效。 13 | * 开启 `WithAutoRenew()` 自动续期时,务必确保业务逻辑无长时间阻塞,否则可能导致续期失败、锁被意外释放。 14 | * 手动续期需主动调用 `Renew(ctx)`,适用于自定义续期逻辑场景。 15 | 16 | ### ⚖️ 公平锁使用建议 17 | * 公平锁通过队列顺序排队获取,**需传入唯一请求 ID(如 UUID)**,以区分不同请求。 18 | * 公平锁适用于高并发场景下,需保障请求获取顺序和等待公平性。 19 | 20 | ### 🧵 读写锁注意事项 21 | * **读锁可并发,但写锁互斥**:确保不会出现“读写冲突”或“写写冲突”。 22 | * 避免读锁阻塞时间过长,可能导致写锁长时间无法获取。 23 | 24 | ### 🔗 联锁(MultiLock)注意事项 25 | * 联锁内部会尝试依次加锁所有资源,**任意一个失败都会释放已获取的锁**。 26 | * 所有子锁需使用不同的 key,防止死锁或误解锁。 27 | * 加锁成功后仍需使用 `Unlock()` 统一释放。 28 | 29 | ### 📡 Redis 服务要求 30 | * 确保 Redis 服务稳定可用,开启持久化和主从复制可进一步提升可靠性。 31 | * 在分布式环境中,建议 Redis 实例部署在同一局域网内,**降低延迟和网络分区风险**。 32 | 33 | ### 🧪 建议测试与监控 34 | * 对锁相关的关键业务流程进行充分测试,排查竞态和死锁问题。 35 | * 在生产环境中建议对锁的获取失败、重试、自旋耗时等行为进行监控,**及时发现潜在问题**。 36 | 37 | 38 | ## 常见误用示例 39 | * **重复使用同一个锁实例**: 40 | 41 | ```go 42 | lock := New(rdb, "key") 43 | lock.Lock(ctx) 44 | lock.Lock(ctx) // ❌ 不建议复用同一实例 45 | ``` 46 | 47 | * **未正确解锁**(如异常退出): 48 | 49 | ```go 50 | lock.Lock(ctx) 51 | // defer lock.UnLock(ctx) ❌ 忘记解锁,可能造成死锁 52 | ``` 53 | 54 | * **公平锁未传 requestId**: 55 | 56 | ```go 57 | lock.FairLock(ctx, "") // ❌ requestId 必须唯一 58 | ``` 59 | 60 | * **TTL 过短,业务未执行完锁就失效**: 61 | 62 | ```go 63 | WithTimeout(1 * time.Second) // ❌ 业务耗时远超 1s 64 | ``` 65 | 66 | * **联锁子 key 重复**: 67 | 68 | ```go 69 | NewMultiLock("same_key", "same_key") // ❌ 子锁应唯一 70 | ``` 71 | 72 | ## 常见误用示例 73 | - 重复使用同一个锁实例 74 | - 忘记解锁(如提前 return 或 panic) 75 | - 公平锁未传唯一 requestId 76 | - TTL 设置过短导致业务未完成锁就失效 77 | - 联锁中传入重复的 key 78 | 79 | ## 🛠 如何排查锁相关问题 80 | * ✅ **加锁失败**:检查 Redis 是否连通、key 是否已被其他实例持有。 81 | * ✅ **无法解锁**:确认解锁时的 token 是否与加锁时一致。 82 | * ✅ **公平锁无效**:确认是否使用唯一 requestId,是否等待超时。 83 | * ✅ **锁提前释放**:检查 TTL 设置是否合理,自动续期是否生效。 84 | * ✅ **死锁/资源卡住**:查看是否未解锁、续期失败或锁资源使用冲突。 85 | * ✅ **联锁失败**:确认是否某个子锁获取失败、子 key 是否重复。 86 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | linux-build: 11 | runs-on: ubuntu-latest 12 | services: 13 | redis: 14 | image: redis:7.2 15 | ports: 16 | - 63790:6379 17 | options: >- 18 | --health-cmd "redis-cli ping" 19 | --health-interval 10s 20 | --health-timeout 5s 21 | --health-retries 5 22 | 23 | strategy: 24 | matrix: 25 | go-version: [ '1.21' ] 26 | 27 | steps: 28 | - uses: actions/checkout@v6 29 | 30 | - name: Set up Go 31 | uses: actions/setup-go@v6 32 | with: 33 | go-version: ${{ matrix.go-version }} 34 | check-latest: true 35 | cache: true 36 | id: go 37 | 38 | - name: GO Env 39 | run: | 40 | go version 41 | go env 42 | 43 | - name: Get dependencies 44 | run: | 45 | go get -v -t -d ./... 46 | 47 | - name: Lint 48 | run: | 49 | go vet $(go list ./...) 50 | go mod tidy 51 | if ! test -z "$(git status --porcelain)"; then 52 | echo "Please run 'go mod tidy'" 53 | exit 1 54 | fi 55 | 56 | - name: Build 57 | run: go build -v ./... 58 | 59 | # Test Unit 60 | - name: Test 61 | run: make test 62 | 63 | - name: Test and create coverage file 64 | run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... 65 | 66 | - name: Upload coverage reports to Codecov 67 | uses: codecov/codecov-action@v5 68 | with: 69 | token: ${{secrets.CODECOV_TOKEN}} 70 | 71 | # Test Analytics 72 | - name: Install dependencies 73 | run: | 74 | go install github.com/jstemmer/go-junit-report@v1.0.0 75 | go install gotest.tools/gotestsum@v1.12.0 76 | 77 | - name: Run tests and generate coverage report 78 | run: gotestsum --junitfile junit.xml 79 | 80 | - name: Upload test results to Codecov 81 | if: ${{ !cancelled() }} 82 | uses: codecov/test-results-action@v1 83 | with: 84 | token: ${{ secrets.CODECOV_TOKEN }} 85 | -------------------------------------------------------------------------------- /docs/good_bad.md: -------------------------------------------------------------------------------- 1 | # 推荐写法:每次加锁创建新的 RedisLock 实例 2 | 在使用 Redis 实现分布式锁时,**强烈推荐每次加锁都创建一个新的 `RedisLock` 实例**。这样可以有效避免状态污染和并发冲突,提高系统的健壮性与可维护性。 3 | 4 | ## ✅ 推荐写法(Good Code) 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "context" 11 | redislock "github.com/jefferyjob/go-redislock" 12 | ) 13 | 14 | // GoodLock 每次加锁创建新的 RedisLock 实例。 15 | // 优点:上下文隔离、线程安全、生命周期清晰,适用于高并发和异步场景。 16 | func GoodLock(ctx context.Context, rdb redislock.RedisInter) error { 17 | lock := redislock.New(rdb, "test_key") 18 | if err := lock.Lock(ctx); err != nil { 19 | return err 20 | } 21 | defer lock.UnLock(ctx) 22 | 23 | // 执行业务逻辑... 24 | return nil 25 | } 26 | ``` 27 | 28 | ### ✅ 优势说明: 29 | * 每次调用 `New()` 创建新实例,**不共享内部状态**; 30 | * **上下文(context)、锁 key、token 等字段独立**,无交叉影响; 31 | * **支持并发调用与异步逻辑**,线程安全; 32 | * **生命周期更清晰**,避免资源复用带来的隐藏副作用; 33 | * 易于单元测试,**可注入 Redis 依赖、Mock 接口**。 34 | 35 | --- 36 | 37 | ## ❌ 不推荐写法(Bad Code) 38 | ```go 39 | // 不推荐:复用全局 RedisLock 实例,存在并发安全问题 40 | var globalLock = &redislock.RedisLock{ 41 | // redis: 提前初始化的 Redis 实例 42 | } 43 | 44 | func BadLock(ctx context.Context) error { 45 | if err := globalLock.Lock(ctx); err != nil { 46 | return err 47 | } 48 | defer globalLock.UnLock(ctx) 49 | 50 | // 执行业务逻辑... 51 | return nil 52 | } 53 | ``` 54 | 55 | ### ❌ 问题分析: 56 | * **共享状态**:多个调用会复用同一个 `RedisLock` 实例,导致上下文、key、token 相互覆盖; 57 | * **并发冲突**:在高并发场景下,容易出现竞态条件、死锁或误释放; 58 | * **可测性差**:全局对象难以注入替代依赖,不利于单元测试与 Mock; 59 | * **可读性差**:锁的生命周期和状态变化隐藏在共享实例中,排查问题困难; 60 | * **不适合异步/多租户逻辑**:实例中字段可能被并发修改,引发不可预期的问题。 61 | 62 | --- 63 | 64 | ## 🔍 推荐与反例对比总结 65 | | 项目 | 每次创建新实例 ✅(推荐) | 重用全局实例 ❌(不推荐) | 66 | | ----------- | ------------- | ------------- | 67 | | 并发安全性 | ✅ 高 | ❌ 低 | 68 | | 状态隔离 | ✅ 完全隔离 | ❌ 共享易污染 | 69 | | 单元测试支持 | ✅ 易于注入依赖 | ❌ 不利于测试 | 70 | | 可读性与维护性 | ✅ 生命周期清晰 | ❌ 状态隐蔽难排查 | 71 | | 异步/多协程兼容性 | ✅ 安全支持 | ❌ 易出错 | 72 | | 适配多场景(如多租户) | ✅ 灵活支持 | ❌ 难以拓展 | 73 | 74 | 75 | ## 🚀 性能优化建议:结合 sync.Pool 使用 76 | 如果你使用的是 `sync.Pool` 优化性能,也应该创建新的 `RedisLock` 实例,再从池中取结构体字段(如脚本、锁逻辑等)重用,而不是锁对象本身共享。 77 | 78 | 79 | ## ✅ 总结建议 80 | 始终遵循以下原则: 81 | 82 | * 每次加锁都 **新建 RedisLock 实例**; 83 | * 不复用带有状态的锁对象; 84 | * 用结构体字段缓存来提升性能,而不是锁实例本身; 85 | * 保持锁的 **短生命周期、强隔离、可测试、可维护**。 86 | 87 | 这样可大幅提升分布式锁的健壮性与开发效率,避免由状态复用带来的复杂问题。 -------------------------------------------------------------------------------- /tests/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 2 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 3 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 4 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 5 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 6 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/jefferyjob/go-redislock/adapter/go-redis/V9 v0.0.0-20251210060753-5b2e8d62842e h1:bEuJQ2y9VA0VnBcqbzJdrRUIN1RbPLs8alSneZ4R4Xs= 14 | github.com/jefferyjob/go-redislock/adapter/go-redis/V9 v0.0.0-20251210060753-5b2e8d62842e/go.mod h1:ra4EsXnZcd0mLEo7M7r0nY7Uh2acvTVaSfUBpU/5g38= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= 18 | github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 19 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 20 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /lua/fairLock.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Fair Queue Distributed Lock Using ZSET (基于有序集合的公平分布式锁脚本) 3 | 4 | 功能描述: 5 | 本 Lua 脚本使用 Redis ZSET 实现一个带排队机制的公平分布式锁。 6 | 每个客户端以请求 ID 加入队列,并按时间顺序排队,只有队首请求才能尝试加锁。 7 | 同时支持设置最大请求等待时间,自动清理过期请求,避免死锁或长时间占用队列。 8 | 9 | 使用场景: 10 | - 多客户端需要公平排队获取同一个资源的场景; 11 | - 需要显式控制锁等待时间,清除僵尸请求; 12 | - 比传统 SETNX 更具公平性和可控性。 13 | 14 | 输入参数: 15 | KEYS[1] - 锁的 key(如 "resource-lock") 16 | ARGV[1] - 请求 ID(一般为客户端 ID + 唯一请求标识,如 UUID) 17 | ARGV[2] - 锁的过期时间(毫秒,lock_ttl) 18 | ARGV[3] - 请求最大等待时间(毫秒,request_timeout) 19 | 20 | Redis 数据结构说明: 21 | 1. 锁 key: Redis String,存储当前持有锁的请求 ID 22 | 2. 排队 key: Redis Sorted Set(ZSET),score 为请求时间戳,value 为请求 ID 23 | 24 | 执行流程: 25 | 1. 获取当前时间戳 current_time; 26 | 2. 清理 queue_key 中所有超过 request_timeout 的请求(ZREMRANGEBYSCORE); 27 | 3. 将当前请求 ID 按当前时间戳添加到 ZSET 队列中(ZADD); 28 | 4. 设置 queue_key 过期时间为 request_timeout(用于自动过期清理); 29 | 5. 检查当前请求是否是队首(ZRANGE 0 0): 30 | - 是,则尝试使用 SET NX EX 获取锁; 31 | - 如果成功,加锁成功,返回 1; 32 | - 否则或不是队首,则返回 0。 33 | 34 | 返回值: 35 | - 1:加锁成功(当前请求是队首且成功获取锁) 36 | - 0 :加锁失败(未轮到或抢锁失败) 37 | 38 | 建议使用说明(客户端逻辑): 39 | - 客户端加锁失败应设置间隔轮询重试; 40 | - 请求 ID 应具备唯一性; 41 | - 可扩展为锁续期、解锁和队列清理等完整锁管理模块。 42 | 43 | 注意事项: 44 | - 当前时间使用 `redis.call('TIME')[1]`,单位为秒; 45 | - 脚本设计为幂等,重复调用不会产生副作用; 46 | - 若客户端意外宕机未解锁,锁将在 TTL 后自动释放,但队列中残留项会自动过期清除; 47 | - 可根据需要改为毫秒时间戳并配合 PEXPIRE 精细控制。 48 | 49 | --]] 50 | 51 | 52 | local lock_key = '{' .. KEYS[1] .. '}' 53 | local queue_key = lock_key .. ':queue' 54 | local request_id = ARGV[1] 55 | local lock_ttl = tonumber(ARGV[2]) 56 | local request_timeout = tonumber(ARGV[3]) 57 | 58 | -- 当前毫秒数 59 | local current_time = tonumber(redis.call('TIME')[1]) 60 | local current_time_ms = current_time * 1000 61 | 62 | -- 清理超时的请求 63 | redis.call('ZREMRANGEBYSCORE', queue_key, 0, current_time_ms - request_timeout) 64 | 65 | -- 加锁(排队) 66 | -- 将请求 ID 添加到队列中,并设置过期时间 67 | redis.call('ZADD', queue_key, 'NX', current_time_ms, request_id) 68 | redis.call('PEXPIRE', queue_key, request_timeout) 69 | 70 | -- 判断自己是否在队首 71 | if redis.call('ZRANK', queue_key, request_id) ~= 0 then 72 | return 0 -- 还没轮到我 73 | end 74 | 75 | -- 只有队首才尝试抢锁 76 | if redis.call('SET', lock_key, request_id, 'NX', 'PX', lock_ttl) then 77 | return 1 78 | end 79 | 80 | return 0 -- 锁被别人占着 81 | -------------------------------------------------------------------------------- /adapter/go-redis/V8/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 2 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 3 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 4 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 5 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 6 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 7 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 8 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 12 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 13 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 14 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 15 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 16 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 17 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= 18 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 19 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 20 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 22 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 23 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 24 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 25 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 26 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 27 | -------------------------------------------------------------------------------- /adapter/README.md: -------------------------------------------------------------------------------- 1 | # Redis 客户端适配器 2 | `go-redislock` 提供了可扩展的客户端适配机制,并内置支持多个主流 Redis 客户端。 如需完整示例,请参考 [examples](../examples)。 3 | 4 | 如果您当前使用的 Redis 客户端未被支持,也可以通过实现 `RedisInter` 接口来自定义适配器。 5 | 6 | ## 📦 导入适配器 7 | 请选择与您当前使用的 Redis 客户端版本相匹配的适配器: 8 | 9 | ```bash 10 | # go-redis v9 11 | go get -u github.com/jefferyjob/go-redislock/adapter/go-redis/V9 12 | 13 | # go-redis v8 14 | go get -u github.com/jefferyjob/go-redislock/adapter/go-redis/V8 15 | 16 | # go-redis v7 17 | go get -u github.com/jefferyjob/go-redislock/adapter/go-redis/V7 18 | 19 | # go-zero 20 | go get -u github.com/jefferyjob/go-redislock/adapter/go-zero/V1 21 | ``` 22 | 23 | ## ❓ 没有适配器符合你的客户端 24 | 如果内置适配器无法满足需求,只需实现以下接口即可接入任何 Redis 客户端: 25 | 26 | ```go 27 | // RedisInter 定义 Redis 客户端的最小能力集 28 | type RedisInter interface { 29 | Eval(ctx context.Context, script string, keys []string, args ...interface{}) RedisCmd 30 | } 31 | 32 | // RedisCmd 定义 Eval 返回结果的通用访问形式 33 | type RedisCmd interface { 34 | Result() (interface{}, error) 35 | Int64() (int64, error) 36 | } 37 | ``` 38 | 39 | 实现以上接口后即可直接与 `go-redislock` 联动。 40 | 41 | ## 🛠 示例:自定义 Goframe gredis 适配器 42 | 以下示例展示如何将 Goframe 的 `gredis` 客户端封装为可用于 `go-redislock` 的 Redis 适配器: 43 | 44 | ```go 45 | package adapter 46 | 47 | import ( 48 | "context" 49 | "fmt" 50 | 51 | "github.com/gogf/gf/v2/database/gredis" 52 | "github.com/gogf/gf/v2/frame/g" 53 | redislock "github.com/jefferyjob/go-redislock" 54 | ) 55 | 56 | type RdbAdapter struct { 57 | client *gredis.Redis 58 | } 59 | 60 | func New(client *gredis.Redis) redislock.RedisInter { 61 | return &RdbAdapter{client: client} 62 | } 63 | 64 | func (r *RdbAdapter) Eval(ctx context.Context, script string, keys []string, args ...interface{}) redislock.RedisCmd { 65 | eval, err := r.client.Eval(ctx, script, int64(len(keys)), keys, args) 66 | return &RdbCmdWrapper{ 67 | cmd: eval, 68 | err: err, 69 | } 70 | } 71 | 72 | type RdbCmdWrapper struct { 73 | cmd *g.Var 74 | err error 75 | } 76 | 77 | func (w *RdbCmdWrapper) Result() (interface{}, error) { 78 | if w.err != nil { 79 | return nil, w.err 80 | } 81 | return w.cmd.Val(), nil 82 | } 83 | 84 | func (w *RdbCmdWrapper) Int64() (int64, error) { 85 | if w.err != nil { 86 | return 0, w.err 87 | } 88 | 89 | switch v := w.cmd.Val().(type) { 90 | case int64: 91 | return v, nil 92 | case int: 93 | return int64(v), nil 94 | case string: 95 | var i int64 96 | _, err := fmt.Sscanf(v, "%d", &i) 97 | return i, err 98 | default: 99 | return 0, fmt.Errorf("cannot convert result to int: %T", w.cmd) 100 | } 101 | } 102 | ``` 103 | -------------------------------------------------------------------------------- /tests/lock_write_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | redislock "github.com/jefferyjob/go-redislock" 11 | ) 12 | 13 | // 测试读锁升级为写锁 14 | func Test_WLockByReadLock(t *testing.T) { 15 | adapter := getRedisClient() 16 | 17 | tests := []struct { 18 | name string 19 | inputKey string 20 | inputRToken string 21 | inputWToken string 22 | wantRErr error 23 | wantWErr error 24 | }{ 25 | { 26 | name: "读锁和写锁同token,读锁升级写锁成功", 27 | inputKey: "testKey", 28 | inputRToken: "testToken", 29 | inputWToken: "testToken", 30 | }, 31 | { 32 | name: "读锁和写锁不同token,读锁升级写锁失败", 33 | inputKey: "testKey", 34 | inputRToken: "testRToken", 35 | inputWToken: "testWToken", 36 | wantWErr: redislock.ErrLockFailed, 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | var ( 43 | wg = &sync.WaitGroup{} 44 | ) 45 | 46 | // 线程1:读锁 47 | f1 := func(wg *sync.WaitGroup) { 48 | defer wg.Done() 49 | ctx := context.Background() 50 | lock := redislock.New(adapter, tt.inputKey, redislock.WithToken(tt.inputRToken)) 51 | err := lock.RLock(ctx) 52 | if !errors.Is(err, tt.wantRErr) { 53 | t.Errorf("Failed to Rlock: %v", err) 54 | } 55 | time.Sleep(time.Second * 2) // 确保读锁保持执行,留给写锁足够的事情抢夺 56 | defer lock.RUnLock(ctx) 57 | } 58 | 59 | // 线程2:写锁 60 | f2 := func(wg *sync.WaitGroup) { 61 | defer wg.Done() 62 | time.Sleep(time.Second * 1) // 确保读锁已经加锁成功 63 | ctx := context.Background() 64 | lock := redislock.New(adapter, tt.inputKey, redislock.WithToken(tt.inputWToken)) 65 | err := lock.WLock(ctx) 66 | if !errors.Is(err, tt.wantWErr) { 67 | t.Errorf("Failed to Wlock: %v", err) 68 | } 69 | defer lock.WUnLock(ctx) 70 | } 71 | 72 | wg.Add(2) 73 | go f1(wg) 74 | go f2(wg) 75 | wg.Wait() 76 | }) 77 | } 78 | } 79 | 80 | // 测试读锁可重入 81 | func TestWLockReentrant(t *testing.T) { 82 | adapter := getRedisClient() 83 | 84 | var ( 85 | inputKey = "testKey" 86 | inputToken = "testToken" 87 | ) 88 | 89 | ctx := context.Background() 90 | 91 | for i := 0; i < 5; i++ { 92 | lock := redislock.New(adapter, inputKey, 93 | redislock.WithToken(inputToken), 94 | redislock.WithAutoRenew(), 95 | ) 96 | err := lock.WLock(ctx) 97 | if err != nil { 98 | t.Errorf("Failed to lock: %v", err) 99 | } 100 | 101 | time.Sleep(time.Second * 1) 102 | 103 | if i == 4 { 104 | lock.WUnLock(ctx) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /adapter/go-zero/V1/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jefferyjob/go-redislock/adapter/go-zero/V1 2 | 3 | go 1.21 4 | 5 | replace github.com/jefferyjob/go-redislock => ../../.. 6 | 7 | require ( 8 | github.com/jefferyjob/go-redislock v1.7.0-beta 9 | github.com/zeromicro/go-zero v1.9.3 10 | ) 11 | 12 | require ( 13 | github.com/beorn7/perks v1.0.1 // indirect 14 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 15 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 16 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 17 | github.com/fatih/color v1.18.0 // indirect 18 | github.com/go-logr/logr v1.4.2 // indirect 19 | github.com/go-logr/stdr v1.2.2 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 22 | github.com/klauspost/compress v1.17.11 // indirect 23 | github.com/mattn/go-colorable v0.1.13 // indirect 24 | github.com/mattn/go-isatty v0.0.20 // indirect 25 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 26 | github.com/openzipkin/zipkin-go v0.4.3 // indirect 27 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 28 | github.com/prometheus/client_golang v1.21.1 // indirect 29 | github.com/prometheus/client_model v0.6.1 // indirect 30 | github.com/prometheus/common v0.62.0 // indirect 31 | github.com/prometheus/procfs v0.15.1 // indirect 32 | github.com/redis/go-redis/v9 v9.16.0 // indirect 33 | github.com/spaolacci/murmur3 v1.1.0 // indirect 34 | go.opentelemetry.io/otel v1.24.0 // indirect 35 | go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect 36 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect 37 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect 38 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect 39 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect 40 | go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect 41 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 42 | go.opentelemetry.io/otel/sdk v1.24.0 // indirect 43 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 44 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 45 | go.uber.org/automaxprocs v1.6.0 // indirect 46 | golang.org/x/net v0.35.0 // indirect 47 | golang.org/x/sys v0.30.0 // indirect 48 | golang.org/x/text v0.22.0 // indirect 49 | google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect 50 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect 51 | google.golang.org/grpc v1.65.0 // indirect 52 | google.golang.org/protobuf v1.36.5 // indirect 53 | gopkg.in/yaml.v2 v2.4.0 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 2 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 6 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 7 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 8 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 9 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 11 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 12 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 14 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 15 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 21 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 22 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 23 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 24 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 25 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 26 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 27 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 28 | -------------------------------------------------------------------------------- /tests/lock_read_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | redislock "github.com/jefferyjob/go-redislock" 11 | ) 12 | 13 | // 测试写锁抢夺读锁 14 | func TestRLockByWriteLock(t *testing.T) { 15 | adapter := getRedisClient() 16 | 17 | tests := []struct { 18 | name string 19 | inputKey string 20 | inputRToken string 21 | inputWToken string 22 | wantRErr error 23 | wantWErr error 24 | }{ 25 | { 26 | name: "读锁抢夺写锁,自己持有写锁:允许同时持有读锁,成功", 27 | inputKey: "testKey", 28 | inputRToken: "testToken", 29 | inputWToken: "testToken", 30 | }, 31 | { 32 | name: "读锁抢夺写锁,他人持有写锁,失败", 33 | inputKey: "testKey2", 34 | inputRToken: "testRToken", 35 | inputWToken: "testWToken", 36 | wantRErr: redislock.ErrLockFailed, 37 | }, 38 | } 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | var ( 43 | wg = &sync.WaitGroup{} 44 | ) 45 | 46 | // 线程1:写锁 47 | f1 := func(wg *sync.WaitGroup) { 48 | defer wg.Done() 49 | ctx := context.Background() 50 | lock := redislock.New(adapter, tt.inputKey, 51 | redislock.WithToken(tt.inputWToken), 52 | // redislock.WithTimeout(time.Second*60), 53 | ) 54 | err := lock.WLock(ctx) 55 | if !errors.Is(err, tt.wantWErr) { 56 | t.Errorf("Failed to Wlock: %v", err) 57 | } 58 | time.Sleep(time.Second * 5) // 确保写锁保持执行,留给读锁足够的事情抢夺 59 | defer lock.WUnLock(ctx) 60 | } 61 | 62 | // 线程2:读锁 63 | f2 := func(wg *sync.WaitGroup) { 64 | defer wg.Done() 65 | time.Sleep(1 * time.Second) // 确保写锁已经加锁成功 66 | ctx := context.Background() 67 | lock := redislock.New(adapter, tt.inputKey, 68 | redislock.WithToken(tt.inputRToken), 69 | // redislock.WithTimeout(time.Second*60), 70 | ) 71 | err := lock.RLock(ctx) 72 | if !errors.Is(err, tt.wantRErr) { 73 | t.Errorf("Failed to Rlock: %v", err) 74 | } 75 | defer lock.RUnLock(ctx) 76 | } 77 | 78 | wg.Add(2) 79 | go f1(wg) 80 | go f2(wg) 81 | wg.Wait() 82 | }) 83 | } 84 | } 85 | 86 | // 读锁的可重入能力 87 | func TestRLockReentrant(t *testing.T) { 88 | adapter := getRedisClient() 89 | 90 | var ( 91 | inputKey = "testKey" 92 | inputToken = "testToken" 93 | ) 94 | 95 | ctx := context.Background() 96 | 97 | for i := 0; i < 5; i++ { 98 | lock := redislock.New(adapter, inputKey, 99 | redislock.WithToken(inputToken), 100 | redislock.WithAutoRenew(), 101 | ) 102 | err := lock.RLock(ctx) 103 | if err != nil { 104 | t.Errorf("Failed to lock: %v", err) 105 | } 106 | time.Sleep(time.Second * 1) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lock_read.go: -------------------------------------------------------------------------------- 1 | package go_redislock 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "errors" 7 | "log" 8 | "time" 9 | ) 10 | 11 | var ( 12 | //go:embed lua/readLock.lua 13 | readLockScript string 14 | //go:embed lua/readUnLock.lua 15 | readUnLockScript string 16 | //go:embed lua/readRenew.lua 17 | readRenewScript string 18 | ) 19 | 20 | func (l *RedisLock) RLock(ctx context.Context) error { 21 | res, err := l.redis.Eval(ctx, readLockScript, 22 | []string{l.key}, 23 | l.token, 24 | l.lockTimeout.Milliseconds(), 25 | ).Int64() 26 | 27 | if err != nil { 28 | return errors.Join(err, ErrException) 29 | } 30 | 31 | if res != 1 { 32 | return ErrLockFailed 33 | } 34 | 35 | if l.isAutoRenew { 36 | ctxRenew, cancel := context.WithCancel(ctx) 37 | l.autoRenewCancel = cancel 38 | go l.autoRLockRenew(ctxRenew) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func (l *RedisLock) RUnLock(ctx context.Context) error { 45 | // 如果已经创建了取消函数,则执行取消操作 46 | if l.autoRenewCancel != nil { 47 | l.autoRenewCancel() 48 | } 49 | 50 | res, err := l.redis.Eval( 51 | ctx, 52 | readUnLockScript, 53 | []string{l.key}, l.token, 54 | ).Int64() 55 | 56 | if err != nil { 57 | return errors.Join(err, ErrException) 58 | } 59 | if res != 1 { 60 | return ErrUnLockFailed 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (l *RedisLock) SpinRLock(ctx context.Context, timeout time.Duration) error { 67 | exp := time.Now().Add(timeout) 68 | for { 69 | if time.Now().After(exp) { 70 | return ErrSpinLockTimeout 71 | } 72 | 73 | // 加锁成功直接返回 74 | if err := l.RLock(ctx); err == nil { 75 | return nil 76 | } 77 | 78 | // 如果加锁失败,则休眠一段时间再尝试 79 | select { 80 | case <-ctx.Done(): 81 | return errors.Join(ErrSpinLockDone, context.Canceled) // 处理取消操作 82 | case <-time.After(100 * time.Millisecond): 83 | // 继续尝试下一轮加锁 84 | } 85 | } 86 | } 87 | 88 | func (l *RedisLock) RRenew(ctx context.Context) error { 89 | res, err := l.redis.Eval( 90 | ctx, 91 | readRenewScript, 92 | []string{l.key}, 93 | l.token, 94 | l.lockTimeout.Milliseconds(), 95 | ).Int64() 96 | 97 | if err != nil { 98 | return errors.Join(err, ErrException) 99 | } 100 | 101 | if res != 1 { 102 | return ErrLockRenewFailed 103 | } 104 | 105 | return nil 106 | } 107 | 108 | // 锁自动续期 109 | func (l *RedisLock) autoRLockRenew(ctx context.Context) { 110 | ticker := time.NewTicker(l.lockTimeout / 3) 111 | defer ticker.Stop() 112 | 113 | for { 114 | select { 115 | case <-ctx.Done(): 116 | return 117 | case <-ticker.C: 118 | err := l.RRenew(ctx) 119 | if err != nil { 120 | log.Printf("Error: autoRRenew failed, %v", err) 121 | return 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lock_write.go: -------------------------------------------------------------------------------- 1 | package go_redislock 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "errors" 7 | "log" 8 | "time" 9 | ) 10 | 11 | var ( 12 | //go:embed lua/writeLock.lua 13 | writeLockScript string 14 | //go:embed lua/writeUnLock.lua 15 | writeUnLockScript string 16 | //go:embed lua/writeRenew.lua 17 | writeRenewScript string 18 | ) 19 | 20 | func (l *RedisLock) WLock(ctx context.Context) error { 21 | res, err := l.redis.Eval(ctx, writeLockScript, 22 | []string{l.key}, 23 | l.token, 24 | l.lockTimeout.Milliseconds(), 25 | ).Int64() 26 | 27 | if err != nil { 28 | return errors.Join(err, ErrException) 29 | } 30 | 31 | if res != 1 { 32 | return ErrLockFailed 33 | } 34 | 35 | if l.isAutoRenew { 36 | ctxRenew, cancel := context.WithCancel(ctx) 37 | l.autoRenewCancel = cancel 38 | go l.autoWLockRenew(ctxRenew) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func (l *RedisLock) WUnLock(ctx context.Context) error { 45 | // 如果已经创建了取消函数,则执行取消操作 46 | if l.autoRenewCancel != nil { 47 | l.autoRenewCancel() 48 | } 49 | 50 | res, err := l.redis.Eval( 51 | ctx, 52 | writeUnLockScript, 53 | []string{l.key}, l.token, 54 | ).Int64() 55 | 56 | if err != nil { 57 | return errors.Join(err, ErrException) 58 | } 59 | if res != 1 { 60 | return ErrUnLockFailed 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (l *RedisLock) SpinWLock(ctx context.Context, timeout time.Duration) error { 67 | exp := time.Now().Add(timeout) 68 | for { 69 | if time.Now().After(exp) { 70 | return ErrSpinLockTimeout 71 | } 72 | 73 | // 加锁成功直接返回 74 | if err := l.WLock(ctx); err == nil { 75 | return nil 76 | } 77 | 78 | // 如果加锁失败,则休眠一段时间再尝试 79 | select { 80 | case <-ctx.Done(): 81 | return errors.Join(ErrSpinLockDone, context.Canceled) // 处理取消操作 82 | case <-time.After(100 * time.Millisecond): 83 | // 继续尝试下一轮加锁 84 | } 85 | } 86 | } 87 | 88 | func (l *RedisLock) WRenew(ctx context.Context) error { 89 | res, err := l.redis.Eval( 90 | ctx, 91 | writeRenewScript, 92 | []string{l.key}, 93 | l.token, 94 | l.lockTimeout.Milliseconds(), 95 | ).Int64() 96 | 97 | if err != nil { 98 | return errors.Join(err, ErrException) 99 | } 100 | 101 | if res != 1 { 102 | return ErrLockRenewFailed 103 | } 104 | 105 | return nil 106 | } 107 | 108 | // 锁自动续期 109 | func (l *RedisLock) autoWLockRenew(ctx context.Context) { 110 | ticker := time.NewTicker(l.lockTimeout / 3) 111 | defer ticker.Stop() 112 | 113 | for { 114 | select { 115 | case <-ctx.Done(): 116 | return 117 | case <-ticker.C: 118 | err := l.WRenew(ctx) 119 | if err != nil { 120 | log.Printf("Error: autoWRenew failed, %v", err) 121 | return 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /docs/读锁升级为写锁.md: -------------------------------------------------------------------------------- 1 | # 读锁升级为写锁流程文档 2 | 3 | ## 背景说明 4 | 在 Redis 实现的读写锁中: 5 | * **读锁(Read Lock)** 可以被多个读者同时持有 6 | * **写锁(Write Lock)** 是独占的,持有写锁的线程可以同时持有读锁(可重入) 7 | * **升级需求**:当一个线程已经持有读锁并希望获取写锁时,需要判断是否可以安全升级为写锁 8 | 9 | 升级写锁的核心原则: 10 | 11 | 1. **只允许自己独占的读锁升级** 12 | * 总读者数等于自己持有的读锁数量 13 | * 其他线程持有的读锁阻止升级 14 | 15 | 2. **升级原子化** 16 | * 必须通过 Lua 脚本在 Redis 中原子操作 17 | * 确保在升级过程中没有其他线程获取读锁或写锁 18 | 19 | ## 升级条件 20 | 21 | 在执行写锁加锁操作时: 22 | 23 | 1. 先获取锁的模式 `mode` 24 | * 如果 `mode` 为 `read`,则进入升级判断 25 | 26 | 2. 获取总读者数 `rcount` 和当前线程读锁计数 `r:owner` 27 | 28 | 3. 判断是否满足升级条件: 29 | ```text 30 | rcount == r:owner 31 | ``` 32 | * 条件成立:当前线程是唯一读锁持有者,可以升级 33 | * 条件不成立:存在其他读锁,无法升级 34 | 35 | ## 升级处理逻辑 36 | 当升级条件满足时,处理步骤如下: 37 | 38 | 1. 将锁模式切换为写锁: 39 | ```text 40 | mode = 'write' 41 | ``` 42 | 43 | 2. 设置写锁持有者: 44 | ```text 45 | writer = owner 46 | ``` 47 | 48 | 3. 初始化写锁可重入计数: 49 | ```text 50 | wcount = 1 51 | ``` 52 | 53 | 4. 刷新锁的 TTL: 54 | ```text 55 | PEXPIRE(local_key, lock_ttl) 56 | ``` 57 | 58 | 5. 返回加锁成功 59 | 60 | 如果升级条件不满足,则返回加锁失败,等待或重试 61 | 62 | ## Lua 脚本示例 63 | 64 | ```lua 65 | -- 获取总读者数和自身读锁计数 66 | local total = tonumber(redis.call('HGET', local_key, 'rcount') or '0') 67 | local self_cnt = tonumber(redis.call('HGET', local_key, 'r:' .. lock_value) or '0') 68 | 69 | if total == self_cnt then 70 | -- 升级读锁为写锁 71 | redis.call('HSET', local_key, 72 | 'mode', 'write', 73 | 'writer', lock_value, 74 | 'wcount', 1) 75 | redis.call('PEXPIRE', local_key, lock_ttl) 76 | return 1-- 升级成功 77 | end 78 | 79 | -- 如果其他线程也持有读锁,则升级失败 80 | return 0 81 | ``` 82 | 83 | ## 流程图 84 | 85 | ```mermaid 86 | flowchart TD 87 | 88 | A[开始 WLock 请求] --> B[获取锁 key 数据] 89 | B --> C{mode == read?} 90 | C -- 否 --> D[按正常写锁加锁流程处理] 91 | C -- 是 --> E[获取 rcount 和自身 r:owner] 92 | E --> F{rcount == r:owner?} 93 | F -- 是 --> G[升级锁为写锁:mode=write, writer=owner, wcount=1] 94 | G --> H[刷新 TTL] 95 | H --> I[返回加锁成功] 96 | F -- 否 --> J[存在其他读者,无法升级] 97 | J --> K[返回加锁失败] 98 | ``` 99 | 100 | ## 锁状态生命周期 101 | 102 | ```mermaid 103 | stateDiagram-v2 104 | 105 | [*] --> 空闲 : 初始化/锁不存在 106 | 空闲 --> 读锁 : RLock 107 | 空闲 --> 写锁 : WLock 108 | 109 | state 读锁 { 110 | [*] --> 单读者 111 | 单读者 --> 多读者 : 其他线程 RLock 112 | 多读者 --> 多读者 : 其他线程 RLock 113 | 单读者 --> 空闲 : RUnLock (最后一个读者) 114 | 多读者 --> 单读者 : RUnLock (减少读者数) 115 | 多读者 --> 空闲 : RUnLock (最后一个读者) 116 | } 117 | 118 | state 写锁 { 119 | [*] --> 写锁持有 120 | 写锁持有 --> 写锁持有 : WLock 可重入 121 | 写锁持有 --> 写锁持有 : RLock 可重入 122 | } 123 | 124 | %% 升级/降级用跨状态箭头 125 | 单读者 --> 写锁 : WLock (升级读锁) 126 | 写锁持有 --> 读锁 : WUnLock (降级) 127 | 写锁持有 --> 空闲 : WUnLock (无读锁) 128 | 129 | 读锁 --> 读锁 : RRenew 130 | 写锁 --> 写锁 : WRenew 131 | ``` 132 | 133 | ## 注意事项 134 | 135 | 1. **原子操作**:升级必须在 Lua 脚本中执行,避免多线程竞争导致错误升级 136 | 2. **可重入**:升级后,写锁持有者可以再次加写锁或读锁 137 | 3. **失败处理**:如果升级失败,线程可选择自旋等待、重试或直接返回失败 138 | 4. **TTL 管理**:升级操作完成后一定要刷新 TTL,避免锁过期被其他线程抢占 139 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main","dev" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main","dev" ] 20 | schedule: 21 | - cron: '31 11 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Use only 'java' to analyze code written in Java, Kotlin or both 38 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 39 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 40 | 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v6 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v4 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | 54 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 55 | # queries: security-extended,security-and-quality 56 | 57 | 58 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 59 | # If this step fails, then you should remove it and run the build manually (see below) 60 | - name: Autobuild 61 | uses: github/codeql-action/autobuild@v4 62 | 63 | # ℹ️ Command-line programs to run using the OS shell. 64 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 65 | 66 | # If the Autobuild fails above, remove it and uncomment the following three lines. 67 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 68 | 69 | # - run: | 70 | # echo "Run, Build Application using script" 71 | # ./location_of_script_within_repo/buildscript.sh 72 | 73 | - name: Perform CodeQL Analysis 74 | uses: github/codeql-action/analyze@v4 75 | with: 76 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /lock_fair.go: -------------------------------------------------------------------------------- 1 | package go_redislock 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "errors" 7 | "log" 8 | "time" 9 | ) 10 | 11 | var ( 12 | //go:embed lua/fairLock.lua 13 | fairLockScript string 14 | //go:embed lua/fairUnlock.lua 15 | fairUnLockScript string 16 | //go:embed lua/fairRenew.lua 17 | fairRenewScript string 18 | ) 19 | 20 | // FairLock 公平锁尝试加锁(使用指定的 requestId 获取公平锁) 21 | // FairLock tries to acquire a fair lock using the given requestId. 22 | // 公平锁确保请求按照顺序获取锁,避免饥饿现象 23 | // 如果是队首且成功获取锁则返回 nil,否则返回 ErrLockFailed 24 | func (l *RedisLock) FairLock(ctx context.Context, requestId string) error { 25 | result, err := l.redis.Eval(ctx, fairLockScript, 26 | []string{l.key}, 27 | requestId, 28 | l.lockTimeout.Milliseconds(), 29 | l.requestTimeout.Milliseconds(), 30 | ).Int64() 31 | 32 | if err != nil { 33 | return errors.Join(err, ErrException) 34 | } 35 | 36 | // 没有抢到锁,则进入排队,不是ok则说明不是队首 37 | if result != 1 { 38 | return ErrLockFailed 39 | } 40 | 41 | if l.isAutoRenew { 42 | ctxRenew, cancel := context.WithCancel(ctx) 43 | l.autoRenewCancel = cancel 44 | go l.autoFairRenew(ctxRenew, requestId) 45 | } 46 | 47 | return nil 48 | } 49 | 50 | // SpinFairLock keeps trying to acquire a fair lock until timeout. 51 | // SpinFairLock 在指定超时时间内不断尝试获取公平锁。 52 | func (l *RedisLock) SpinFairLock(ctx context.Context, requestId string, timeout time.Duration) error { 53 | exp := time.Now().Add(timeout) 54 | for { 55 | // 检查自旋锁是否超时 56 | if time.Now().After(exp) { 57 | return ErrSpinLockTimeout 58 | } 59 | 60 | // 尝试公平锁锁成功 61 | if err := l.FairLock(ctx, requestId); err == nil { 62 | return nil 63 | } 64 | 65 | // 如果加锁失败,则休眠一段时间再尝试 66 | select { 67 | case <-ctx.Done(): // 检查上下文是否已取消 68 | return errors.Join(ErrSpinLockDone, context.Canceled) 69 | case <-time.After(100 * time.Millisecond): 70 | // 继续尝试下一轮加锁 71 | } 72 | } 73 | } 74 | 75 | // FairUnLock releases the fair lock held by the given requestId. 76 | // FairUnLock 根据 requestId 释放公平锁。 77 | func (l *RedisLock) FairUnLock(ctx context.Context, requestId string) error { 78 | if l.autoRenewCancel != nil { 79 | l.autoRenewCancel() 80 | } 81 | 82 | result, err := l.redis.Eval( 83 | ctx, 84 | fairUnLockScript, 85 | []string{l.key}, 86 | requestId, 87 | ).Int64() 88 | 89 | if err != nil { 90 | return errors.Join(err, ErrException) 91 | } 92 | 93 | if result != 1 { 94 | return ErrUnLockFailed 95 | } 96 | 97 | return nil 98 | } 99 | 100 | // FairRenew manually extends the expiration of a fair lock. 101 | // FairRenew 手动延长指定 requestId 的公平锁有效期。 102 | func (l *RedisLock) FairRenew(ctx context.Context, requestId string) error { 103 | res, err := l.redis.Eval( 104 | ctx, 105 | fairRenewScript, 106 | []string{l.key}, 107 | requestId, 108 | l.lockTimeout.Milliseconds(), 109 | ).Int64() 110 | 111 | if err != nil { 112 | return errors.Join(err, ErrException) 113 | } 114 | 115 | if res != 1 { 116 | return ErrLockRenewFailed 117 | } 118 | 119 | return nil 120 | } 121 | 122 | // 锁自动续期 123 | func (l *RedisLock) autoFairRenew(ctx context.Context, requestId string) { 124 | ticker := time.NewTicker(l.lockTimeout / 3) 125 | defer ticker.Stop() 126 | 127 | for { 128 | select { 129 | case <-ctx.Done(): 130 | return 131 | case <-ticker.C: 132 | err := l.FairRenew(ctx, requestId) 133 | if err != nil { 134 | log.Printf("Error: autoFairRenew failed, Err: %v \n", err) 135 | return 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lock_reentrant.go: -------------------------------------------------------------------------------- 1 | package go_redislock 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | "errors" 7 | "log" 8 | "time" 9 | ) 10 | 11 | var ( 12 | //go:embed lua/reentrantLock.lua 13 | reentrantLockScript string 14 | //go:embed lua/reentrantUnLock.lua 15 | reentrantUnLockScript string 16 | //go:embed lua/reentrantRenew.lua 17 | reentrantRenewScript string 18 | ) 19 | 20 | // Lock tries to acquire a standard lock. 21 | // This implementation supports "reentrant locks". If the lock is currently held by the same key+token, reentry is allowed and the count is increased. Unlock() needs to be called a corresponding number of times to release the lock. 22 | // 23 | // Lock 尝试获取普通锁。 24 | // 该实现支持“可重入锁”,如果当前已由相同 key+token 持有,允许重入并增加计数。需调用相应次数 Unlock() 释放 25 | func (l *RedisLock) Lock(ctx context.Context) error { 26 | result, err := l.redis.Eval(ctx, reentrantLockScript, 27 | []string{l.key}, 28 | l.token, 29 | l.lockTimeout.Milliseconds(), 30 | ).Int64() 31 | 32 | if err != nil { 33 | return errors.Join(err, ErrException) 34 | } 35 | if result != 1 { 36 | return ErrLockFailed 37 | } 38 | 39 | if l.isAutoRenew { 40 | ctxRenew, cancel := context.WithCancel(ctx) 41 | l.autoRenewCancel = cancel 42 | go l.autoRenew(ctxRenew) 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // UnLock releases the standard lock. 49 | // If it is a reentrant lock, each call will reduce the holding count until the count reaches 0 and the lock will be released. 50 | // 51 | // UnLock 释放普通锁。 52 | // 如果为重入锁,每调用一次减少一次持有计数,直到计数为 0 锁会被释放 53 | func (l *RedisLock) UnLock(ctx context.Context) error { 54 | // 如果已经创建了取消函数,则执行取消操作 55 | if l.autoRenewCancel != nil { 56 | l.autoRenewCancel() 57 | } 58 | 59 | result, err := l.redis.Eval( 60 | ctx, 61 | reentrantUnLockScript, 62 | []string{l.key}, l.token, 63 | ).Int64() 64 | 65 | if err != nil { 66 | return errors.Join(err, ErrException) 67 | } 68 | if result != 1 { 69 | return ErrUnLockFailed 70 | } 71 | 72 | return nil 73 | } 74 | 75 | // SpinLock keeps trying to acquire the lock until timeout. 76 | // SpinLock 在指定超时时间内不断尝试加锁。 77 | func (l *RedisLock) SpinLock(ctx context.Context, timeout time.Duration) error { 78 | exp := time.Now().Add(timeout) 79 | for { 80 | if time.Now().After(exp) { 81 | return ErrSpinLockTimeout 82 | } 83 | 84 | // 加锁成功直接返回 85 | if err := l.Lock(ctx); err == nil { 86 | return nil 87 | } 88 | 89 | // 如果加锁失败,则休眠一段时间再尝试 90 | select { 91 | case <-ctx.Done(): 92 | return errors.Join(ErrSpinLockDone, context.Canceled) // 处理取消操作 93 | case <-time.After(100 * time.Millisecond): 94 | // 继续尝试下一轮加锁 95 | } 96 | } 97 | } 98 | 99 | // Renew manually extends the lock expiration. 100 | // Renew 手动延长锁的有效期。 101 | func (l *RedisLock) Renew(ctx context.Context) error { 102 | res, err := l.redis.Eval( 103 | ctx, 104 | reentrantRenewScript, 105 | []string{l.key}, 106 | l.token, 107 | l.lockTimeout.Milliseconds(), 108 | ).Int64() 109 | 110 | if err != nil { 111 | return errors.Join(err, ErrException) 112 | } 113 | 114 | if res != 1 { 115 | return ErrLockRenewFailed 116 | } 117 | 118 | return nil 119 | } 120 | 121 | // 锁自动续期 122 | func (l *RedisLock) autoRenew(ctx context.Context) { 123 | ticker := time.NewTicker(l.lockTimeout / 3) 124 | defer ticker.Stop() 125 | 126 | for { 127 | select { 128 | case <-ctx.Done(): 129 | return 130 | case <-ticker.C: 131 | err := l.Renew(ctx) 132 | if err != nil { 133 | log.Printf("Error: autoRenew failed, %v", err) 134 | return 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /adapter/go-redis/V7/go.sum: -------------------------------------------------------------------------------- 1 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 2 | github.com/go-redis/redis/v7 v7.4.1 h1:PASvf36gyUpr2zdOUS/9Zqc80GbM+9BDyiJSJDDOrTI= 3 | github.com/go-redis/redis/v7 v7.4.1/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg= 4 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 5 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 6 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 7 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 8 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 9 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 14 | github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= 15 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 16 | github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= 17 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 18 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 19 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 20 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= 21 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 22 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 23 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 24 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 25 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY= 26 | golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 28 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 29 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 30 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 34 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 35 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 36 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 37 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 38 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 39 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 40 | -------------------------------------------------------------------------------- /lock.go: -------------------------------------------------------------------------------- 1 | package go_redislock 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | ) 10 | 11 | // RedisInter Redis 客户端接口 12 | type RedisInter interface { 13 | Eval(ctx context.Context, script string, keys []string, args ...interface{}) RedisCmd 14 | } 15 | 16 | // RedisCmd Eval 返回结果的接口 17 | type RedisCmd interface { 18 | Result() (interface{}, error) 19 | Int64() (int64, error) 20 | } 21 | 22 | // type RedisInter interface { 23 | // redis.Scripter 24 | // } 25 | 26 | // RedisLockInter defines the interface for distributed Redis locks 27 | type RedisLockInter interface { 28 | // Lock 加锁 29 | Lock(ctx context.Context) error 30 | // SpinLock 自旋锁。 31 | SpinLock(ctx context.Context, timeout time.Duration) error 32 | // UnLock 解锁 33 | UnLock(ctx context.Context) error 34 | // Renew 锁续期 35 | Renew(ctx context.Context) error 36 | 37 | // FairLock 公平锁加锁 38 | FairLock(ctx context.Context, requestId string) error 39 | // SpinFairLock 自旋公平锁 40 | SpinFairLock(ctx context.Context, requestId string, timeout time.Duration) error 41 | // FairUnLock 公平锁解锁 42 | FairUnLock(ctx context.Context, requestId string) error 43 | // FairRenew 公平锁续期 44 | FairRenew(ctx context.Context, requestId string) error 45 | 46 | // RLock 读锁加锁 47 | RLock(ctx context.Context) error 48 | // RUnLock 读锁解锁 49 | RUnLock(ctx context.Context) error 50 | // SpinRLock 自旋读锁 51 | SpinRLock(ctx context.Context, timeout time.Duration) error 52 | // RRenew 读锁续期 53 | RRenew(ctx context.Context) error 54 | 55 | // WLock 写锁加锁 56 | WLock(ctx context.Context) error 57 | // WUnLock 写锁解锁 58 | WUnLock(ctx context.Context) error 59 | // SpinWLock 自旋写锁 60 | SpinWLock(ctx context.Context, timeout time.Duration) error 61 | // WRenew 写锁续期 62 | WRenew(ctx context.Context) error 63 | 64 | // MultiLock 联锁加锁 65 | // MultiLock(ctx context.Context, locks []RedisLockInter) error 66 | // MultiUnLock 联锁解锁 67 | // MultiUnLock(ctx context.Context, locks []RedisLockInter) error 68 | // SpinMultiLock 自旋联锁 69 | // SpinMultiLock(ctx context.Context, locks []RedisLockInter, timeout time.Duration) error 70 | // MultiRenew 联锁续期 71 | // MultiRenew(ctx context.Context, locks []RedisLockInter) error 72 | } 73 | 74 | type RedisLock struct { 75 | redis RedisInter 76 | key string 77 | token string 78 | lockTimeout time.Duration 79 | isAutoRenew bool 80 | requestTimeout time.Duration 81 | autoRenewCancel context.CancelFunc 82 | } 83 | 84 | type Option func(lock *RedisLock) 85 | 86 | // New creates a RedisLock instance 87 | func New(redisClient RedisInter, lockKey string, options ...Option) RedisLockInter { 88 | lock := &RedisLock{ 89 | redis: redisClient, 90 | lockTimeout: lockTime, // 锁默认超时时间 91 | requestTimeout: requestTimeout, // 公平锁在队列中的最大等待时间 92 | } 93 | 94 | for _, f := range options { 95 | f(lock) 96 | } 97 | lock.key = lockKey 98 | 99 | // 如果未设置锁的Token,则生成一个唯一的Token 100 | if lock.token == "" { 101 | lock.token = fmt.Sprintf("lock_token:%s", uuid.New().String()) 102 | } 103 | 104 | return lock 105 | } 106 | 107 | // WithTimeout sets the expiration time of the lock 108 | // WithTimeout 设置锁的过期时间 109 | func WithTimeout(timeout time.Duration) Option { 110 | return func(lock *RedisLock) { 111 | lock.lockTimeout = timeout 112 | } 113 | } 114 | 115 | // WithAutoRenew enables automatic lock renewal 116 | // WithAutoRenew 是否开启自动续期 117 | func WithAutoRenew() Option { 118 | return func(lock *RedisLock) { 119 | lock.isAutoRenew = true 120 | } 121 | } 122 | 123 | // WithToken sets a custom token for the lock instance 124 | // WithToken 设置锁的 Token,用于标识当前持有者 125 | func WithToken(token string) Option { 126 | return func(lock *RedisLock) { 127 | lock.token = token 128 | } 129 | } 130 | 131 | // WithRequestTimeout sets the maximum wait time in the fair lock queue 132 | // WithRequestTimeout 设置公平锁在队列中的最大等待时间 133 | func WithRequestTimeout(timeout time.Duration) Option { 134 | return func(lock *RedisLock) { 135 | lock.requestTimeout = timeout 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/lock_fair_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | redislock "github.com/jefferyjob/go-redislock" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_FairLock(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | inputKey string 17 | inputReqId string 18 | sleepTime time.Duration // 模拟业务执行时间 19 | wantErr error 20 | }{ 21 | { 22 | name: "公平锁-加锁成功", 23 | inputKey: "test_key", 24 | inputReqId: "test_req", 25 | }, 26 | } 27 | 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | ctx := context.Background() 31 | 32 | lock := redislock.New(getRedisClient(), tt.inputKey, 33 | redislock.WithAutoRenew(), 34 | redislock.WithRequestTimeout(lockTime), // 设置公平锁请求超时时间 35 | ) 36 | 37 | err := lock.FairLock(ctx, tt.inputReqId) 38 | if !errors.Is(err, tt.wantErr) { 39 | t.Errorf("Expected error = %v, wantErr %v", err, tt.wantErr) 40 | } 41 | defer lock.FairUnLock(ctx, tt.inputReqId) 42 | 43 | if tt.sleepTime != time.Duration(0) { 44 | // 模拟业务执行时间 45 | time.Sleep(tt.sleepTime) 46 | } 47 | }) 48 | } 49 | } 50 | 51 | func Test_FairUnLock(t *testing.T) { 52 | tests := []struct { 53 | name string 54 | inputKey string 55 | inputReqId string 56 | wantLockErr error 57 | wantUnlockErr error 58 | }{ 59 | { 60 | name: "公平锁-解锁成功", 61 | inputKey: "test_key", 62 | inputReqId: "test_req_id", 63 | wantLockErr: nil, 64 | wantUnlockErr: nil, 65 | }, 66 | } 67 | 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | ctx := context.Background() 71 | 72 | lock := redislock.New(getRedisClient(), tt.inputKey, redislock.WithAutoRenew()) 73 | err := lock.FairLock(ctx, tt.inputReqId) 74 | if !errors.Is(err, tt.wantLockErr) { 75 | t.Errorf("Expected error = %v, wantErr %v", err, tt.wantLockErr) 76 | } 77 | 78 | err = lock.FairUnLock(ctx, tt.inputReqId) 79 | if !errors.Is(err, tt.wantUnlockErr) { 80 | t.Errorf("Expected error = %v, wantErr %v", err, tt.wantUnlockErr) 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func Test_FairRenew(t *testing.T) { 87 | tests := []struct { 88 | name string 89 | inputKey string 90 | inputReqId string 91 | sleepTime time.Duration // 模拟业务执行时间 92 | wantErr error 93 | wantRenewErr error 94 | }{ 95 | { 96 | name: "公平锁-续期成功", 97 | inputKey: "key", 98 | inputReqId: "req_id", 99 | sleepTime: 2 * time.Second, 100 | wantErr: nil, 101 | wantRenewErr: nil, 102 | }, 103 | { 104 | name: "公平锁-续期失败", 105 | inputKey: "key", 106 | inputReqId: "req_id", 107 | sleepTime: 6 * time.Second, 108 | wantErr: nil, 109 | wantRenewErr: redislock.ErrLockRenewFailed, // 续期失败 110 | }, 111 | } 112 | 113 | for _, tt := range tests { 114 | t.Run(tt.name, func(t *testing.T) { 115 | ctx := context.Background() 116 | 117 | lock := redislock.New(getRedisClient(), tt.inputKey) 118 | err := lock.FairLock(ctx, tt.inputReqId) 119 | if !errors.Is(err, tt.wantErr) { 120 | t.Errorf("Expected error = %v, wantErr %v", err, tt.wantErr) 121 | } 122 | defer lock.FairUnLock(ctx, tt.inputReqId) 123 | 124 | // 模拟业务执行时间 125 | if tt.sleepTime != time.Duration(0) { 126 | time.Sleep(tt.sleepTime) 127 | } 128 | 129 | err = lock.FairRenew(ctx, tt.inputReqId) 130 | if !errors.Is(err, tt.wantRenewErr) { 131 | t.Errorf("Expected error = %v, wantErr %v", err, tt.wantErr) 132 | } 133 | }) 134 | } 135 | } 136 | 137 | func Test_SpinFairLock(t *testing.T) { 138 | tests := []struct { 139 | name string 140 | inputKey string 141 | inputReqId string 142 | spinTimeout time.Duration 143 | before func() 144 | wantErr error 145 | }{ 146 | { 147 | name: "公平锁-自旋锁-加锁成功", 148 | inputKey: "test_key", 149 | inputReqId: "test_req", 150 | spinTimeout: 3 * time.Second, 151 | wantErr: nil, 152 | }, 153 | { 154 | name: "公平锁-自旋锁-加锁失败", 155 | inputKey: "test_key", 156 | inputReqId: "test_req", 157 | spinTimeout: 3 * time.Second, 158 | before: func() { 159 | ctx := context.Background() 160 | 161 | go func() { 162 | lock := redislock.New(getRedisClient(), "test_key_1") 163 | err := lock.SpinFairLock(ctx, "test_req_1", 5*time.Second) 164 | require.NoError(t, err) 165 | defer lock.FairUnLock(ctx, "test_req_1") 166 | time.Sleep(1 * time.Second) // 模拟业务执行时间 167 | }() 168 | }, 169 | wantErr: nil, 170 | }, 171 | } 172 | 173 | for _, tt := range tests { 174 | t.Run(tt.name, func(t *testing.T) { 175 | ctx := context.Background() 176 | 177 | lock := redislock.New(getRedisClient(), tt.inputKey) 178 | err := lock.SpinFairLock(ctx, tt.inputReqId, tt.spinTimeout) 179 | if !errors.Is(err, tt.wantErr) { 180 | t.Errorf("Expected error = %v, wantErr %v", err, tt.wantErr) 181 | } 182 | defer lock.FairUnLock(ctx, tt.inputReqId) 183 | }) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /docs/读写锁.md: -------------------------------------------------------------------------------- 1 | # 读写锁设计文档 2 | 本文档详细说明了基于 Redis 的读写锁实现,包括实现原理、锁状态、流程、事件和 Lua 脚本逻辑。 3 | 4 | ## 锁的设计目标 5 | 6 | 1. 读写分离: 7 | * 多个读者可以并发持有读锁。 8 | * 写者独占锁,写锁存在时阻止其他读/写锁获取。 9 | 10 | 2. 可重入: 11 | * 同一持有者可重复加读锁或写锁。 12 | * 持有写锁的同一持有者可以同时获取读锁。 13 | * 允许从“仅自己在读”升级到写锁。 14 | 15 | 3. 租约机制: 16 | * 锁设置 TTL(毫秒)保证超时自动释放。 17 | * 支持续期操作刷新 TTL。 18 | 19 | 4. 原子性: 20 | * 所有状态判断和更新通过单条 Lua 脚本完成,避免竞态。 21 | 22 | 5. 可观测性: 23 | * 脚本返回值统一,用于判断成功、失败和剩余 TTL。 24 | 25 | 26 | ## 锁的数据结构 27 | 28 | 使用 Redis Hash 存储单个资源锁状态,结构如下: 29 | 30 | | 字段名 | 类型 | 说明 | 31 | |----------| ------ | ----------------------------- | 32 | | mode | string | 锁模式: "read" 或 "write",不存在表示空闲 | 33 | | rcount | int | 总读者数 | 34 | | r:{uuid} | int | 某个持有者的读锁可重入计数 | 35 | | writer | string | 当前写锁持有者 ID(仅 write 模式存在) | 36 | | wcount | int | 写锁可重入计数(仅 write 模式存在) | 37 | 38 | **uuid**: 当前使用 `github.com/google/uuid` 获取唯一标识。 39 | 40 | 41 | ## 锁状态流程图 42 | 43 | ### 读锁流程图 44 | #### 读锁加锁流程(RLock) 45 | 46 | ```mermaid 47 | flowchart TD 48 | A[开始 RLock] --> B[获取锁 key 数据] 49 | B --> C{锁是否存在?} 50 | C -- 否 --> D[设置 mode=read, rcount=1, r:owner=1] 51 | D --> E[设置 TTL] 52 | E --> F[返回成功] 53 | C -- 是 --> G{mode == read?} 54 | G -- 是 --> H[自身 r:owner+1, rcount+1] 55 | H --> I[刷新 TTL] 56 | I --> F 57 | G -- 否 --> J{mode == write 且 writer == owner?} 58 | J -- 是 --> K[允许重入:r:owner+1, rcount+1] 59 | K --> I 60 | J -- 否 --> L[返回失败] 61 | L --> F 62 | ``` 63 | 64 | #### 读锁解锁流程(RUnLock) 65 | 66 | ```mermaid 67 | flowchart TD 68 | A[开始 RUnLock] --> B[获取 r:owner 计数] 69 | B --> C{self_cnt <= 0?} 70 | C -- 是 --> D[返回失败] 71 | C -- 否 --> E[self_cnt-1, rcount-1] 72 | E --> F{self_cnt == 0?} 73 | F -- 是 --> G[删除 r:owner 字段] 74 | F -- 否 --> H[继续] 75 | H --> I[获取总 rcount] 76 | I --> J{rcount <=0 ?} 77 | J -- 是 --> K{mode == read ?} 78 | K -- 是 --> L[DEL key] 79 | K -- 否 --> M[HDEL rcount] 80 | J -- 否 --> N[保持锁] 81 | L --> O[返回成功] 82 | M --> O 83 | N --> O 84 | ``` 85 | 86 | #### 读锁续期流程(RRenew) 87 | 88 | ```mermaid 89 | flowchart TD 90 | A[开始 RRenew] --> B[获取 r:owner 计数] 91 | B --> C{self_cnt <= 0?} 92 | C -- 是 --> D[返回失败] 93 | C -- 否 --> E[刷新 TTL] 94 | E --> F[返回成功] 95 | ``` 96 | 97 | ### 写锁流程图 98 | #### 写锁加锁流程(WLock) 99 | 100 | ```mermaid 101 | flowchart TD 102 | A[开始 WLock] --> B[获取锁 key 数据] 103 | B --> C{锁是否存在?} 104 | C -- 否 --> D[设置 mode=write, writer=owner, wcount=1] 105 | D --> E[设置 TTL] 106 | E --> F[返回成功] 107 | C -- 是 --> G{mode == write 且 writer == owner?} 108 | G -- 是 --> H[wcount+1, 刷新 TTL] 109 | H --> F 110 | G -- 否 --> I{mode == read 且 rcount == r:owner?} 111 | I -- 是 --> J[升级锁:mode=write, writer=owner, wcount=1] 112 | J --> E 113 | I -- 否 --> K[返回失败] 114 | K --> F 115 | ``` 116 | 117 | #### 写锁解锁流程(WUnLock) 118 | 119 | ```mermaid 120 | flowchart TD 121 | A[开始 WUnLock] --> B[获取 writer] 122 | B --> C{writer != owner?} 123 | C -- 是 --> D[返回失败] 124 | C -- 否 --> E[wcount-1] 125 | E --> F{wcount>0?} 126 | F -- 是 --> G[返回成功] 127 | F -- 否 --> H[删除 writer, wcount] 128 | H --> I[获取 rcount] 129 | I --> J{rcount>0?} 130 | J -- 是 --> K[切回 read mode] 131 | J -- 否 --> L[DEL key] 132 | K --> M[返回成功] 133 | L --> M 134 | ``` 135 | 136 | #### 写锁续期流程(WRenew) 137 | 138 | ```mermaid 139 | flowchart TD 140 | A[开始 WRenew] --> B[获取 writer] 141 | B --> C{writer != owner?} 142 | C -- 是 --> D[返回失败] 143 | C -- 否 --> E[刷新 TTL] 144 | E --> F[返回成功] 145 | ``` 146 | 147 | ### 其他情况流程图 148 | #### 读锁加锁遇到写锁流程 149 | ```mermaid 150 | flowchart TD 151 | A[开始 RLock] --> B[获取锁 key 数据] 152 | B --> C{mode == write?} 153 | C -- 否 --> D[按正常读锁流程处理] 154 | C -- 是 --> E[获取 writer] 155 | E --> F{writer == owner?} 156 | F -- 是 --> G[允许重入:r:owner+1, rcount+1] 157 | G --> H[刷新 TTL] 158 | H --> I[返回成功] 159 | F -- 否 --> J[无法获取读锁] 160 | J --> K[返回失败] 161 | ``` 162 | 163 | #### 写锁加锁遇到读锁流程 164 | ```mermaid 165 | flowchart TD 166 | A[开始 WLock] --> B[获取锁 key 数据] 167 | B --> C{mode == read?} 168 | C -- 否 --> D[按正常写锁流程处理] 169 | C -- 是 --> E[获取 rcount 和自身 r:owner] 170 | E --> F{rcount == r:owner?} 171 | F -- 是 --> G[升级锁:mode=write, writer=owner, wcount=1] 172 | G --> H[刷新 TTL] 173 | H --> I[返回成功] 174 | F -- 否 --> J[存在其他读者,无法获取写锁] 175 | J --> K[返回失败] 176 | ``` 177 | 178 | ## 锁操作事件 179 | | 操作 | 条件/逻辑说明 | 180 | | ------- | ----------------------------------- | 181 | | RLock | 空闲或读锁模式时成功;写锁模式下若持有者为自己也成功,否则失败 | 182 | | RUnLock | 自身读锁计数减 1;若总读者数为 0 且无写锁则删除锁键 | 183 | | WLock | 空闲时成功;持有写锁者可重入;读模式且仅自己读时可升级为写锁;否则失败 | 184 | | WUnLock | 写计数减 1;归零时若有读者则切回读锁,否则删除锁键 | 185 | | RRenew | 刷新 TTL,仅当持有读锁的 owner 调用成功 | 186 | | WRenew | 刷新 TTL,仅当持有写锁的 owner 调用成功 | 187 | 188 | 189 | ## 注意事项 190 | 1. **uuid 唯一性**:确保每个客户端/线程使用唯一 uuid,避免破坏可重入计数。 191 | 2. **租期策略**:TTL 由 Redis 管理,客户端可以使用续期看门狗策略保持锁。 192 | 3. **升级与降级**:读锁可升级为写锁仅限“仅自己读”;写锁降级为读锁在写计数归零时自动处理。 193 | 4. **高并发优化**:单键结构在高并发场景下可能成为热点,可按资源做分片或使用 Lua 脚本保证原子性。 194 | 5. **异常恢复**:客户端崩溃后锁会自动过期,确保不会永久阻塞其他线程。 195 | -------------------------------------------------------------------------------- /CHANGELOG.cn.md: -------------------------------------------------------------------------------- 1 | ## v1.5.0 2 | - adapter 进行独立的包管理 [#108](https://github.com/jefferyjob/go-redislock/pull/108) 3 | - 更新 changelog 文件 [#109](https://github.com/jefferyjob/go-redislock/pull/109) 4 | 5 | ## v1.4.0 6 | - Bump github.com/zeromicro/go-zero from 1.8.5 to 1.9.0 [#85](https://github.com/jefferyjob/go-redislock/pull/85) 7 | - 将 README 默认语言改为中文 [#86](https://github.com/jefferyjob/go-redislock/pull/86) 8 | - Bump github.com/stretchr/testify from 1.10.0 to 1.11.0 [#87](https://github.com/jefferyjob/go-redislock/pull/87) 9 | - 更新 changelog 文件 [#88](https://github.com/jefferyjob/go-redislock/pull/88) 10 | - 新增读锁和写锁功能 [#89](https://github.com/jefferyjob/go-redislock/pull/89) 11 | - 文档更新 [#90](https://github.com/jefferyjob/go-redislock/pull/90) 12 | - Bump actions/setup-go from 5 to 6 [#91](https://github.com/jefferyjob/go-redislock/pull/91) 13 | - Bump github.com/redis/go-redis/v9 from 9.12.1 to 9.13.0 [#92](https://github.com/jefferyjob/go-redislock/pull/92) 14 | - Bump github.com/redis/go-redis/v9 from 9.13.0 to 9.14.0 [#96](https://github.com/jefferyjob/go-redislock/pull/96) 15 | - Bump github.com/zeromicro/go-zero from 1.9.0 to 1.9.1 [#97](https://github.com/jefferyjob/go-redislock/pull/97) 16 | - 为读锁和写锁增加单元测试代码 [#98](https://github.com/jefferyjob/go-redislock/pull/98) 17 | - 因为 [v9.15.1](https://github.com/redis/go-redis/releases/tag/v9.15.1) 更新调整逻辑 [#99](https://github.com/jefferyjob/go-redislock/pull/99) 18 | - 新增与移除 Lua 脚本 [#100](https://github.com/jefferyjob/go-redislock/pull/100) 19 | - Bump github/codeql-action from 3 to 4 [#103](https://github.com/jefferyjob/go-redislock/pull/103) 20 | - Bump github.com/redis/go-redis/v9 from 9.14.0 to 9.14.1 [#105](https://github.com/jefferyjob/go-redislock/pull/105) 21 | - 文档优化 [#107](https://github.com/jefferyjob/go-redislock/pull/107) 22 | 23 | ## v1.3.0 24 | - ci actions 标签配置 [#57](https://github.com/jefferyjob/go-redislock/pull/57) 25 | - 更新 change log [#58](https://github.com/jefferyjob/go-redislock/pull/58) 26 | - 支持公平锁加锁、解锁、自旋锁 [#59](https://github.com/jefferyjob/go-redislock/pull/59) 27 | - 新增示例 demo [#60](https://github.com/jefferyjob/go-redislock/pull/60) 28 | - 文件名调整 [#61](https://github.com/jefferyjob/go-redislock/pull/61) 29 | - 支持中英文注释 [#62](https://github.com/jefferyjob/go-redislock/pull/62) 30 | - 优化 context 定义 [#63](https://github.com/jefferyjob/go-redislock/pull/63) 31 | - 每个方法传入 ctx [#66](https://github.com/jefferyjob/go-redislock/pull/66) 32 | - 支持适配不同的 Redis 客户端包 [#67](https://github.com/jefferyjob/go-redislock/pull/67) 33 | - 增加 good code 和 bad code 示例 [#69](https://github.com/jefferyjob/go-redislock/pull/69) 34 | - 完善 examples 示例内容 [#70](https://github.com/jefferyjob/go-redislock/pull/70) 35 | - 本地单元测试 [#71](https://github.com/jefferyjob/go-redislock/pull/71) 36 | - 优化 MustNewRedisAdapter [#72](https://github.com/jefferyjob/go-redislock/pull/72) 37 | - 提交单元测试代码 [#73](https://github.com/jefferyjob/go-redislock/pull/73) 38 | - 修复单元测试 panic 错误(无法收集) [#74](https://github.com/jefferyjob/go-redislock/pull/74) 39 | - 优化 codecov.yml 配置 [#75](https://github.com/jefferyjob/go-redislock/pull/75) 40 | - 移除 gozero 和 goframe 的 redis adapter [#76](https://github.com/jefferyjob/go-redislock/pull/76) 41 | - 回滚 “移除 gozero 和 goframe 的 redis adapter” [#78](https://github.com/jefferyjob/go-redislock/pull/78) 42 | - 优化文档内容 [#79](https://github.com/jefferyjob/go-redislock/pull/79) 43 | - 新建 Adapter 包 [#80](https://github.com/jefferyjob/go-redislock/pull/80) 44 | - CI 增加 redis 安装流程 [#81](https://github.com/jefferyjob/go-redislock/pull/81) 45 | - 适配器单元测试 [#82](https://github.com/jefferyjob/go-redislock/pull/82) 46 | - Bump github.com/redis/go-redis/v9 依赖:9.11.0 → 9.12.1 [#83](https://github.com/jefferyjob/go-redislock/pull/83) 47 | - Bump actions/checkout from 3 → 5 [#84](https://github.com/jefferyjob/go-redislock/pull/84) 48 | 49 | ## v1.2.0 50 | - go版本升级到1.24 [#54](https://github.com/jefferyjob/go-redislock/pull/54) 51 | 52 | ## v1.1.4 53 | - 修复lua脚本支持集群哈希卡槽的错误 [#44](https://github.com/jefferyjob/go-redislock/pull/44) 54 | 55 | ## v1.1.3 56 | - codecov:测试分析 [#32](https://github.com/jefferyjob/go-redislock/pull/32) 57 | - Go多版本CI测试 [#33](https://github.com/jefferyjob/go-redislock/pull/33) 58 | - 更新lua脚本为毫秒单位 [#35](https://github.com/jefferyjob/go-redislock/pull/35) 59 | - 更新changelog文件 [#37](https://github.com/jefferyjob/go-redislock/pull/37) 60 | - 修改文档中的错误 [#38](https://github.com/jefferyjob/go-redislock/pull/38) 61 | - 修复可重入锁解锁 [#39](https://github.com/jefferyjob/go-redislock/pull/39) 62 | 63 | ## v1.1.2 64 | - Dependabot 计划间隔每周 [#27](https://github.com/jefferyjob/go-redislock/pull/27) 65 | - 删除毫无意义的 `sync.Mutex` [#28](https://github.com/jefferyjob/go-redislock/pull/28) 66 | - 优化可重入锁的命名 [#29](https://github.com/jefferyjob/go-redislock/pull/29) 67 | - 更新问题表单 [#31](https://github.com/jefferyjob/go-redislock/pull/31) 68 | 69 | ## v1.1.1 70 | - 单元测试覆盖与错误优化 [#25](https://github.com/jefferyjob/go-redislock/pull/25) 71 | - 错误修复:在并发情况下,token相似会导致多次获取锁 [#26](https://github.com/jefferyjob/go-redislock/pull/26) 72 | 73 | ## v1.1.0 74 | - 兼容新版本`redis/go-redis` [#17](https://github.com/jefferyjob/go-redislock/pull/17) 75 | - 错误统一定义 [#18](https://github.com/jefferyjob/go-redislock/pull/18) 76 | - 删除未使用的选项方法 [#19](https://github.com/jefferyjob/go-redislock/pull/19) 77 | - 调整自动续订时间 [#20](https://github.com/jefferyjob/go-redislock/pull/20) 78 | - 将 `github.com/redis/go-redis/v9` 从 `9.5.4` 升级到 `9.6.1` [#23](https://github.com/jefferyjob/go-redislock/pull/23) 79 | 80 | ## v1.0.3 81 | - 优化Lua脚本 [#16](https://github.com/jefferyjob/go-redislock/pull/16) 82 | 83 | ## v1.0.2 84 | - 讲 `v1.0.0` 标记废弃 [#15](https://github.com/jefferyjob/go-redislock/pull/15) 85 | - 将 `codecov/codecov-action` 升级到版本4 [#11](https://github.com/jefferyjob/go-redislock/pull/11) 86 | 87 | ## v1.0.1 88 | - 修复包名问题 [#10](https://github.com/jefferyjob/go-redislock/pull/10) 89 | 90 | ## v1.0.0 91 | - 利用 Redis 后端存储,确保分布式锁的稳定性和可靠性 92 | - 提供简单易用的 API,轻松实现加锁、解锁、自旋锁、自动续期和手动续期等功能 93 | - 支持自定义超时时间和自动续期,根据实际需求进行灵活配置 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-redislock 2 | 3 | [![Go](https://img.shields.io/badge/Go->=1.21-green)](https://go.dev) 4 | [![Release](https://img.shields.io/github/v/release/jefferyjob/go-redislock.svg)](https://github.com/jefferyjob/go-redislock/releases) 5 | [![Action](https://github.com/jefferyjob/go-redislock/actions/workflows/go.yml/badge.svg)](https://github.com/jefferyjob/go-redislock/actions/workflows/go.yml) 6 | [![Report](https://goreportcard.com/badge/github.com/jefferyjob/go-redislock)](https://goreportcard.com/report/github.com/jefferyjob/go-redislock) 7 | [![Coverage](https://codecov.io/gh/jefferyjob/go-redislock/branch/main/graph/badge.svg)](https://codecov.io/gh/jefferyjob/go-redislock) 8 | [![Doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/jefferyjob/go-redislock) 9 | [![License](https://img.shields.io/github/license/jefferyjob/go-redislock)](https://github.com/jefferyjob/go-redislock/blob/main/LICENSE) 10 | 11 | [English](README.en.md) | 简体中文 12 | 13 | ## 介绍 14 | go-redislock 是一个用于 Go 的库,用于使用 Redis 作为后端存储提供分布式锁功能。确保在分布式环境中的并发访问下实现数据共享和资源互斥。我们的分布式锁具备可靠性、高性能、超时机制、可重入性和灵活的锁释放方式等特性,简化了分布式锁的使用,让您专注于业务逻辑的实现。 15 | 16 | 我们实现了以下关键能力: 17 | 18 | - 🔒 普通分布式锁(可重入) 19 | - 🔁 自旋锁 20 | - ⚖️ 公平锁(FIFO 顺序) 21 | - 🧵读锁(多个读者并发访问,互斥写者) 22 | - ✍️写锁(独占访问资源) 23 | - 🔄 手动续期与自动续期 24 | - ✅ 多 Redis 客户端适配(v7/v8/v9、go-zero) 25 | 26 | ## 快速开始 27 | 28 | ### 安装 29 | ```bash 30 | go get -u github.com/jefferyjob/go-redislock 31 | 32 | # 根据所使用的 Redis 客户端选择匹配的适配器。 33 | go get -u github.com/jefferyjob/go-redislock/adapter/go-redis/V9 34 | ``` 35 | 36 | ### 使用Demo 37 | ```go 38 | package main 39 | 40 | import ( 41 | "context" 42 | "fmt" 43 | 44 | redislock "github.com/jefferyjob/go-redislock" 45 | adapter "github.com/jefferyjob/go-redislock/adapter/go-redis/V9" 46 | "github.com/redis/go-redis/v9" 47 | ) 48 | 49 | func main() { 50 | // Create a Redis client adapter 51 | rdbAdapter := adapter.New(redis.NewClient(&redis.Options{ 52 | Addr: "localhost:6379", 53 | })) 54 | 55 | // Create a context for canceling lock operations 56 | ctx := context.Background() 57 | 58 | // Create a RedisLock object 59 | lock := redislock.New(rdbAdapter, "test_key") 60 | 61 | // acquire lock 62 | err := lock.Lock(ctx) 63 | if err != nil { 64 | fmt.Println("lock acquisition failed:", err) 65 | return 66 | } 67 | defer lock.UnLock(ctx) // unlock 68 | 69 | // Perform tasks during lockdown 70 | // ... 71 | fmt.Println("task execution completed") 72 | } 73 | 74 | ``` 75 | 76 | ### 配置选项 77 | | **选项函数** | **说明** | **默认值** | 78 | | ----------------------------------- |------------------|---------| 79 | | WithTimeout(d time.Duration) | 锁超时时间(TTL) | 5s | 80 | | WithAutoRenew() | 是否自动续期 | false | 81 | | WithToken(token string) | 可重入锁 Token(唯一标识) | 随机 UUID | 82 | | WithRequestTimeout(d time.Duration) | 公平锁队列最大等待时间 | 同 TTL | 83 | 84 | 85 | ## 核心功能一览 86 | ### 普通锁 87 | | 方法名 | 说明 | 88 | |------------------------------|------------------------| 89 | | `Lock(ctx)` | 获取普通锁(支持可重入) | 90 | | `SpinLock(ctx, timeout)` | 自旋方式获取普通锁 | 91 | | `UnLock(ctx)` | 解锁操作 | 92 | | `Renew(ctx)` | 手动续期 | 93 | 94 | ### 公平锁(FIFO) 95 | | 方法名 | 说明 | 96 | |--------------------------------------------|----------------------| 97 | | `FairLock(ctx, requestId)` | 获取公平锁(FIFO) | 98 | | `SpinFairLock(ctx, requestId, timeout)` | 自旋方式获取公平锁 | 99 | | `FairUnLock(ctx, requestId)` | 公平锁解锁 | 100 | | `FairRenew(ctx, requestId)` | 公平锁续期 | 101 | 102 | ### 读锁 103 | | 方法名 | 说明 | 104 | |--------------------------|-------------| 105 | | `RLock(ctx)` | 获取读锁(支持可重入) | 106 | | `SpinRLock(ctx, timeout)` | 自旋方式获取读锁 | 107 | | `UnLRock(ctx)` | 解锁操作 | 108 | | `RRenew(ctx)` | 手动续期 | 109 | 110 | ### 写锁 111 | | 方法名 | 说明 | 112 | |--------------------------|-------------| 113 | | `WLock(ctx)` | 获取写锁(支持可重入) | 114 | | `SpinWLock(ctx, timeout)` | 自旋方式获取写锁 | 115 | | `UnWLock(ctx)` | 解锁操作 | 116 | | `WRenew(ctx)` | 手动续期 | 117 | 118 | ### 接口定义如下 119 | ```go 120 | type RedisLockInter interface { 121 | // Lock 加锁 122 | Lock(ctx context.Context) error 123 | // SpinLock 自旋锁 124 | SpinLock(ctx context.Context, timeout time.Duration) error 125 | // UnLock 解锁 126 | UnLock(ctx context.Context) error 127 | // Renew 手动续期 128 | Renew(ctx context.Context) error 129 | 130 | // FairLock 公平锁加锁 131 | FairLock(ctx context.Context, requestId string) error 132 | // SpinFairLock 自旋公平锁 133 | SpinFairLock(ctx context.Context, requestId string, timeout time.Duration) error 134 | // FairUnLock 公平锁解锁 135 | FairUnLock(ctx context.Context, requestId string) error 136 | // FairRenew 公平锁续期 137 | FairRenew(ctx context.Context, requestId string) error 138 | 139 | // RLock 读锁加锁 140 | RLock(ctx context.Context) error 141 | // RUnLock 读锁解锁 142 | RUnLock(ctx context.Context) error 143 | // SpinRLock 自旋读锁 144 | SpinRLock(ctx context.Context, timeout time.Duration) error 145 | // RRenew 读锁续期 146 | RRenew(ctx context.Context) error 147 | 148 | // WLock 写锁加锁 149 | WLock(ctx context.Context) error 150 | // WUnLock 写锁解锁 151 | WUnLock(ctx context.Context) error 152 | // SpinWLock 自旋写锁 153 | SpinWLock(ctx context.Context, timeout time.Duration) error 154 | // WRenew 写锁续期 155 | WRenew(ctx context.Context) error 156 | } 157 | ``` 158 | 159 | ## Redis客户端适配器支持 160 | go-redislock 提供高度可扩展的客户端适配机制,已内置支持以下主流 Redis 客户端,详细示例请参考 [examples](examples) 。 161 | 162 | | Redis客户端版本 | 包路径 | 是否支持 | 163 | |------------------|----------------------------------------------------------| -------- | 164 | | go-redis v7 | `github.com/jefferyjob/go-redislock/adapter/go-redis/V7` | ✅ | 165 | | go-redis v8 | `github.com/jefferyjob/go-redislock/adapter/go-redis/V8` | ✅ | 166 | | go-redis v9 | `github.com/jefferyjob/go-redislock/adapter/go-redis/V9` | ✅ | 167 | | go-zero redis | `github.com/jefferyjob/go-redislock/adapter/go-zero/V1` | ✅ | 168 | 169 | 如您使用的 Redis 客户端不在上述列表中,也可以实现接口 `RedisInter` 来接入任意 Redis 客户端。 170 | 171 | 172 | ## 注意事项 173 | - 每次加锁建议使用新的锁实例。 174 | - 加锁和解锁必须使用同一个 key 和 token。 175 | - 默认 TTL 是 5 秒,建议根据任务耗时自行设置。 176 | - 自动续期适合无阻塞任务,避免长时间阻塞。 177 | - 建议关键逻辑中使用 `defer unlock`,防止泄露。 178 | - 建议对锁获取失败、重试等行为做日志或监控。 179 | - 公平锁需传入唯一的 requestId(建议使用 UUID)。 180 | - 读锁可并发,写锁互斥,避免读写冲突。 181 | - 联锁中任一子锁失败,会释放已加成功的锁。 182 | - Redis 不可用时可能造成死锁风险。 183 | 184 | ## 许可证 185 | 本库采用 MIT 进行授权。有关详细信息,请参阅 LICENSE 文件。 186 | 187 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.5.0 2 | - Manage adapter as an independent package [#108](https://github.com/jefferyjob/go-redislock/pull/108) 3 | - Update changelog file [#109](https://github.com/jefferyjob/go-redislock/pull/109) 4 | 5 | ## v1.4.0 6 | - Bump github.com/zeromicro/go-zero from 1.8.5 to 1.9.0 [#85](https://github.com/jefferyjob/go-redislock/pull/85) 7 | - Set README default language to Chinese [#86](https://github.com/jefferyjob/go-redislock/pull/86) 8 | - Bump github.com/stretchr/testify from 1.10.0 to 1.11.0 [#87](https://github.com/jefferyjob/go-redislock/pull/87) 9 | - Update changelog file [#88](https://github.com/jefferyjob/go-redislock/pull/88) 10 | - Add read lock and write lock features [#89](https://github.com/jefferyjob/go-redislock/pull/89) 11 | - Docs update [#90](https://github.com/jefferyjob/go-redislock/pull/90) 12 | - Bump actions/setup-go from 5 to 6 [#91](https://github.com/jefferyjob/go-redislock/pull/91) 13 | - Bump github.com/redis/go-redis/v9 from 9.12.1 to 9.13.0 [#92](https://github.com/jefferyjob/go-redislock/pull/92) 14 | - Bump github.com/redis/go-redis/v9 from 9.13.0 to 9.14.0 [#96](https://github.com/jefferyjob/go-redislock/pull/96) 15 | - Bump github.com/zeromicro/go-zero from 1.9.0 to 1.9.1 [#97](https://github.com/jefferyjob/go-redislock/pull/97) 16 | - Add unit tests for read and write locks [#98](https://github.com/jefferyjob/go-redislock/pull/98) 17 | - Adjust logic due to [v9.15.1](https://github.com/redis/go-redis/releases/tag/v9.15.1) changes [#99](https://github.com/jefferyjob/go-redislock/pull/99) 18 | - Add and remove Lua scripts [#100](https://github.com/jefferyjob/go-redislock/pull/100) 19 | - Bump github/codeql-action from 3 to 4 [#103](https://github.com/jefferyjob/go-redislock/pull/103) 20 | - Bump github.com/redis/go-redis/v9 from 9.14.0 to 9.14.1 [#105](https://github.com/jefferyjob/go-redislock/pull/105) 21 | - Docs optimization [#107](https://github.com/jefferyjob/go-redislock/pull/107) 22 | 23 | 24 | ## v1.3.0 25 | - ci actions label configuration [#57](https://github.com/jefferyjob/go-redislock/pull/57) 26 | - change log update [#58](https://github.com/jefferyjob/go-redislock/pull/58) 27 | - Support fair lock for lock, unlock, and spin lock [#59](https://github.com/jefferyjob/go-redislock/pull/59) 28 | - example demo [#60](https://github.com/jefferyjob/go-redislock/pull/60) 29 | - File name adjustment [#61](https://github.com/jefferyjob/go-redislock/pull/61) 30 | - Support Chinese and English annotations [#62](https://github.com/jefferyjob/go-redislock/pull/62) 31 | - Optimize the definition of context [#63](https://github.com/jefferyjob/go-redislock/pull/63) 32 | - Pass ctx into each method [#66](https://github.com/jefferyjob/go-redislock/pull/66) 33 | - Support different Redis client adapters [#67](https://github.com/jefferyjob/go-redislock/pull/67) 34 | - good code and bad code examples [#69](https://github.com/jefferyjob/go-redislock/pull/69) 35 | - Improve examples content [#70](https://github.com/jefferyjob/go-redislock/pull/70) 36 | - Local unit tests [#71](https://github.com/jefferyjob/go-redislock/pull/71) 37 | - Optimize MustNewRedisAdapter [#72](https://github.com/jefferyjob/go-redislock/pull/72) 38 | - Submit unit test code [#73](https://github.com/jefferyjob/go-redislock/pull/73) 39 | - Fix panic in unit test that cannot be collected [#74](https://github.com/jefferyjob/go-redislock/pull/74) 40 | - codecov.yml configuration optimization [#75](https://github.com/jefferyjob/go-redislock/pull/75) 41 | - Remove the redis adapter for gozero and goframe [#76](https://github.com/jefferyjob/go-redislock/pull/76) 42 | - Revert "Remove the redis adapter for gozero and goframe" [#78](https://github.com/jefferyjob/go-redislock/pull/78) 43 | - Optimize document content [#79](https://github.com/jefferyjob/go-redislock/pull/79) 44 | - Creating an Adapter Package [#80](https://github.com/jefferyjob/go-redislock/pull/80) 45 | - workflow: ci install redis [#81](https://github.com/jefferyjob/go-redislock/pull/81) 46 | - Unit testing of adapters [#82](https://github.com/jefferyjob/go-redislock/pull/82) 47 | - Bump github.com/redis/go-redis/v9 from 9.11.0 to 9.12.1 [#83](https://github.com/jefferyjob/go-redislock/pull/83) 48 | - Bump actions/checkout from 3 to 5 [#84](https://github.com/jefferyjob/go-redislock/pull/84) 49 | 50 | ## v1.2.0 51 | - Go version upgraded to 1.24 [#54](https://github.com/jefferyjob/go-redislock/pull/54) 52 | 53 | ## v1.1.4 54 | - Script attempted to access a non local key in a cluster node script [#44](https://github.com/jefferyjob/go-redislock/pull/44) 55 | 56 | ## v1.1.3 57 | - codecov:Test Analytics [#32](https://github.com/jefferyjob/go-redislock/pull/32) 58 | - Go multi-version CI test [#33](https://github.com/jefferyjob/go-redislock/pull/33) 59 | - feat:update ttl to ms [#35](https://github.com/jefferyjob/go-redislock/pull/35) 60 | - Update the changelog file [#37](https://github.com/jefferyjob/go-redislock/pull/37) 61 | - fix: Modify errors in the document [#38](https://github.com/jefferyjob/go-redislock/pull/38) 62 | - Fix reentrant lock unlock [#39](https://github.com/jefferyjob/go-redislock/pull/39) 63 | 64 | ## v1.1.2 65 | - Dependabot scheduled every week [#27](https://github.com/jefferyjob/go-redislock/pull/27) 66 | - Delete meaningless `sync.Mutex` [#28](https://github.com/jefferyjob/go-redislock/pull/28) 67 | - Optimize the naming of reentrant locks [#29](https://github.com/jefferyjob/go-redislock/pull/29) 68 | - Update to issue question form [#31](https://github.com/jefferyjob/go-redislock/pull/31) 69 | 70 | ## v1.1.1 71 | - Unit test coverage and error optimization [#25](https://github.com/jefferyjob/go-redislock/pull/25) 72 | - Fix: In concurrent situations, similar tokens will cause multiple lock acquisitions [#26](https://github.com/jefferyjob/go-redislock/pull/26) 73 | 74 | ## v1.1.0 75 | - Compatible with new version `redis/go-redis` [#17](https://github.com/jefferyjob/go-redislock/pull/17) 76 | - Unify error definitions [#18](https://github.com/jefferyjob/go-redislock/pull/18) 77 | - Delete unused option methods [#19](https://github.com/jefferyjob/go-redislock/pull/19) 78 | - Adjust auto-renewal time [#20](https://github.com/jefferyjob/go-redislock/pull/20) 79 | - Upgrade `github.com/redis/go-redis/v9` from `9.5.4` to `9.6.1` [#23](https://github.com/jefferyjob/go-redislock/pull/23) 80 | 81 | ## v1.0.3 82 | - Optimize Lua scripts [#16](https://github.com/jefferyjob/go-redislock/pull/16) 83 | 84 | ## v1.0.2 85 | - Mark `v1.0.0` as deprecated [#15](https://github.com/jefferyjob/go-redislock/pull/15) 86 | - Upgrade `codecov/codecov-action` to version 4 [#11](https://github.com/jefferyjob/go-redislock/pull/11) 87 | 88 | ## v1.0.1 89 | - Fix package name issue [#10](https://github.com/jefferyjob/go-redislock/pull/10) 90 | 91 | ## v1.0.0 92 | - Use Redis backend storage to ensure the stability and reliability of distributed locks 93 | - Provides an easy-to-use API to easily implement functions such as lock, unlock, spin lock, automatic renewal, and manual renewal 94 | - Support custom timeout and automatic renewal, flexible configuration according to actual needs -------------------------------------------------------------------------------- /tests/lock_reentrant_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | redislock "github.com/jefferyjob/go-redislock" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_Lock(t *testing.T) { 14 | adapter := getRedisClient() 15 | 16 | tests := []struct { 17 | name string 18 | inputKey string 19 | inputToken string 20 | before func(inputKey string) 21 | wantErr error 22 | }{ 23 | { 24 | name: "创建锁对象-正常", 25 | inputKey: "test_key", 26 | inputToken: "test_token", 27 | wantErr: nil, 28 | }, 29 | { 30 | name: "创建锁对象-失败", 31 | inputKey: "test_key", 32 | inputToken: "test_token", 33 | before: func(inputKey string) { 34 | ctx := context.Background() 35 | lock := redislock.New(adapter, inputKey, redislock.WithToken("other_token")) 36 | _ = lock.Lock(ctx) 37 | }, 38 | wantErr: redislock.ErrLockFailed, 39 | }, 40 | } 41 | 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | if tt.before != nil { 45 | tt.before(tt.inputKey) 46 | } 47 | 48 | ctx := context.Background() 49 | lock := redislock.New(adapter, tt.inputKey, redislock.WithToken(tt.inputToken)) 50 | err := lock.Lock(ctx) 51 | if !errors.Is(err, tt.wantErr) { 52 | t.Errorf("expected error %v, got %v", tt.wantErr, err) 53 | } 54 | defer lock.UnLock(ctx) 55 | }) 56 | } 57 | } 58 | 59 | func Test_UnLock(t *testing.T) { 60 | adapter := getRedisClient() 61 | 62 | tests := []struct { 63 | name string 64 | inputKey string 65 | inputToken string 66 | between func(inputKey string, inputToken string) 67 | wantErr error 68 | }{ 69 | { 70 | name: "释放锁对象-正常", 71 | inputKey: "test_key", 72 | inputToken: "test_token", 73 | wantErr: nil, 74 | }, 75 | { 76 | name: "创建锁对象-失败", 77 | inputKey: "test_key", 78 | inputToken: "test_token", 79 | between: func(inputKey string, inputToken string) { 80 | ctx := context.Background() 81 | lock := redislock.New(adapter, inputKey, redislock.WithToken(inputToken)) 82 | _ = lock.UnLock(ctx) 83 | }, 84 | wantErr: redislock.ErrUnLockFailed, 85 | }, 86 | } 87 | 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | ctx := context.Background() 91 | lock := redislock.New(adapter, tt.inputKey, redislock.WithToken(tt.inputToken)) 92 | err := lock.Lock(ctx) 93 | if err != nil { 94 | require.NoError(t, err) 95 | } 96 | 97 | if tt.between != nil { 98 | tt.between(tt.inputKey, tt.inputToken) 99 | } 100 | 101 | err = lock.UnLock(ctx) 102 | if !errors.Is(err, tt.wantErr) { 103 | t.Errorf("expected error %v, got %v", tt.wantErr, err) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func Test_SpinLock(t *testing.T) { 110 | adapter := getRedisClient() 111 | 112 | tests := []struct { 113 | name string 114 | inputKey string 115 | inputToken string 116 | before func(ctx context.Context, lock redislock.RedisLockInter, cancel context.CancelFunc) 117 | inputTimeout time.Duration 118 | wantErr error 119 | }{ 120 | { 121 | name: "自旋锁-成功加锁", 122 | inputKey: "spin_key_success", 123 | inputToken: "token_success", 124 | before: func(ctx context.Context, lock redislock.RedisLockInter, cancel context.CancelFunc) { 125 | // 无操作,让 lock.SpinLock 能直接成功 126 | }, 127 | inputTimeout: time.Second * 3, 128 | wantErr: nil, 129 | }, 130 | { 131 | name: "自旋锁-超时未拿到锁", 132 | inputKey: "spin_key_timeout", 133 | inputToken: "token_timeout", 134 | before: func(ctx context.Context, lock redislock.RedisLockInter, cancel context.CancelFunc) { 135 | // 提前上锁,阻塞后续尝试 136 | go func() { 137 | owner := redislock.New(adapter, "spin_key_timeout", redislock.WithToken("other")) 138 | _ = owner.Lock(ctx) 139 | time.Sleep(time.Second * 5) // 保持锁 140 | _ = owner.UnLock(ctx) 141 | }() 142 | time.Sleep(time.Millisecond * 500) 143 | }, 144 | inputTimeout: time.Second * 2, 145 | wantErr: redislock.ErrSpinLockTimeout, 146 | }, 147 | { 148 | name: "自旋锁-Ctx取消", 149 | inputKey: "spin_key_cancel", 150 | inputToken: "token_cancel", 151 | before: func(ctx context.Context, lock redislock.RedisLockInter, cancel context.CancelFunc) { 152 | go func() { 153 | owner := redislock.New(adapter, "spin_key_cancel", redislock.WithToken("other")) 154 | _ = owner.Lock(ctx) 155 | time.Sleep(time.Second * 5) 156 | _ = owner.UnLock(ctx) 157 | }() 158 | time.Sleep(time.Millisecond * 500) 159 | go func() { 160 | time.Sleep(time.Second) 161 | cancel() // 主动取消 context 162 | }() 163 | }, 164 | inputTimeout: time.Second * 5, 165 | wantErr: redislock.ErrSpinLockDone, 166 | }, 167 | } 168 | 169 | for _, tt := range tests { 170 | t.Run(tt.name, func(t *testing.T) { 171 | ctx, cancel := context.WithCancel(context.Background()) 172 | lock := redislock.New(adapter, tt.inputKey, redislock.WithToken(tt.inputToken)) 173 | 174 | if tt.before != nil { 175 | tt.before(ctx, lock, cancel) 176 | } 177 | 178 | err := lock.SpinLock(ctx, tt.inputTimeout) 179 | if !errors.Is(err, tt.wantErr) { 180 | t.Errorf("expected error %v, got %v", tt.wantErr, err) 181 | } 182 | defer lock.UnLock(ctx) 183 | }) 184 | } 185 | } 186 | 187 | func Test_LockRenew(t *testing.T) { 188 | adapter := getRedisClient() 189 | 190 | tests := []struct { 191 | name string 192 | inputKey string 193 | inputToken string 194 | inputRenewToken string 195 | inputSleep time.Duration 196 | wantErr error 197 | }{ 198 | { 199 | name: "锁手动续期成功", 200 | inputKey: "test_key", 201 | inputToken: "token_ok", 202 | inputRenewToken: "token_ok", 203 | inputSleep: 6 * time.Second, 204 | wantErr: nil, 205 | }, 206 | { 207 | name: "锁手动续期失败-token不匹配", 208 | inputKey: "test_key", 209 | inputToken: "token_fail", 210 | inputRenewToken: "token_other", 211 | inputSleep: 6 * time.Second, 212 | wantErr: redislock.ErrLockRenewFailed, 213 | }, 214 | } 215 | 216 | for _, tt := range tests { 217 | t.Run(tt.name, func(t *testing.T) { 218 | ctx := context.Background() 219 | lock := redislock.New(adapter, tt.inputKey, redislock.WithToken(tt.inputToken)) 220 | err := lock.Lock(ctx) 221 | require.NoError(t, err) 222 | defer lock.UnLock(ctx) 223 | 224 | go func() { 225 | time.Sleep(time.Second * 2) 226 | ctx := context.Background() 227 | lock := redislock.New(adapter, tt.inputKey, redislock.WithToken(tt.inputRenewToken)) 228 | err = lock.Renew(ctx) 229 | if !errors.Is(err, tt.wantErr) { 230 | t.Errorf("expected error %v, got %v", tt.wantErr, err) 231 | } 232 | }() 233 | 234 | // 等待一段时间,确保续期操作完成 235 | time.Sleep(tt.inputSleep) 236 | }) 237 | } 238 | } 239 | 240 | func Test_LockAutoRenew(t *testing.T) { 241 | adapter := getRedisClient() 242 | 243 | tests := []struct { 244 | name string 245 | inputKey string 246 | inputToken string 247 | inputSleep time.Duration 248 | cancelTime time.Duration 249 | }{ 250 | { 251 | name: "锁自动续期成功", 252 | inputKey: "test_key_auto_ok", 253 | inputToken: "token_auto_ok", 254 | inputSleep: 10 * time.Second, 255 | cancelTime: 0, // 不提前取消 256 | }, 257 | { 258 | name: "锁自动续期-提前取消ctx", 259 | inputKey: "test_key_auto_cancel", 260 | inputToken: "token_auto_cancel", 261 | inputSleep: 10 * time.Second, 262 | cancelTime: 3 * time.Second, 263 | }, 264 | } 265 | 266 | for _, tt := range tests { 267 | t.Run(tt.name, func(t *testing.T) { 268 | ctx, cancel := context.WithCancel(context.Background()) 269 | lock := redislock.New(adapter, tt.inputKey, 270 | redislock.WithToken(tt.inputToken), 271 | redislock.WithAutoRenew(), 272 | ) 273 | 274 | err := lock.Lock(ctx) 275 | require.NoError(t, err) 276 | 277 | if tt.cancelTime > 0 { 278 | go func() { 279 | time.Sleep(tt.cancelTime) 280 | cancel() 281 | }() 282 | } 283 | 284 | time.Sleep(tt.inputSleep) 285 | _ = lock.UnLock(context.Background()) 286 | }) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # go-redislock 2 | 3 | [![Go](https://img.shields.io/badge/Go->=1.21-green)](https://go.dev) 4 | [![Release](https://img.shields.io/github/v/release/jefferyjob/go-redislock.svg)](https://github.com/jefferyjob/go-redislock/releases) 5 | [![Action](https://github.com/jefferyjob/go-redislock/actions/workflows/go.yml/badge.svg)](https://github.com/jefferyjob/go-redislock/actions/workflows/go.yml) 6 | [![Report](https://goreportcard.com/badge/github.com/jefferyjob/go-redislock)](https://goreportcard.com/report/github.com/jefferyjob/go-redislock) 7 | [![Coverage](https://codecov.io/gh/jefferyjob/go-redislock/branch/main/graph/badge.svg)](https://codecov.io/gh/jefferyjob/go-redislock) 8 | [![Doc](https://img.shields.io/badge/go.dev-reference-brightgreen?logo=go&logoColor=white&style=flat)](https://pkg.go.dev/github.com/jefferyjob/go-redislock) 9 | [![License](https://img.shields.io/github/license/jefferyjob/go-redislock)](https://github.com/jefferyjob/go-redislock/blob/main/LICENSE) 10 | 11 | English | [简体中文](README.md) 12 | 13 | ## Introduce 14 | go-redislock is a library for Go that provides distributed lock functionality using Redis as the backend storage. Ensure data sharing and resource mutual exclusion under concurrent access in a distributed environment. Our distributed lock has the characteristics of reliability, high performance, timeout mechanism, reentrancy and flexible lock release method, which simplifies the use of distributed lock and allows you to focus on the realization of business logic. 15 | 16 | We implemented the following key capabilities: 17 | 18 | - 🔒 Standard distributed locks (reentrant) 19 | - 🔁 Spin locks 20 | - ⚖️ Fair locks (FIFO order) 21 | - 🧵Read lock (multiple readers access concurrently, mutually exclusive writers) 22 | - ✍️Write lock (exclusive access to a resource) 23 | - 🔄 Manual and automatic renewal 24 | - ✅ Compatibility with multiple Redis clients (v7/v8/v9, go-zero) 25 | 26 | ## Quick start 27 | 28 | ### Install 29 | ```bash 30 | go get -u github.com/jefferyjob/go-redislock 31 | 32 | # Choose the matching adapter based on the Redis client you are using. 33 | go get -u github.com/jefferyjob/go-redislock/adapter/go-redis/V9 34 | ``` 35 | 36 | ### Use Demo 37 | ```go 38 | package main 39 | 40 | import ( 41 | "context" 42 | "fmt" 43 | 44 | redislock "github.com/jefferyjob/go-redislock" 45 | adapter "github.com/jefferyjob/go-redislock/adapter/go-redis/V9" 46 | "github.com/redis/go-redis/v9" 47 | ) 48 | 49 | func main() { 50 | // Create a Redis client adapter 51 | rdbAdapter := adapter.New(redis.NewClient(&redis.Options{ 52 | Addr: "localhost:6379", 53 | })) 54 | 55 | // Create a context for canceling lock operations 56 | ctx := context.Background() 57 | 58 | // Create a RedisLock object 59 | lock := redislock.New(rdbAdapter, "test_key") 60 | 61 | // acquire lock 62 | err := lock.Lock(ctx) 63 | if err != nil { 64 | fmt.Println("lock acquisition failed:", err) 65 | return 66 | } 67 | defer lock.UnLock(ctx) // unlock 68 | 69 | // Perform tasks during lockdown 70 | // ... 71 | fmt.Println("task execution completed") 72 | } 73 | 74 | ``` 75 | 76 | ### Configuration options 77 | | **Option function** | **Description** | **Default value** | 78 | | ----------------------------------- |------------------|---------| 79 | | WithTimeout(d time.Duration) | Lock timeout (TTL) | 5s | 80 | | WithAutoRenew() | Whether to automatically renew | false | 81 | | WithToken(token string) | Reentrant lock Token (unique identifier) | Random UUID | 82 | | WithRequestTimeout(d time.Duration) | Maximum waiting time for fair lock queue | Same as TTL | 83 | 84 | ## Core Function Overview 85 | ### Normal Lock 86 | | Method Name | Description | 87 | |------------------------------|------------------------| 88 | | `Lock(ctx)` | Acquire a normal lock (supports reentrancy) | 89 | | `SpinLock(ctx, timeout)` | Acquire a normal lock using a spinlock method | 90 | | `UnLock(ctx)` | Unlock operation | 91 | | `Renew(ctx)` | Manual renewal | 92 | 93 | ### Fair Lock (FIFO) 94 | | Method Name | Description | 95 | |--------------------------------------------|----------------------| 96 | | `FairLock(ctx, requestId)` | Acquire a fair lock (FIFO) | 97 | | `SpinFairLock(ctx, requestId, timeout)` | Acquire a fair lock using a spinlock method | 98 | | `FairUnLock(ctx, requestId)` | Unlock a fair lock | 99 | | `FairRenew(ctx, requestId)` | Fair Lock Renewal | 100 | 101 | ### Read Lock 102 | | Method Name | Description | 103 | |--------------------------|-------------| 104 | | `RLock(ctx)` | Acquire a read lock (supports reentrancy) | 105 | | `SpinRLock(ctx, timeout)` | Acquire a read lock using a spinlock | 106 | | `UnLRock(ctx)` | Unlock operation | 107 | | `RRenew(ctx)` | Manually renew the lock | 108 | 109 | ### Write Lock 110 | | Method Name | Description | 111 | |--------------------------|-------------| 112 | | `WLock(ctx)` | Acquire a write lock (supports reentrancy) | 113 | | `SpinWLock(ctx, timeout)` | Acquire a write lock using a spinlock | 114 | | `UnWLock(ctx)` | Unlock operation | 115 | | `WRenew(ctx)` | Manually renew the lock | 116 | 117 | ### The interface is defined as follows 118 | ```go 119 | type RedisLockInter interface { 120 | // Lock Locking 121 | Lock(ctx context.Context) error 122 | // SpinLock Spinlock 123 | SpinLock(ctx context.Context, timeout time.Duration) error 124 | // UnLock Unlocking 125 | UnLock(ctx context.Context) error 126 | // Renew Manual renewal 127 | Renew(ctx context.Context) error 128 | 129 | // FairLock Fair lock locking 130 | FairLock(ctx context.Context, requestId string) error 131 | // SpinFairLock Spin Fair Lock 132 | SpinFairLock(ctx context.Context, requestId string, timeout time.Duration) error 133 | // FairUnLock Fair Lock Unlock 134 | FairUnLock(ctx context.Context, requestId string) error 135 | // FairRenew Fair Lock Renew 136 | FairRenew(ctx context.Context, requestId string) error 137 | 138 | // RLock read lock locked 139 | RLock(ctx context.Context) error 140 | // RUnLock read lock unlocked 141 | RUnLock(ctx context.Context) error 142 | // SpinRLock spin read lock 143 | SpinRLock(ctx context.Context, timeout time.Duration) error 144 | // RRenew read lock renewed 145 | RRenew(ctx context.Context) error 146 | 147 | // WLock write lock locked 148 | WLock(ctx context.Context) error 149 | // WUnLock write lock unlocked 150 | WUnLock(ctx context.Context) error 151 | // SpinWLock spin write lock 152 | SpinWLock(ctx context.Context, timeout time.Duration) error 153 | // WRenew write lock renewed 154 | WRenew(ctx context.Context) error 155 | } 156 | ``` 157 | 158 | ## Redis client adapter supports 159 | go-redislock provides a highly scalable client adaptation mechanism, and has built-in support for the following mainstream Redis clients. For detailed examples, please refer to [examples](examples) . 160 | 161 | | Redis Client Version | Package path | Supported | 162 | |------------------|----------------------------------------------------------| -------- | 163 | | go-redis v7 | `github.com/jefferyjob/go-redislock/adapter/go-redis/V7` | ✅ | 164 | | go-redis v8 | `github.com/jefferyjob/go-redislock/adapter/go-redis/V8` | ✅ | 165 | | go-redis v9 | `github.com/jefferyjob/go-redislock/adapter/go-redis/V9` | ✅ | 166 | | go-zero redis | `github.com/jefferyjob/go-redislock/adapter/go-zero/V1` | ✅ | 167 | 168 | If the Redis client you are using is not in the above list, you can also implement the interface `RedisInter` to connect to any Redis client. 169 | 170 | 171 | ## Precautions 172 | - It is recommended to use a new lock instance each time you acquire a lock. 173 | - The same key and token must be used for locking and unlocking. 174 | - The default TTL is 5 seconds, and it is recommended to set it based on the duration of the task. 175 | - Automatic renewal is suitable for non-blocking tasks to avoid long blocking times. 176 | - It is recommended to use `defer unlock` in critical logic to prevent leaks. 177 | - It is recommended to log or monitor lock acquisition failures, retries, and other behaviors. 178 | - Fair locks require a unique requestId (UUID is recommended). 179 | - Read locks can be concurrent, while write locks are mutually exclusive to avoid read-write conflicts. 180 | - If any sub-lock in the interlock fails, the successfully acquired lock will be released. 181 | - There is a risk of deadlock if Redis is unavailable. 182 | 183 | ## License 184 | This library is licensed under the MIT. See the LICENSE file for details. 185 | 186 | -------------------------------------------------------------------------------- /mocks/lock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: lock.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | time "time" 11 | 12 | gomock "github.com/golang/mock/gomock" 13 | go_redislock "github.com/jefferyjob/go-redislock" 14 | ) 15 | 16 | // MockRedisInter is a mock of RedisInter interface. 17 | type MockRedisInter struct { 18 | ctrl *gomock.Controller 19 | recorder *MockRedisInterMockRecorder 20 | } 21 | 22 | // MockRedisInterMockRecorder is the mock recorder for MockRedisInter. 23 | type MockRedisInterMockRecorder struct { 24 | mock *MockRedisInter 25 | } 26 | 27 | // NewMockRedisInter creates a new mock instance. 28 | func NewMockRedisInter(ctrl *gomock.Controller) *MockRedisInter { 29 | mock := &MockRedisInter{ctrl: ctrl} 30 | mock.recorder = &MockRedisInterMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockRedisInter) EXPECT() *MockRedisInterMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // Eval mocks base method. 40 | func (m *MockRedisInter) Eval(ctx context.Context, script string, keys []string, args ...interface{}) go_redislock.RedisCmd { 41 | m.ctrl.T.Helper() 42 | varargs := []interface{}{ctx, script, keys} 43 | for _, a := range args { 44 | varargs = append(varargs, a) 45 | } 46 | ret := m.ctrl.Call(m, "Eval", varargs...) 47 | ret0, _ := ret[0].(go_redislock.RedisCmd) 48 | return ret0 49 | } 50 | 51 | // Eval indicates an expected call of Eval. 52 | func (mr *MockRedisInterMockRecorder) Eval(ctx, script, keys interface{}, args ...interface{}) *gomock.Call { 53 | mr.mock.ctrl.T.Helper() 54 | varargs := append([]interface{}{ctx, script, keys}, args...) 55 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eval", reflect.TypeOf((*MockRedisInter)(nil).Eval), varargs...) 56 | } 57 | 58 | // MockRedisCmd is a mock of RedisCmd interface. 59 | type MockRedisCmd struct { 60 | ctrl *gomock.Controller 61 | recorder *MockRedisCmdMockRecorder 62 | } 63 | 64 | // MockRedisCmdMockRecorder is the mock recorder for MockRedisCmd. 65 | type MockRedisCmdMockRecorder struct { 66 | mock *MockRedisCmd 67 | } 68 | 69 | // NewMockRedisCmd creates a new mock instance. 70 | func NewMockRedisCmd(ctrl *gomock.Controller) *MockRedisCmd { 71 | mock := &MockRedisCmd{ctrl: ctrl} 72 | mock.recorder = &MockRedisCmdMockRecorder{mock} 73 | return mock 74 | } 75 | 76 | // EXPECT returns an object that allows the caller to indicate expected use. 77 | func (m *MockRedisCmd) EXPECT() *MockRedisCmdMockRecorder { 78 | return m.recorder 79 | } 80 | 81 | // Int64 mocks base method. 82 | func (m *MockRedisCmd) Int64() (int64, error) { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "Int64") 85 | ret0, _ := ret[0].(int64) 86 | ret1, _ := ret[1].(error) 87 | return ret0, ret1 88 | } 89 | 90 | // Int64 indicates an expected call of Int64. 91 | func (mr *MockRedisCmdMockRecorder) Int64() *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Int64", reflect.TypeOf((*MockRedisCmd)(nil).Int64)) 94 | } 95 | 96 | // Result mocks base method. 97 | func (m *MockRedisCmd) Result() (interface{}, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "Result") 100 | ret0, _ := ret[0].(interface{}) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // Result indicates an expected call of Result. 106 | func (mr *MockRedisCmdMockRecorder) Result() *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Result", reflect.TypeOf((*MockRedisCmd)(nil).Result)) 109 | } 110 | 111 | // MockRedisLockInter is a mock of RedisLockInter interface. 112 | type MockRedisLockInter struct { 113 | ctrl *gomock.Controller 114 | recorder *MockRedisLockInterMockRecorder 115 | } 116 | 117 | // MockRedisLockInterMockRecorder is the mock recorder for MockRedisLockInter. 118 | type MockRedisLockInterMockRecorder struct { 119 | mock *MockRedisLockInter 120 | } 121 | 122 | // NewMockRedisLockInter creates a new mock instance. 123 | func NewMockRedisLockInter(ctrl *gomock.Controller) *MockRedisLockInter { 124 | mock := &MockRedisLockInter{ctrl: ctrl} 125 | mock.recorder = &MockRedisLockInterMockRecorder{mock} 126 | return mock 127 | } 128 | 129 | // EXPECT returns an object that allows the caller to indicate expected use. 130 | func (m *MockRedisLockInter) EXPECT() *MockRedisLockInterMockRecorder { 131 | return m.recorder 132 | } 133 | 134 | // FairLock mocks base method. 135 | func (m *MockRedisLockInter) FairLock(ctx context.Context, requestId string) error { 136 | m.ctrl.T.Helper() 137 | ret := m.ctrl.Call(m, "FairLock", ctx, requestId) 138 | ret0, _ := ret[0].(error) 139 | return ret0 140 | } 141 | 142 | // FairLock indicates an expected call of FairLock. 143 | func (mr *MockRedisLockInterMockRecorder) FairLock(ctx, requestId interface{}) *gomock.Call { 144 | mr.mock.ctrl.T.Helper() 145 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FairLock", reflect.TypeOf((*MockRedisLockInter)(nil).FairLock), ctx, requestId) 146 | } 147 | 148 | // FairRenew mocks base method. 149 | func (m *MockRedisLockInter) FairRenew(ctx context.Context, requestId string) error { 150 | m.ctrl.T.Helper() 151 | ret := m.ctrl.Call(m, "FairRenew", ctx, requestId) 152 | ret0, _ := ret[0].(error) 153 | return ret0 154 | } 155 | 156 | // FairRenew indicates an expected call of FairRenew. 157 | func (mr *MockRedisLockInterMockRecorder) FairRenew(ctx, requestId interface{}) *gomock.Call { 158 | mr.mock.ctrl.T.Helper() 159 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FairRenew", reflect.TypeOf((*MockRedisLockInter)(nil).FairRenew), ctx, requestId) 160 | } 161 | 162 | // FairUnLock mocks base method. 163 | func (m *MockRedisLockInter) FairUnLock(ctx context.Context, requestId string) error { 164 | m.ctrl.T.Helper() 165 | ret := m.ctrl.Call(m, "FairUnLock", ctx, requestId) 166 | ret0, _ := ret[0].(error) 167 | return ret0 168 | } 169 | 170 | // FairUnLock indicates an expected call of FairUnLock. 171 | func (mr *MockRedisLockInterMockRecorder) FairUnLock(ctx, requestId interface{}) *gomock.Call { 172 | mr.mock.ctrl.T.Helper() 173 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FairUnLock", reflect.TypeOf((*MockRedisLockInter)(nil).FairUnLock), ctx, requestId) 174 | } 175 | 176 | // Lock mocks base method. 177 | func (m *MockRedisLockInter) Lock(ctx context.Context) error { 178 | m.ctrl.T.Helper() 179 | ret := m.ctrl.Call(m, "Lock", ctx) 180 | ret0, _ := ret[0].(error) 181 | return ret0 182 | } 183 | 184 | // Lock indicates an expected call of Lock. 185 | func (mr *MockRedisLockInterMockRecorder) Lock(ctx interface{}) *gomock.Call { 186 | mr.mock.ctrl.T.Helper() 187 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockRedisLockInter)(nil).Lock), ctx) 188 | } 189 | 190 | // Renew mocks base method. 191 | func (m *MockRedisLockInter) Renew(ctx context.Context) error { 192 | m.ctrl.T.Helper() 193 | ret := m.ctrl.Call(m, "Renew", ctx) 194 | ret0, _ := ret[0].(error) 195 | return ret0 196 | } 197 | 198 | // Renew indicates an expected call of Renew. 199 | func (mr *MockRedisLockInterMockRecorder) Renew(ctx interface{}) *gomock.Call { 200 | mr.mock.ctrl.T.Helper() 201 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Renew", reflect.TypeOf((*MockRedisLockInter)(nil).Renew), ctx) 202 | } 203 | 204 | // SpinFairLock mocks base method. 205 | func (m *MockRedisLockInter) SpinFairLock(ctx context.Context, requestId string, timeout time.Duration) error { 206 | m.ctrl.T.Helper() 207 | ret := m.ctrl.Call(m, "SpinFairLock", ctx, requestId, timeout) 208 | ret0, _ := ret[0].(error) 209 | return ret0 210 | } 211 | 212 | // SpinFairLock indicates an expected call of SpinFairLock. 213 | func (mr *MockRedisLockInterMockRecorder) SpinFairLock(ctx, requestId, timeout interface{}) *gomock.Call { 214 | mr.mock.ctrl.T.Helper() 215 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpinFairLock", reflect.TypeOf((*MockRedisLockInter)(nil).SpinFairLock), ctx, requestId, timeout) 216 | } 217 | 218 | // SpinLock mocks base method. 219 | func (m *MockRedisLockInter) SpinLock(ctx context.Context, timeout time.Duration) error { 220 | m.ctrl.T.Helper() 221 | ret := m.ctrl.Call(m, "SpinLock", ctx, timeout) 222 | ret0, _ := ret[0].(error) 223 | return ret0 224 | } 225 | 226 | // SpinLock indicates an expected call of SpinLock. 227 | func (mr *MockRedisLockInterMockRecorder) SpinLock(ctx, timeout interface{}) *gomock.Call { 228 | mr.mock.ctrl.T.Helper() 229 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpinLock", reflect.TypeOf((*MockRedisLockInter)(nil).SpinLock), ctx, timeout) 230 | } 231 | 232 | // UnLock mocks base method. 233 | func (m *MockRedisLockInter) UnLock(ctx context.Context) error { 234 | m.ctrl.T.Helper() 235 | ret := m.ctrl.Call(m, "UnLock", ctx) 236 | ret0, _ := ret[0].(error) 237 | return ret0 238 | } 239 | 240 | // UnLock indicates an expected call of UnLock. 241 | func (mr *MockRedisLockInterMockRecorder) UnLock(ctx interface{}) *gomock.Call { 242 | mr.mock.ctrl.T.Helper() 243 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnLock", reflect.TypeOf((*MockRedisLockInter)(nil).UnLock), ctx) 244 | } 245 | -------------------------------------------------------------------------------- /adapter/go-zero/V1/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= 2 | github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= 3 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 6 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 7 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 8 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 9 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 10 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 11 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 12 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 18 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 19 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 20 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 21 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 22 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 23 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 24 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 25 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 26 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 28 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 29 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= 30 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= 31 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= 32 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= 33 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 34 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 35 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 36 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 37 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 38 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 39 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 40 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 41 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 42 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 43 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 44 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 45 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 46 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 47 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 48 | github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= 49 | github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= 50 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 51 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 55 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 56 | github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= 57 | github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg= 58 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 59 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 60 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 61 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 62 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 63 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 64 | github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= 65 | github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 66 | github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 67 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 68 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 69 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 72 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 73 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 74 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 75 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 76 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 77 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 78 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 79 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 80 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 81 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= 82 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 83 | github.com/zeromicro/go-zero v1.9.3 h1:dJ568uUoRJY0RUxo4aH4htSglbEUF60WiM1MZVkTK9A= 84 | github.com/zeromicro/go-zero v1.9.3/go.mod h1:JBAtfXQvErk+V7pxzcySR0mW6m2I4KPhNQZGASltDRQ= 85 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= 86 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 87 | go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= 88 | go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= 89 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= 90 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= 91 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE= 92 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM= 93 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= 94 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= 95 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8= 96 | go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= 97 | go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= 98 | go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= 99 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= 100 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 101 | go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= 102 | go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= 103 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= 104 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 105 | go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= 106 | go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= 107 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 108 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 109 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 110 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 111 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 112 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 113 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 114 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 116 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 117 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 118 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 119 | google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY= 120 | google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0= 121 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= 122 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 123 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 124 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 125 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 126 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 127 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 128 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 129 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 130 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= 131 | gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= 132 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 133 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 134 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 135 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 136 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 137 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= 138 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 139 | --------------------------------------------------------------------------------