且慢!先来实现分布式锁!
很遗憾,想要实现 TCC 业务侧,必然是需要分布式锁,有很多的方案都可以实现分布式锁,我们这里采用简单的 Redis 分布式锁。
本文将不再啰嗦!否则成了小学生,啥都要解说。这是一份有人味儿的 Code!
Option
package dlock
import "time"
type Config struct {
// 锁的默认有效期
Expiry time.Duration
// 获取锁最大尝试次数
Tries int
// 两次尝试之间的间隔
RetryDelay time.Duration
// Do 获取锁时的最大等待时间
AcquireTimeout time.Duration
// 解锁操作的最大执行时间
UnlockTimeout time.Duration
}
func DefaultConfig() Config {
return Config{
Expiry: 10 * time.Second,
Tries: 3,
RetryDelay: 100 * time.Millisecond,
AcquireTimeout: 800 * time.Millisecond,
UnlockTimeout: 2 * time.Second,
}
}
func normalizeConfig(config Config) Config {
defaults := DefaultConfig()
if config.Expiry <= 0 {
config.Expiry = defaults.Expiry
}
if config.Tries <= 0 {
config.Tries = defaults.Tries
}
if config.RetryDelay <= 0 {
config.RetryDelay = defaults.RetryDelay
}
if config.AcquireTimeout <= 0 {
config.AcquireTimeout = defaults.AcquireTimeout
}
if config.UnlockTimeout <= 0 {
config.UnlockTimeout = defaults.UnlockTimeout
}
return config
}
type mutexOptions struct {
expiry time.Duration
tries int
retryDelay time.Duration
acquireTimeout time.Duration
}
type Option func(*mutexOptions)
func WithExpiry(expiry time.Duration) Option {
return func(options *mutexOptions) {
if expiry > 0 {
options.expiry = expiry
}
}
}
func WithTries(tries int) Option {
return func(options *mutexOptions) {
if tries > 0 {
options.tries = tries
}
}
}
func WithRetryDelay(delay time.Duration) Option {
return func(options *mutexOptions) {
if delay > 0 {
options.retryDelay = delay
}
}
}
func WithAcquireTimeout(timeout time.Duration) Option {
return func(options *mutexOptions) {
if timeout > 0 {
options.acquireTimeout = timeout
}
}
}
Errors
package dlock
import "errors"
var (
// ErrNotAcquired 表示锁被其他实例占用,或者重试次数耗尽
ErrNotAcquired = errors.New("distributed lock not acquired")
// ErrUnlockFailed 表示锁没有成功释放
ErrUnlockFailed = errors.New("distributed lock unlock failed")
// ErrExtendFailed 表示锁续期失败
ErrExtendFailed = errors.New("distributed lock extend failed")
)
Locker Interface
package dlock
import (
"context"
"time"
)
// 来自十年人物的提示:
// Locker 可以作为全局
// Mutex 请操作时创建,可以在 struct 中封装一个
// Locker 工厂函数
type Locker interface {
// NewMutex 每次业务操作都应该创建新的 Mutex
NewMutex(key string, opts ...Option) Mutex
// Do 获取锁,执行业务并释放锁
Do(ctx context.Context, key string, fn func(context.Context) error, opts ...Option) error
}
type Mutex interface {
// Lock 按照配置可以重试多次获取锁
Lock(ctx context.Context) error
// TryLock 仅尝试一次,定时任务请使用这个
TryLock(ctx context.Context) error
// Unlock 解锁
Unlock(ctx context.Context) error
// Extend 锁续期
Extend(ctx context.Context) error
// 返回当当前锁预计截至时间
Until() time.Time
}
Redsync Impl
这部分是对工厂函数的实现,可以用自己喜欢的方式封装,这里选择基于 Redis 的分布式锁: https://github.com/go-redsync/redsync
Redsync provides a Redis-based distributed mutual exclusion lock implementation for Go as described in this post. A reference library (by antirez) for Ruby is available at github.com/antirez/redlock-rb.
package dlock
import (
"context"
"errors"
"fmt"
"time"
"github.com/go-redsync/redsync/v4"
redsyncRedis "github.com/go-redsync/redsync/v4/redis/goredis/v9"
goredis "github.com/redis/go-redis/v9"
)
type redisLocker struct {
rs *redsync.Redsync
config Config
}
// NewRedisLocker 创建锁鸡架,因为在这里想到了吃,所以叫做鸡架
func NewRedisLocker(client goredis.UniversalClient, config Config) Locker {
config = normalizeConfig(config)
pool := redsyncRedis.NewPool(client)
return &redisLocker{
rs: redsync.New(pool),
config: config,
}
}
// redisMutex 实现 Mutex interface
type redisMutex struct {
mutex *redsync.Mutex
}
var _ Mutex = (*redisMutex)(nil)
var _ Locker = (*redisLocker)(nil)
func (m *redisMutex) Lock(ctx context.Context) error {
err := m.mutex.LockContext(ctx)
if err == nil {
return nil
}
if errors.Is(err, redsync.ErrFailed) {
return errors.Join(ErrNotAcquired, err)
}
return err
}
func (m *redisMutex) TryLock(ctx context.Context) error {
err := m.mutex.TryLockContext(ctx)
if err == nil {
return nil
}
if errors.Is(err, redsync.ErrFailed) {
return errors.Join(ErrNotAcquired, err)
}
return err
}
func (m *redisMutex) Unlock(ctx context.Context) error {
ok, err := m.mutex.UnlockContext(ctx)
if err != nil {
return errors.Join(ErrUnlockFailed, err)
}
if !ok {
return ErrUnlockFailed
}
return nil
}
func (m *redisMutex) Extend(ctx context.Context) error {
ok, err := m.mutex.ExtendContext(ctx)
if err != nil {
return errors.Join(ErrExtendFailed, err)
}
if !ok {
return ErrExtendFailed
}
return nil
}
func (m *redisMutex) Until() time.Time {
return m.mutex.Until()
}
// NewMutex 创建锁
func (l *redisLocker) NewMutex(key string, opts ...Option) Mutex {
return l.newMutex(key, l.buildOptions(opts...))
}
// newMutex 内部方法
func (l *redisLocker) newMutex(
key string,
options mutexOptions,
) Mutex {
mutex := l.rs.NewMutex(
key,
redsync.WithExpiry(options.expiry),
redsync.WithTries(options.tries),
redsync.WithRetryDelay(options.retryDelay),
)
return &redisMutex{
mutex: mutex,
}
}
// buildOptions 注入函数选项配置
func (l *redisLocker) buildOptions(opts ...Option) mutexOptions {
options := mutexOptions{
expiry: l.config.Expiry,
tries: l.config.Tries,
retryDelay: l.config.RetryDelay,
acquireTimeout: l.config.AcquireTimeout,
}
for _, apply := range opts {
if apply != nil {
apply(&options)
}
}
return options
}
// Do 执行短任务时调用,避免繁琐的使用 Mutex
func (l *redisLocker) Do(ctx context.Context, key string, fn func(ctx context.Context) error, opts ...Option) (err error) {
if ctx == nil {
ctx = context.Background()
}
if fn == nil {
return errors.New("distributed lock callback function is nil")
}
options := l.buildOptions(opts...)
mutex := l.newMutex(key, options)
acquireCtx, acquireCancel := context.WithTimeout(ctx, options.acquireTimeout)
defer acquireCancel()
if err := mutex.Lock(acquireCtx); err != nil {
return fmt.Errorf("acquire distributed lock %q: %w", key, err)
}
defer func() {
// 不使用可能已经超时或取消的业务 ctx,所以这里使用 context.WithoutCancel(ctx) 重新继承
// 这样会刷掉父 ctx 的 deadline 和 cancel 状态
unlockCtx, unlockCancel := context.WithTimeout(context.WithoutCancel(ctx), l.config.UnlockTimeout)
defer unlockCancel()
if unlockErr := mutex.Unlock(unlockCtx); unlockErr != nil {
// 这里会把 fn(ctx) 返回的 err 合并
err = errors.Join(err,
fmt.Errorf("unlock distributed lock %q: %w", key, unlockErr))
}
}()
return fn(ctx)
}
评论区