全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-21 8 分钟 ✍️ juanwangdev

分布式锁集成

GORM 运行在单机进程中,多实例部署时需借助 Redis 等外部组件实现分布式锁,确保跨进程的并发安全。

Redis 分布式锁基础

SET NX EX 机制

使用 Redis 的 SET key value NX EX timeout 命令实现原子加锁:

Go
type RedisLock struct {
    client  *redis.Client
    key     string
    value   string
    timeout time.Duration
}

func NewRedisLock(client *redis.Client, key string, timeout time.Duration) *RedisLock {
    return &RedisLock{
        client:  client,
        key:     key,
        value:   uuid.New().String(), // 唯一标识,防止误删他人锁
        timeout: timeout,
    }
}

func (l *RedisLock) Lock(ctx context.Context) error {
    // SET key value NX EX timeout
    ok, err := l.client.SetNX(ctx, l.key, l.value, l.timeout).Result()
    if err != nil {
        return err
    }
    if !ok {
        return errors.New("获取锁失败,锁已被占用")
    }
    return nil
}

func (l *RedisLock) Unlock(ctx context.Context) error {
    // Lua 脚本保证原子性:仅当值匹配时才删除
    script := `
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
    `
    _, err := l.client.Eval(ctx, script, []string{l.key}, l.value).Result()
    return err
}

释放锁必须使用 Lua 脚本校验值,防止锁过期后被其他客户端获取,原客户端误删新锁。

GORM 回调集成

BeforeSave 钩子加锁

在 GORM 模型钩子中获取分布式锁,确保更新前已持有锁:

Go
type Account struct {
    ID      uint    `gorm:"primaryKey"`
    Balance float64
}

func (a *Account) BeforeUpdate(tx *gorm.DB) error {
    lockKey := fmt.Sprintf("lock:account:%d", a.ID)
    lock := NewRedisLock(redisClient, lockKey, 5*time.Second)

    ctx := context.Background()
    if err := lock.Lock(ctx); err != nil {
        return fmt.Errorf("获取分布式锁失败: %w", err)
    }

    // 将锁存入 context,供 AfterUpdate 释放
    tx.Statement.Context = context.WithValue(tx.Statement.Context, "account_lock", lock)
    return nil
}

func (a *Account) AfterUpdate(tx *gorm.DB) error {
    if lock, ok := tx.Statement.Context.Value("account_lock").(*RedisLock); ok {
        ctx := context.Background()
        if err := lock.Unlock(ctx); err != nil {
            log.Printf("释放分布式锁失败: %v", err)
        }
    }
    return nil
}

钩子中加锁需注意:钩子运行在事务内,但分布式锁在事务外,锁持有时间可能长于事务。

业务层封装推荐

更推荐在业务层显式控制锁,而非放在 GORM 钩子中:

Go
func TransferWithDistributedLock(db *gorm.DB, fromID, toID uint, amount float64) error {
    lockKeys := []string{
        fmt.Sprintf("lock:account:%d", fromID),
        fmt.Sprintf("lock:account:%d", toID),
    }
    // 按 key 排序,避免死锁
    sort.Strings(lockKeys)

    var locks []*RedisLock
    for _, key := range lockKeys {
        lock := NewRedisLock(redisClient, key, 10*time.Second)
        if err := lock.Lock(context.Background()); err != nil {
            // 获取失败,回滚已获取的锁
            for _, l := range locks {
                l.Unlock(context.Background())
            }
            return err
        }
        locks = append(locks, lock)
    }

    // 全部获取成功,执行数据库操作
    defer func() {
        for _, l := range locks {
            l.Unlock(context.Background())
        }
    }()

    return db.Transaction(func(tx *gorm.DB) error {
        var from, to Account
        if err := tx.First(&from, fromID).Error; err != nil {
            return err
        }
        if from.Balance < amount {
            return errors.New("余额不足")
        }

        if err := tx.First(&to, toID).Error; err != nil {
            return err
        }

        from.Balance -= amount
        to.Balance += amount

        tx.Save(&from)
        return tx.Save(&to).Error
    })
}

锁续期机制

业务执行时间不确定时,需防止锁过期被自动释放:

Go
func (l *RedisLock) WatchDog(ctx context.Context) {
    go func() {
        ticker := time.NewTicker(l.timeout / 3) // 每 1/3 超时时间续期
        defer ticker.Stop()

        for {
            select {
            case <-ticker.C:
                // 仅当锁仍存在且值匹配时才续期
                script := `
                    if redis.call("get", KEYS[1]) == ARGV[1] then
                        return redis.call("expire", KEYS[1], ARGV[2])
                    else
                        return 0
                    end
                `
                l.client.Eval(ctx, script, []string{l.key}, l.value, int(l.timeout/time.Second))
            case <-ctx.Done():
                return
            }
        }
    }()
}

注意事项

  • 锁粒度:分布式锁应尽量细化,避免锁住整个系统成为性能瓶颈
  • 超时时间:必须设置过期时间,防止客户端崩溃后锁无法释放
  • 可重入性:标准 Redis 锁不支持可重入,同一客户端重复获取会失败
  • Redlock 算法:单节点 Redis 存在脑裂风险,高可靠场景应使用 Redlock 多节点方案
  • 锁与事务解耦:分布式锁在 Redis 中,数据库事务在 MySQL 中,两者无法保证原子性

要点总结

  • 使用 Redis SET NX EX + Lua 脚本实现安全的分布式锁
  • 推荐在业务层显式控制锁,避免在 GORM 钩子中隐式加锁
  • 多锁获取必须按固定顺序,防止死锁
  • 必须实现看门狗续期机制,防止长业务导致锁过期
  • 分布式锁与数据库事务无法保证跨系统原子性

存放路径:D:\git2\jwdev\articles\GORM\专家\高级并发与锁机制\分布式锁集成.md

📝 发现内容有误?点击此处直接编辑

← 上一篇 乐观锁实现
下一篇 → 悲观锁实现
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库