2025-03-25 17:00:09 +08:00
|
|
|
|
package distributedlock
|
2025-02-28 16:00:16 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
2025-03-24 16:37:43 +08:00
|
|
|
|
"context"
|
2025-03-07 16:16:26 +08:00
|
|
|
|
"errors"
|
2025-02-28 16:00:16 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2025-06-13 15:34:49 +08:00
|
|
|
|
constants "modelRT/distributedlock/constant"
|
2025-02-28 16:00:16 +08:00
|
|
|
|
luascript "modelRT/distributedlock/luascript"
|
2025-03-07 16:16:26 +08:00
|
|
|
|
"modelRT/logger"
|
2025-02-28 16:00:16 +08:00
|
|
|
|
|
2025-03-25 17:00:09 +08:00
|
|
|
|
uuid "github.com/gofrs/uuid"
|
2025-03-24 16:37:43 +08:00
|
|
|
|
"github.com/redis/go-redis/v9"
|
2025-02-28 16:00:16 +08:00
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
2025-04-18 15:17:51 +08:00
|
|
|
|
internalLockLeaseTime = uint64(30 * 1000)
|
2025-02-28 16:00:16 +08:00
|
|
|
|
unlockMessage = 0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2025-03-11 15:35:15 +08:00
|
|
|
|
// RedissionLockConfig define redission lock config
|
2025-02-28 16:00:16 +08:00
|
|
|
|
type RedissionLockConfig struct {
|
2025-03-11 15:35:15 +08:00
|
|
|
|
LockLeaseTime uint64
|
2025-03-12 16:24:28 +08:00
|
|
|
|
Token string
|
2025-02-28 16:00:16 +08:00
|
|
|
|
Prefix string
|
|
|
|
|
|
ChanPrefix string
|
2025-03-07 16:16:26 +08:00
|
|
|
|
TimeoutPrefix string
|
2025-02-28 16:00:16 +08:00
|
|
|
|
Key string
|
2025-03-12 16:24:28 +08:00
|
|
|
|
NeedRefresh bool
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type redissionLocker struct {
|
2025-04-07 16:49:06 +08:00
|
|
|
|
lockLeaseTime uint64
|
2025-04-18 14:02:03 +08:00
|
|
|
|
Token string
|
|
|
|
|
|
Key string
|
2025-04-07 16:49:06 +08:00
|
|
|
|
waitChanKey string
|
|
|
|
|
|
needRefresh bool
|
|
|
|
|
|
refreshExitChan chan struct{}
|
|
|
|
|
|
subExitChan chan struct{}
|
|
|
|
|
|
client *redis.Client
|
|
|
|
|
|
refreshOnce *sync.Once
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-24 16:37:43 +08:00
|
|
|
|
func (rl *redissionLocker) Lock(ctx context.Context, timeout ...time.Duration) error {
|
2025-04-07 16:49:06 +08:00
|
|
|
|
if rl.refreshExitChan == nil {
|
|
|
|
|
|
rl.refreshExitChan = make(chan struct{})
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
2025-06-13 15:34:49 +08:00
|
|
|
|
result := rl.tryLock(ctx).(*constants.RedisResult)
|
|
|
|
|
|
if result.Code == constants.UnknownInternalError {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Error(ctx, result.OutputResultMessage())
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return fmt.Errorf("get lock failed:%w", result)
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-13 15:34:49 +08:00
|
|
|
|
if (result.Code == constants.LockSuccess) && rl.needRefresh {
|
2025-04-07 16:49:06 +08:00
|
|
|
|
rl.refreshOnce.Do(func() {
|
2025-03-07 16:16:26 +08:00
|
|
|
|
// async refresh lock timeout unitl receive exit singal
|
2025-03-24 16:37:43 +08:00
|
|
|
|
go rl.refreshLockTimeout(ctx)
|
2025-02-28 16:00:16 +08:00
|
|
|
|
})
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return nil
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-07 16:16:26 +08:00
|
|
|
|
subMsg := make(chan struct{}, 1)
|
|
|
|
|
|
defer close(subMsg)
|
2025-03-24 16:37:43 +08:00
|
|
|
|
sub := rl.client.Subscribe(ctx, rl.waitChanKey)
|
2025-02-28 16:00:16 +08:00
|
|
|
|
defer sub.Close()
|
2025-06-06 16:41:52 +08:00
|
|
|
|
go rl.subscribeLock(ctx, sub, subMsg)
|
2025-02-28 16:00:16 +08:00
|
|
|
|
|
|
|
|
|
|
if len(timeout) > 0 && timeout[0] > 0 {
|
2025-03-07 16:16:26 +08:00
|
|
|
|
acquireTimer := time.NewTimer(timeout[0])
|
|
|
|
|
|
for {
|
2025-02-28 16:00:16 +08:00
|
|
|
|
select {
|
2025-03-07 16:16:26 +08:00
|
|
|
|
case _, ok := <-subMsg:
|
2025-02-28 16:00:16 +08:00
|
|
|
|
if !ok {
|
2025-03-07 16:16:26 +08:00
|
|
|
|
err := errors.New("failed to read the lock waiting for for the channel message")
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Error(ctx, "failed to read the lock waiting for for the channel message")
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return err
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-13 15:34:49 +08:00
|
|
|
|
resultErr := rl.tryLock(ctx).(*constants.RedisResult)
|
|
|
|
|
|
if (resultErr.Code == constants.LockFailure) || (resultErr.Code == constants.UnknownInternalError) {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, resultErr.OutputResultMessage())
|
2025-03-07 16:16:26 +08:00
|
|
|
|
continue
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-13 15:34:49 +08:00
|
|
|
|
if resultErr.Code == constants.LockSuccess {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, resultErr.OutputResultMessage())
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return nil
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
2025-03-07 16:16:26 +08:00
|
|
|
|
case <-acquireTimer.C:
|
|
|
|
|
|
err := errors.New("the waiting time for obtaining the lock operation has timed out")
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, "the waiting time for obtaining the lock operation has timed out")
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return err
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return fmt.Errorf("lock the redis lock failed:%w", result)
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 16:41:52 +08:00
|
|
|
|
func (rl *redissionLocker) subscribeLock(ctx context.Context, sub *redis.PubSub, subMsgChan chan struct{}) {
|
2025-04-07 16:49:06 +08:00
|
|
|
|
if sub == nil || subMsgChan == nil {
|
2025-02-28 16:00:16 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, "lock: enter sub routine", zap.String("token", rl.Token))
|
2025-03-07 16:16:26 +08:00
|
|
|
|
|
2025-04-03 17:22:40 +08:00
|
|
|
|
for {
|
2025-02-28 16:00:16 +08:00
|
|
|
|
select {
|
2025-04-07 16:49:06 +08:00
|
|
|
|
case <-rl.subExitChan:
|
|
|
|
|
|
close(subMsgChan)
|
2025-03-12 16:24:28 +08:00
|
|
|
|
return
|
2025-04-07 16:49:06 +08:00
|
|
|
|
case <-sub.Channel():
|
|
|
|
|
|
// 这里只会收到真正的数据消息
|
|
|
|
|
|
subMsgChan <- struct{}{}
|
|
|
|
|
|
default:
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-07 16:16:26 +08:00
|
|
|
|
/*
|
|
|
|
|
|
KEYS[1]:锁的键名(key),通常是锁的唯一标识。
|
|
|
|
|
|
ARGV[1]:锁的过期时间(lockLeaseTime),单位为秒。
|
|
|
|
|
|
ARGV[2]:当前客户端的唯一标识(token),用于区分不同的客户端。
|
|
|
|
|
|
*/
|
2025-03-24 16:37:43 +08:00
|
|
|
|
func (rl *redissionLocker) refreshLockTimeout(ctx context.Context) {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, "lock refresh by key and token", zap.String("token", rl.Token), zap.String("key", rl.Key))
|
2025-03-07 16:16:26 +08:00
|
|
|
|
|
2025-04-18 15:17:51 +08:00
|
|
|
|
lockTime := time.Duration(rl.lockLeaseTime/3) * time.Millisecond
|
2025-02-28 16:00:16 +08:00
|
|
|
|
timer := time.NewTimer(lockTime)
|
|
|
|
|
|
defer timer.Stop()
|
2025-03-07 16:16:26 +08:00
|
|
|
|
|
2025-02-28 16:00:16 +08:00
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case <-timer.C:
|
2025-03-07 16:16:26 +08:00
|
|
|
|
// extend key lease time
|
2025-04-18 14:02:03 +08:00
|
|
|
|
res := rl.client.Eval(ctx, luascript.RefreshLockScript, []string{rl.Key}, rl.lockLeaseTime, rl.Token)
|
2025-02-28 16:00:16 +08:00
|
|
|
|
val, err := res.Int()
|
2025-03-07 16:16:26 +08:00
|
|
|
|
if err != redis.Nil && err != nil {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, "lock refresh failed", "token", rl.Token, "key", rl.Key, "error", err)
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
2025-03-07 16:16:26 +08:00
|
|
|
|
|
2025-06-13 15:34:49 +08:00
|
|
|
|
if constants.RedisCode(val) == constants.RefreshLockFailure {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Error(ctx, "lock refreash failed,can not find the lock by key and token", "token", rl.Token, "key", rl.Key)
|
2025-03-07 16:16:26 +08:00
|
|
|
|
break
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-13 15:34:49 +08:00
|
|
|
|
if constants.RedisCode(val) == constants.RefreshLockSuccess {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, "lock refresh success by key and token", "token", rl.Token, "key", rl.Key)
|
2025-03-07 16:16:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
timer.Reset(lockTime)
|
2025-04-07 16:49:06 +08:00
|
|
|
|
case <-rl.refreshExitChan:
|
2025-03-12 16:24:28 +08:00
|
|
|
|
return
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (rl *redissionLocker) cancelRefreshLockTime() {
|
2025-04-07 16:49:06 +08:00
|
|
|
|
if rl.refreshExitChan != nil {
|
|
|
|
|
|
close(rl.refreshExitChan)
|
|
|
|
|
|
rl.refreshOnce = &sync.Once{}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 16:41:52 +08:00
|
|
|
|
func (rl *redissionLocker) closeSub(ctx context.Context, sub *redis.PubSub, noticeChan chan struct{}) {
|
2025-04-07 16:49:06 +08:00
|
|
|
|
if sub != nil {
|
2025-04-11 16:36:54 +08:00
|
|
|
|
err := sub.Close()
|
|
|
|
|
|
if err != nil {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Error(ctx, "close sub failed", "token", rl.Token, "key", rl.Key, "error", err)
|
2025-04-11 16:36:54 +08:00
|
|
|
|
}
|
2025-04-07 16:49:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if noticeChan != nil {
|
|
|
|
|
|
close(noticeChan)
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-07 16:16:26 +08:00
|
|
|
|
/*
|
|
|
|
|
|
KEYS[1]:锁的键名(key),通常是锁的唯一标识。
|
|
|
|
|
|
ARGV[1]:锁的过期时间(lockLeaseTime),单位为秒。
|
|
|
|
|
|
ARGV[2]:当前客户端的唯一标识(token),用于区分不同的客户端。
|
|
|
|
|
|
*/
|
2025-03-24 16:37:43 +08:00
|
|
|
|
func (rl *redissionLocker) tryLock(ctx context.Context) error {
|
2025-06-13 15:34:49 +08:00
|
|
|
|
lockType := constants.LockType
|
2025-04-18 14:02:03 +08:00
|
|
|
|
res := rl.client.Eval(ctx, luascript.LockScript, []string{rl.Key}, rl.lockLeaseTime, rl.Token)
|
2025-03-07 16:16:26 +08:00
|
|
|
|
val, err := res.Int()
|
2025-02-28 16:00:16 +08:00
|
|
|
|
if err != redis.Nil && err != nil {
|
2025-06-13 15:34:49 +08:00
|
|
|
|
return constants.NewRedisResult(constants.UnknownInternalError, lockType, err.Error())
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
2025-06-13 15:34:49 +08:00
|
|
|
|
return constants.NewRedisResult(constants.RedisCode(val), lockType, "")
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-07 16:16:26 +08:00
|
|
|
|
/*
|
|
|
|
|
|
KEYS[1]:锁的键名(key),通常是锁的唯一标识。
|
|
|
|
|
|
KEYS[2]:锁的释放通知频道(chankey),用于通知其他客户端锁已释放。
|
|
|
|
|
|
ARGV[1]:解锁消息(unlockMessage),用于通知其他客户端锁已释放。
|
|
|
|
|
|
ARGV[2]:当前客户端的唯一标识(token),用于区分不同的客户端。
|
|
|
|
|
|
*/
|
2025-03-24 16:37:43 +08:00
|
|
|
|
func (rl *redissionLocker) UnLock(ctx context.Context) error {
|
2025-04-18 14:02:03 +08:00
|
|
|
|
res := rl.client.Eval(ctx, luascript.UnLockScript, []string{rl.Key, rl.waitChanKey}, unlockMessage, rl.Token)
|
2025-03-07 16:16:26 +08:00
|
|
|
|
val, err := res.Int()
|
2025-02-28 16:00:16 +08:00
|
|
|
|
if err != redis.Nil && err != nil {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, "unlock lock failed", zap.String("token", rl.Token), zap.String("key", rl.Key), zap.Error(err))
|
2025-06-13 15:34:49 +08:00
|
|
|
|
return fmt.Errorf("unlock lock failed:%w", constants.NewRedisResult(constants.UnknownInternalError, constants.UnLockType, err.Error()))
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
2025-03-07 16:16:26 +08:00
|
|
|
|
|
2025-06-13 15:34:49 +08:00
|
|
|
|
if constants.RedisCode(val) == constants.UnLockSuccess {
|
2025-03-07 16:16:26 +08:00
|
|
|
|
if rl.needRefresh {
|
|
|
|
|
|
rl.cancelRefreshLockTime()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, "unlock lock success", zap.String("token", rl.Token), zap.String("key", rl.Key))
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return nil
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
2025-03-07 16:16:26 +08:00
|
|
|
|
|
2025-06-13 15:34:49 +08:00
|
|
|
|
if constants.RedisCode(val) == constants.UnLocakFailureWithLockOccupancy {
|
2025-06-06 16:41:52 +08:00
|
|
|
|
logger.Info(ctx, "unlock lock failed", zap.String("token", rl.Token), zap.String("key", rl.Key))
|
2025-06-13 15:34:49 +08:00
|
|
|
|
return fmt.Errorf("unlock lock failed:%w", constants.NewRedisResult(constants.UnLocakFailureWithLockOccupancy, constants.UnLockType, ""))
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
2025-03-07 16:16:26 +08:00
|
|
|
|
return nil
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-25 17:00:09 +08:00
|
|
|
|
// TODO 优化 panic
|
2025-02-28 16:00:16 +08:00
|
|
|
|
func GetLocker(client *redis.Client, ops *RedissionLockConfig) *redissionLocker {
|
2025-03-12 16:24:28 +08:00
|
|
|
|
if ops.Token == "" {
|
2025-03-25 17:00:09 +08:00
|
|
|
|
token, err := uuid.NewV4()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
panic(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
ops.Token = token.String()
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(ops.Prefix) <= 0 {
|
|
|
|
|
|
ops.Prefix = "redission-lock"
|
|
|
|
|
|
}
|
2025-03-07 16:16:26 +08:00
|
|
|
|
|
2025-02-28 16:00:16 +08:00
|
|
|
|
if len(ops.ChanPrefix) <= 0 {
|
|
|
|
|
|
ops.ChanPrefix = "redission-lock-channel"
|
|
|
|
|
|
}
|
2025-03-07 16:16:26 +08:00
|
|
|
|
|
2025-02-28 16:00:16 +08:00
|
|
|
|
if ops.LockLeaseTime == 0 {
|
2025-03-12 16:24:28 +08:00
|
|
|
|
ops.LockLeaseTime = internalLockLeaseTime
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
r := &redissionLocker{
|
2025-04-18 14:02:03 +08:00
|
|
|
|
Token: ops.Token,
|
|
|
|
|
|
Key: strings.Join([]string{ops.Prefix, ops.Key}, ":"),
|
2025-04-07 16:49:06 +08:00
|
|
|
|
waitChanKey: strings.Join([]string{ops.ChanPrefix, ops.Key, "wait"}, ":"),
|
|
|
|
|
|
needRefresh: ops.NeedRefresh,
|
|
|
|
|
|
client: client,
|
|
|
|
|
|
refreshExitChan: make(chan struct{}),
|
2025-02-28 16:00:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
return r
|
|
|
|
|
|
}
|