=====================
本篇为个人学习知识点笔记,方便随时看看
不保证你有收获
=====================
简单来说就是分布环境下面不同实例之间抢一把锁
基本上都和网络有关
redis
实现一个分布式锁,就是利用setnx,确保可以排他的设置一个键值对
只有一个实例能够设置成功,其他的实例都会阻塞在那里
问题
-
如何加锁?
- 加锁设置的值怎么确定?
加锁的值应该具有唯一性,能够知道这个锁是谁加的,或者说是当前实例能判断这个锁是不是自己的
- 加锁的时候要不要设置过期时间?
加锁要设置过期时间,如果没有过期时间,前面的实例如果崩溃了,就没有办法再解锁了
type Client struct {
Client redis.Cmdable
}
type Lock struct {
client redis.Cmdable
key string
value string
}
var (
errFailToPreemptLock = errors.New("抢锁失败")
//go:embed lua/unlock.lua
luaUnlock string
)
func NewClientAble(clien redis.Cmdable) *Client {
return &Client{
Client: clien,
}
}
func (c *Client) TryLock(ctx context.Context, key string, expiration time.Duration) (*Lock, error) {
val := uuid.New().String()
ok, err := c.Client.SetNX(ctx, key, val, expiration).Result()
if err != nil {
return nil, err
}
if !ok {
//代表有人抢到
return nil, errFailToPreemptLock
}
return &Lock{
client: c.Client,
key: key,
value: val,
}, nil
}
- 如何解锁?
- 要注意当前的锁是不是自己的
- 我在读取这个锁的时候别人是不能操作这个锁的,原子性使用lua
func (l *Lock) Unlock(ctx context.Context) error {
// lua脚本不会,到时候学习一下
// 这里是防止你在检查是不是自己的锁的时候别人就把锁给修改了
// 所以当你检查的时候别人是不能操作的
res, err := l.client.Eval(ctx, luaUnlock, []string{l.key}, l.value).Int64()
if err != nil {
return err
}
if res != 1 {
return errors.New("解锁失败,锁不存在")
}
return nil
}
//rdb := redis.NewClient(&redis.Options{
// Addr: "",
// Password: "",
//})
手动续约和自动续约
就是你设置了过期时间,但是你的业务并不会按照你的时间设置,在执行任务的时候,过期了
续约还是需要使用lua脚本,在续约的时候还是要判断是不是自己的锁
func (l *Lock) Refresh(ctx context.Context) error {
res, err := l.client.Eval(ctx, luaRefresh, []string{l.key}, l.value, l.expirarion.Seconds()).Int64()
if err != nil {
return err
}
if res != 1 {
return errors.New("解锁失败,锁不存在")
}
return nil
}
客户端怎么续约?
间隔多久续约?
续约超时怎么办?
如果出现错误怎么办?
func ExampleLock_Refresh() {
var l *Lock
ch := make(chan struct{})
errch := make(chan struct{})
timeoutCh := make(chan struct{}, 1)
go func() {
timer := time.NewTicker(time.Second * 10)
var num int
for {
select {
case <-timer.C:
ctx, cancle := context.WithTimeout(context.Background(), time.Second*2)
err := l.Refresh(ctx)
cancle()
if err == context.DeadlineExceeded {
timeoutCh <- struct{}{}
continue
}
if err != nil {
errch <- struct{}{}
return
}
num = 0
case <-timeoutCh:
//超时重试,限制次数 和次数归零 变量?
if num > 10 {
errch <- struct{}{}
return
}
num++
ctx, cancle := context.WithTimeout(context.Background(), time.Second*2)
err := l.Refresh(ctx)
cancle()
if err == context.DeadlineExceeded {
timeoutCh <- struct{}{}
continue
}
if err != nil {
errch <- struct{}{}
return
}
case <-ch:
ctx, cancle := context.WithTimeout(context.Background(), time.Second*2)
l.Unlock(ctx)
cancle()
}
}
}()
// 在业务中间怎么处理错误,如果给一个chan来进行检测错误信号
// 如果业务没有循环都要检测
// 如果有循环还好操作
ch <- struct{}{}
}
redis自动续约
自动续约的问题和前面还是一样的
自动续约的可控性比较差
type Lock struct {
client redis.Cmdable
key string
value string
expirarion time.Duration
unlockChan chan struct{}
}
func (l *Lock) AutoRefresh(interval time.Duration, timeout time.Duration) error {
timeoutCh := make(chan struct{}, 1)
timer := time.NewTicker(interval)
for {
select {
case <-timer.C:
ctx, cancle := context.WithTimeout(context.Background(), timeout)
err := l.Refresh(ctx)
cancle()
if err == context.DeadlineExceeded {
timeoutCh <- struct{}{}
continue
}
if err != nil {
return err
}
case <-timeoutCh:
ctx, cancle := context.WithTimeout(context.Background(), timeout)
err := l.Refresh(ctx)
cancle()
if err == context.DeadlineExceeded {
timeoutCh <- struct{}{}
continue
}
if err != nil {
return err
}
case <-l.unlockChan:
return nil
}
}
}
加锁重试
加锁的时候可能会出现一些问题
- 如果超时,直接加锁
- 检查一下key对应的值是不是我们刚才超时加锁请求的值,
- 如果是直接返回,前面一次加锁成功
- 如果不是,直接返回加锁失败
一般都是在超时的时候进行再次尝试
type RetryStrategy interface {
// 第一个值为重试间隔
// 第二个值为要不要继续重试
Next() (time.Duration, bool)
}
type FixedIntervalRetryStragety struct {
Interval time.Duration
MaxCnt int
Cnt int
}
func (f *FixedIntervalRetryStragety) Next() (time.Duration, bool) {
if f.Cnt > f.MaxCnt {
return 0, false
}
return f.Interval, true
}
func NewClientAble(clien redis.Cmdable) *Client {
return &Client{
Client: clien,
}
}
func (c *Client) Lock(ctx context.Context,
key string, expiration time.Duration,
retry RetryStrategy,
timeout time.Duration) (*Lock, error) {
// ctx可以控制整个链路
val := uuid.New().String()
var timer *time.Timer
for {
lctx, cancel := context.WithTimeout(ctx, timeout)
res, err := c.Client.Eval(lctx, luaLock, []string{key}, val, expiration.Seconds()).Result()
cancel()
//不为bil并且不超时
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
if res == "OK" {
return &Lock{
client: c.Client,
key: key,
value: val,
expirarion: expiration,
}, nil
}
interval, ok := retry.Next()
if ok {
return nil, errors.New("redis lock:超出重试限制")
}
if timer == nil {
timer = time.NewTimer(interval)
} else {
timer.Reset(interval)
}
select {
case <-timer.C:
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
如何使用singlefilght优化
- 非常高并发
- 非常热点集中