分布式锁集成
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
📝 发现内容有误?点击此处直接编辑