文章目录
思维导图
为什么需要分布式锁?
保证分布式系统并发请求或不同服务实例操作共享资源的安全性,确保在同一时间内,仅有一个进程能够修改共享资源,例如数据库记录或文件,主要用于解决分布式环境中的数据一致性和并发控制问题。 应用场景:用户下单,库存扣减,余额扣减。我们的场景:防止用户信息多写,积分扣减(扣减时几个请求都满足积分余额可扣的情况,只允许一个去扣),防止重复设置定时任务。
预扣费用时用分布式锁是为了确保在扣减权益余额时的并发安全性。在处理多个请求同时尝试冻结同一个用户的某个特定权益时,分布式锁可以确保只有一个请求能够执行扣减余额的操作,从而避免出现并发问题,如扣减多次、余额不足等错误情况。
- 使用分布式锁可能会对性能产生一定的影响,但这是为了确保数据的一致性和正确性所必需的;如果操作是操作是幂等的(即使多次执行也会产生相同的结果),可能不需要分布式锁;
- 如果共享资源是MySQL数据,可以用MySQL 的乐观锁 SELECT FOR UPDATE 来实现分布式锁的。
go语言分布式锁的实现
Redis
https://github.com/zeromicro/go-zero/blob/master/core/stores/redis/redislock.go
go-zero里已经实现了redislock,但没有续约机制
自己的实现
- 为什么需要自己封装? 用的框架里没有,去参考了go-zero,里面的分布式锁有基础的加锁和解锁,缺少续约
// 需要实现的能力
// 1.排他性、原子性
// 2.主动释放/自动释放
// 3.可重入
// 4.可续约
package r_lc
import (
"context"
"errors"
"github.com/go-redis/redis/v8"
"time"
"github.com/google/uuid"
)
// 实现以下4个方法
type Lock interface {
// TryLock 尝试锁
TryLock(ctx context.Context) (lcNum int, res bool)
// LockWait 尝试锁并等待
LockWait(ctx context.Context, wait time.Duration) (lcNum int, res bool)
// Renew 续约
Renew(ctx context.Context)
// Unlock 解锁
Unlock(ctx context.Context) (leftLcNum int, err error)
}
// RLc 基于redis的分布式锁
type RLc struct {
rdb *redis.Client
// key 锁标识
key string
// lcTag 唯一标识,防止串锁
lcTag string
// expiresIn 过期时间
expiresIn time.Duration
// releaseCh 锁释放信号 (看门狗)
releaseCh chan struct{
}
// RetryInterval LockWait重试锁的间隔。默认100ms
RetryInterval time.Duration
// RenewInterval 续约锁间隔,默认为expiresIn/2
RenewInterval time.Duration
// MaxRenewDur 自动续约最长时间。默认1小时,当expiresIn大于1小时,为expiresIn
MaxRenewDur time.Duration
}
type RlcOpt func(lc *RLc)
const (
retryIntervalDefault = 100 * time.Millisecond
maxRenewDurDefault = time.Hour
)
// LUA脚本
var (
// tryLockLua
// return 0. 加锁失败
// return >0. 加锁成功,当前锁的数量
tryLockLua = `
local key = KEYS[1]
local val = ARGV[1]
local expiresIn = ARGV[2]
-- 锁不存在,加锁
if redis.call('EXISTS', key) == 0 then
redis.call('HINCRBY', key, val, 1)
redis.call('PEXPIRE', key, expiresIn)
return 1
end
-- 锁存在,判断持有锁,增加加锁次数 (可重入)
if redis.call('HEXISTS', key, val) == 1 then
return redis.call('HINCRBY', key, val, 1)
end
-- 锁被其他进程占用
return 0
`
// unlockLua
// return > 0. 剩余待解锁次数
// return = 0. 解锁成功
// return = -1. 锁不存在 | 未持有锁
unlockLua = `
local key = KEYS[1]
local val = ARGV[1]
-- 锁不存在或未持有锁
if redis.call('HEXISTS', key, val) == 0 then
return -1
end
-- 按次数解锁
local count = redis.call('HINCRBY', key, val, -1)
if count <= 0 then
-- 全部解锁
redis.call("DEL",key)
return 0
end
-- 剩余待解锁次数
return count
`
// renewLua
// return 0. 续约失败
// return 1. 续约成功
renewLua = `
local key = KEYS[1]
local val = ARGV[1]
local expiresIn = ARGV[2]
-- 锁不存在或未持有锁
if redis.call('HEXISTS', key, val) == 0 then
return 0
end
-- 设置过期时间
return redis.call('PEXPIRE', key, expiresIn)
`
)
var (
ErrLostKey = errors.New("lost key") // 锁不存在或被其他进程占用
)
func NewRLc(rdb *redis.Client, key string, expiresIn time.Duration, opts ...RlcOpt) *RLc {
lc := &RLc{
rdb: rdb,
key: key,
lcTag: uuid.New().String(),
expiresIn: expiresIn,
releaseCh: make(chan struct{
}),
}
for _, opt := range opts {
opt(lc)
}
if lc.RetryInterval == 0 {
lc.RetryInterval = retryIntervalDefault
}
if lc.RenewInterval == 0 {
lc.RenewInterval