2026-07-13 08:59:13 +00:00
|
|
|
package redislock
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
2026-07-15 15:23:59 +00:00
|
|
|
"crypto/rand"
|
|
|
|
|
"encoding/hex"
|
2026-07-13 08:59:13 +00:00
|
|
|
"fmt"
|
2026-07-15 15:23:59 +00:00
|
|
|
"strings"
|
|
|
|
|
"sync"
|
2026-07-13 08:59:13 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
|
|
|
|
)
|
|
|
|
|
|
2026-07-15 15:23:59 +00:00
|
|
|
const (
|
|
|
|
|
renewScript = `
|
|
|
|
|
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
|
|
|
return redis.call('PEXPIRE', KEYS[1], ARGV[2])
|
|
|
|
|
end
|
|
|
|
|
return 0
|
|
|
|
|
`
|
|
|
|
|
releaseScript = `
|
|
|
|
|
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
|
|
|
return redis.call('DEL', KEYS[1])
|
|
|
|
|
end
|
|
|
|
|
return 0
|
|
|
|
|
`
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Client is the subset of Redis used by Lock.
|
|
|
|
|
type Client interface {
|
|
|
|
|
SetnxExCtx(ctx context.Context, key, value string, seconds int) (bool, error)
|
|
|
|
|
EvalCtx(ctx context.Context, script string, keys []string, args ...any) (any, error)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lock is a small Redis lease. Each acquisition gets a unique token so an old
|
|
|
|
|
// owner cannot renew or release a successor's lease.
|
2026-07-13 08:59:13 +00:00
|
|
|
type Lock struct {
|
2026-07-15 15:23:59 +00:00
|
|
|
client Client
|
2026-07-13 08:59:13 +00:00
|
|
|
key string
|
2026-07-15 15:23:59 +00:00
|
|
|
prefix string
|
2026-07-13 08:59:13 +00:00
|
|
|
ttl time.Duration
|
2026-07-15 15:23:59 +00:00
|
|
|
|
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
token string
|
2026-07-13 08:59:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-15 15:23:59 +00:00
|
|
|
func New(conf redis.RedisConf, key, ownerPrefix string, ttl time.Duration) *Lock {
|
|
|
|
|
return NewWithClient(redis.MustNewRedis(conf), key, ownerPrefix, ttl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewWithClient(client Client, key, ownerPrefix string, ttl time.Duration) *Lock {
|
|
|
|
|
return &Lock{client: client, key: key, prefix: strings.TrimSpace(ownerPrefix), ttl: ttl}
|
2026-07-13 08:59:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *Lock) Acquire(ctx context.Context) (bool, error) {
|
2026-07-15 15:23:59 +00:00
|
|
|
if err := l.validate(); err != nil {
|
|
|
|
|
return false, err
|
2026-07-13 08:59:13 +00:00
|
|
|
}
|
2026-07-15 15:23:59 +00:00
|
|
|
token, err := newToken(l.prefix)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
acquired, err := l.client.SetnxExCtx(ctx, l.key, token, durationSeconds(l.ttl))
|
|
|
|
|
if err != nil || !acquired {
|
|
|
|
|
return acquired, err
|
|
|
|
|
}
|
|
|
|
|
l.mu.Lock()
|
|
|
|
|
l.token = token
|
|
|
|
|
l.mu.Unlock()
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Renew extends the lease only while this acquisition still owns it.
|
|
|
|
|
func (l *Lock) Renew(ctx context.Context) (bool, error) {
|
|
|
|
|
if err := l.validate(); err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
token := l.currentToken()
|
|
|
|
|
if token == "" {
|
|
|
|
|
return false, nil
|
2026-07-13 08:59:13 +00:00
|
|
|
}
|
2026-07-15 15:23:59 +00:00
|
|
|
ttlMilliseconds := l.ttl.Milliseconds()
|
|
|
|
|
if ttlMilliseconds < 1 {
|
|
|
|
|
ttlMilliseconds = 1
|
|
|
|
|
}
|
|
|
|
|
result, err := l.client.EvalCtx(ctx, renewScript, []string{l.key}, token, ttlMilliseconds)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
return resultIsOne(result), nil
|
2026-07-13 08:59:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *Lock) Release(ctx context.Context) error {
|
2026-07-15 15:23:59 +00:00
|
|
|
if err := l.validate(); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
token := l.currentToken()
|
|
|
|
|
if token == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
result, err := l.client.EvalCtx(ctx, releaseScript, []string{l.key}, token)
|
|
|
|
|
if err == nil && resultIsOne(result) {
|
|
|
|
|
l.mu.Lock()
|
|
|
|
|
if l.token == token {
|
|
|
|
|
l.token = ""
|
|
|
|
|
}
|
|
|
|
|
l.mu.Unlock()
|
2026-07-13 08:59:13 +00:00
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-07-15 15:23:59 +00:00
|
|
|
|
|
|
|
|
func (l *Lock) validate() error {
|
|
|
|
|
if l == nil || l.client == nil || l.key == "" || l.prefix == "" || l.ttl <= 0 {
|
|
|
|
|
return fmt.Errorf("invalid redis lock")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *Lock) currentToken() string {
|
|
|
|
|
l.mu.RLock()
|
|
|
|
|
defer l.mu.RUnlock()
|
|
|
|
|
return l.token
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newToken(prefix string) (string, error) {
|
|
|
|
|
random := make([]byte, 16)
|
|
|
|
|
if _, err := rand.Read(random); err != nil {
|
|
|
|
|
return "", fmt.Errorf("generate redis lock token: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return prefix + ":" + hex.EncodeToString(random), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func durationSeconds(ttl time.Duration) int {
|
|
|
|
|
seconds := int(ttl / time.Second)
|
|
|
|
|
if ttl%time.Second != 0 {
|
|
|
|
|
seconds++
|
|
|
|
|
}
|
|
|
|
|
if seconds < 1 {
|
|
|
|
|
seconds = 1
|
|
|
|
|
}
|
|
|
|
|
return seconds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func resultIsOne(result any) bool {
|
|
|
|
|
switch value := result.(type) {
|
|
|
|
|
case int64:
|
|
|
|
|
return value == 1
|
|
|
|
|
case int:
|
|
|
|
|
return value == 1
|
|
|
|
|
case uint64:
|
|
|
|
|
return value == 1
|
|
|
|
|
default:
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|