mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-10-11 20:43:44 +08:00
增加集群部署支持,修复定时任务分组添加后选项不显示
This commit is contained in:
59
server/internal/library/hgrds/lock/consts.go
Normal file
59
server/internal/library/hgrds/lock/consts.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Package lock
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package lock
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// 加锁脚本
|
||||
lockScript = `
|
||||
local token = redis.call('get', KEYS[1])
|
||||
if token then
|
||||
return 0
|
||||
else
|
||||
local setResult = redis.call('setex', KEYS[1], ARGV[2], ARGV[1])
|
||||
return setResult
|
||||
end
|
||||
`
|
||||
|
||||
// 续约脚本
|
||||
renewalScript = `
|
||||
if redis.call('get',KEYS[1])==ARGV[2] then
|
||||
return redis.call('expire',KEYS[1],ARGV[1])
|
||||
end
|
||||
return 0
|
||||
`
|
||||
|
||||
// 解锁脚本
|
||||
unlockScript = `
|
||||
if redis.call("get",KEYS[1]) == ARGV[1] then
|
||||
return redis.call("del",KEYS[1])
|
||||
else
|
||||
return 2
|
||||
end
|
||||
`
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultTTL 锁默认过期时间
|
||||
DefaultTTL = time.Second * 10
|
||||
// DefaultTryLockInterval 默认重试获取锁间隔时间
|
||||
DefaultTryLockInterval = time.Millisecond * 100
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrLockFailed 加锁失败
|
||||
ErrLockFailed = gerror.New("lock failed")
|
||||
// ErrTimeout 加锁超时
|
||||
ErrTimeout = gerror.New("timeout")
|
||||
// ErrNotCaller 锁持有者不是当前实例
|
||||
ErrNotCaller = gerror.New("lock not held by the caller")
|
||||
// ErrNotExist 锁不存在
|
||||
ErrNotExist = gerror.New("lock does not exist")
|
||||
)
|
155
server/internal/library/hgrds/lock/lock.go
Normal file
155
server/internal/library/hgrds/lock/lock.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// Package lock
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package lock
|
||||
|
||||
// 分布式锁
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config 锁配置
|
||||
type Config struct {
|
||||
ttl time.Duration // 过期时间
|
||||
tryLockInterval time.Duration // 重新获取锁间隔
|
||||
}
|
||||
|
||||
// Lock 一把锁 不可重复使用
|
||||
type Lock struct {
|
||||
resource string // 锁定的资源
|
||||
randomValue string // 随机值
|
||||
watchDog chan struct{} // 看门狗
|
||||
ttl time.Duration // 过期时间
|
||||
tryLockInterval time.Duration // 重新获取锁间隔
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewConfig 初始化一个锁配置
|
||||
func NewConfig(ttl, tryLockInterval time.Duration) *Config {
|
||||
return &Config{
|
||||
ttl: ttl,
|
||||
tryLockInterval: tryLockInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// Mutex 根据配置创建一把锁
|
||||
func (lc *Config) Mutex(resource string) *Lock {
|
||||
return &Lock{
|
||||
resource: resource,
|
||||
randomValue: guid.S(),
|
||||
watchDog: make(chan struct{}),
|
||||
ttl: lc.ttl,
|
||||
tryLockInterval: lc.tryLockInterval,
|
||||
}
|
||||
}
|
||||
|
||||
// Lock 阻塞加锁
|
||||
func (l *Lock) Lock(ctx context.Context) error {
|
||||
// 尝试加锁
|
||||
err := l.TryLock(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !gerror.Is(err, ErrLockFailed) {
|
||||
return err
|
||||
}
|
||||
// 加锁失败,不断尝试
|
||||
ticker := time.NewTicker(l.tryLockInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// 超时
|
||||
return ErrTimeout
|
||||
case <-ticker.C:
|
||||
// 重新尝试加锁
|
||||
err = l.TryLock(ctx)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !gerror.Is(err, ErrLockFailed) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TryLock 尝试加锁,如果失败立即返回错误,而不会阻塞等待锁
|
||||
func (l *Lock) TryLock(ctx context.Context) error {
|
||||
var args = []interface{}{l.randomValue, l.ttl.Seconds()}
|
||||
eval, err := g.Redis().GroupScript().Eval(ctx, lockScript, 1, []string{l.resource}, args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if eval.String() != "OK" {
|
||||
return ErrLockFailed
|
||||
}
|
||||
|
||||
go l.startWatchDog()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock 解锁
|
||||
func (l *Lock) Unlock(ctx context.Context) error {
|
||||
var args []interface{}
|
||||
args = append(args, l.randomValue)
|
||||
eval, err := g.Redis().GroupScript().Eval(ctx, unlockScript, 1, []string{l.resource}, args)
|
||||
|
||||
if eval.Int() == 2 {
|
||||
return ErrNotCaller
|
||||
}
|
||||
|
||||
if eval.Int() == 0 {
|
||||
return ErrNotExist
|
||||
}
|
||||
|
||||
close(l.watchDog)
|
||||
return err
|
||||
}
|
||||
|
||||
// startWatchDog 看门狗
|
||||
func (l *Lock) startWatchDog() {
|
||||
resetTTLInterval := l.ttl / 3
|
||||
ticker := time.NewTicker(resetTTLInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
l.wg.Add(1)
|
||||
defer l.wg.Wait()
|
||||
|
||||
conn := g.Redis()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// 延长锁的过期时间
|
||||
ctx, cancel := context.WithTimeout(context.Background(), resetTTLInterval)
|
||||
var args = []interface{}{l.ttl.Seconds(), l.randomValue}
|
||||
eval, err := conn.GroupScript().Eval(ctx, renewalScript, 1, []string{l.resource}, args)
|
||||
cancel()
|
||||
|
||||
// 异常或锁已经不存在则不再续期
|
||||
if err != nil || eval.Int() < 1 {
|
||||
return
|
||||
}
|
||||
case <-l.watchDog:
|
||||
// 已经解锁
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// defaultMutex 一个默认配置锁
|
||||
var defaultMutex = NewConfig(DefaultTTL, DefaultTryLockInterval)
|
||||
|
||||
// Mutex 获取默认锁
|
||||
func Mutex(resource string) *Lock {
|
||||
return defaultMutex.Mutex(resource)
|
||||
}
|
118
server/internal/library/hgrds/lock/lock_test.go
Normal file
118
server/internal/library/hgrds/lock/lock_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// Package lock_test
|
||||
// @Link https://github.com/bufanyun/hotgo
|
||||
// @Copyright Copyright (c) 2023 HotGo CLI
|
||||
// @Author Ms <133814250@qq.com>
|
||||
// @License https://github.com/bufanyun/hotgo/blob/master/LICENSE
|
||||
package lock_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"hotgo/internal/library/hgrds/lock"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDefaultLock(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
l := lock.Mutex("test")
|
||||
err := l.TryLock(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
time.Sleep(lock.DefaultTTL)
|
||||
err = l.Unlock(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
l := lock.Mutex("test")
|
||||
err := l.TryLock(context.Background())
|
||||
if err != nil && !gerror.Is(err, lock.ErrLockFailed) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestNewLock(t *testing.T) {
|
||||
locker := lock.NewConfig(time.Second*30, time.Second)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
l := locker.Mutex("test")
|
||||
err := l.TryLock(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
time.Sleep(lock.DefaultTTL)
|
||||
err = l.Unlock(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
l := lock.Mutex("test")
|
||||
err := l.TryLock(context.Background())
|
||||
if err != nil && !gerror.Is(err, lock.ErrLockFailed) {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestNewLock2(t *testing.T) {
|
||||
locker := lock.NewConfig(time.Second*30, time.Second)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
count := 0
|
||||
times := 1000
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < times; i++ {
|
||||
l := locker.Mutex("test")
|
||||
err := l.Lock(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
count++
|
||||
err = l.Unlock(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < times; i++ {
|
||||
l := lock.Mutex("test")
|
||||
err := l.Lock(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
count++
|
||||
err = l.Unlock(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
if count != times*2 {
|
||||
t.Errorf("count = %d", count)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user