lockx.go 1.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. package redisx
  2. import (
  3. "context"
  4. "errors"
  5. "time"
  6. "github.com/google/uuid"
  7. "github.com/redis/go-redis/v9"
  8. )
  9. // 分布式锁
  10. type RedisLock struct {
  11. key string
  12. token string
  13. ttl time.Duration
  14. }
  15. // TryLock 非阻塞尝试加锁
  16. func TryLock(ctx context.Context, key string, ttl time.Duration) (*RedisLock, bool, error) {
  17. token := uuid.NewString()
  18. ok, err := getClient().SetNX(ctx, key, token, ttl).Result()
  19. if err != nil {
  20. return nil, false, err
  21. }
  22. if !ok {
  23. return nil, false, nil
  24. }
  25. return &RedisLock{key: key, token: token, ttl: ttl}, true, nil
  26. }
  27. // Lock 阻塞加锁,可设置重试间隔和超时
  28. func Lock(ctx context.Context, key string, ttl, retryInterval, timeout time.Duration) (*RedisLock, error) {
  29. start := time.Now()
  30. for {
  31. lock, ok, err := TryLock(ctx, key, ttl)
  32. if err != nil {
  33. return nil, err
  34. }
  35. if ok {
  36. return lock, nil
  37. }
  38. if timeout > 0 && time.Since(start) >= timeout {
  39. return nil, errors.New("lock timeout")
  40. }
  41. select {
  42. case <-ctx.Done():
  43. return nil, ctx.Err()
  44. case <-time.After(retryInterval):
  45. }
  46. }
  47. }
  48. // Release 安全释放锁(Lua 脚本)
  49. func (l *RedisLock) Release(ctx context.Context) error {
  50. const luaScript = `
  51. if redis.call("get", KEYS[1]) == ARGV[1] then
  52. return redis.call("del", KEYS[1])
  53. else
  54. return 0
  55. end
  56. `
  57. res, err := redis.NewScript(luaScript).Run(ctx, getClient(), []string{l.key}, l.token).Int()
  58. if err != nil {
  59. return err
  60. }
  61. if res == 0 {
  62. return errors.New("unlock failed: not owner")
  63. }
  64. return nil
  65. }
  66. // SafeRelease 带日志保护版本
  67. func (l *RedisLock) SafeRelease(ctx context.Context) {
  68. _ = l.Release(ctx)
  69. }