且慢!先来实现分布式锁!

2026/7/31 17:17:17Henry7 阅读0 点赞0 评论

很遗憾,想要实现 TCC 业务侧,必然是需要分布式锁,有很多的方案都可以实现分布式锁,我们这里采用简单的 Redis 分布式锁。

本文将不再啰嗦!否则成了小学生,啥都要解说。这是一份有人味儿的 Code!

Option

GO
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

GO
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

GO
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.

GO
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)
}

评论区