之前梳理了一篇用PHP实现Redis分布式锁的文章:《关于秒杀场景用Redis来实现分布式锁解决库存超卖现象 》
现在使用Go来完善Redis分布式锁的逻辑,话不多说,流程都用注释说明了
// 基于redis的分布式锁
package lock
import (
"context"
"errors"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"sync"
"time"
)
type (
RedisLock struct {
lockChan chan struct{}
rwLock sync.RWMutex
lockKey string // 锁的key
lockValue string // 锁的value
client *redis.Client
}
InvokeMethod func() error
)
func GetRedisLock(client *redis.Client, lockKey string) *RedisLock {
return &RedisLock{
lockChan: make(chan struct{}, 1),
lockKey: lockKey,
client: client,
}
}
func (lock *RedisLock) TryLock(ctx context.Context, method InvokeMethod, timeout time.Duration, interval time.Duration) error {
if interval == 0 {
interval = 100 * time.Millisecond
}
lock.rwLock.Lock()
defer lock.rwLock.Unlock()
var err error
go func() {
for {
// 如果这个lockValue此时有值,说明上一个人在使用中,那么就一直等待,等待的间隔默认是100 * time.Millisecond(100毫秒)
if lock.lockValue == "" {
// 此时没有值,说明redis已经将这个值释放了,这时创建一个锁的value,放到redis里
lock.lockValue = uuid.New().String()
// 这里利用redis的SetNX方法,为lock设置一个超时时间,一定时间后,锁会自己清掉数据
// 这样做的目的是,比如进程挂了,没有执行UnLock,但利用redis可以把锁给清掉,防止死锁
hasSet, setErr := lock.client.SetNX(ctx, lock.lockKey, lock.lockValue, timeout).Result()
// 锁发生错误,也要退出
if setErr != nil {
err = setErr
lock.lockChan <- struct{}{}
return
}
// 当值设置成功后,通知主协程,执行相应方法method,同时整个goroutine退出,return
if hasSet {
lock.lockChan <- struct{}{}
return
}
}
// 这里是单个锁执行最小的时间间隔,间隔5毫秒相当于每秒最多处理200个任务
// 如果太快了,整个goroutine轮巡次数太快,可能上一个任务还没处理完
// 可以自己看情况设定,其实这个时间间隔差不多刚刚好
time.Sleep(interval)
}
}()
select {
case <-lock.lockChan:
if err != nil {
return err
}
return method()
case <-time.After(timeout):
return errors.New("lock timeout")
}
}
func (lock *RedisLock) UnLock(ctx context.Context) (bool, error) {
if lock.lockValue == "" {
return false, errors.New("锁已经被释放")
}
// 执行语句释放redis,Lua语句内容:redis里的lock对应的key的value删除
script := "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
// EVAL是执行上面的script的Lua语句
result, err := lock.client.Do(ctx, "EVAL", script, 1, lock.lockKey, lock.lockValue).Bool()
if err != nil {
return false, err
}
if !result {
return false, errors.New("出现分布式并发释放锁错误")
}
lock.lockValue = ""
return true, nil
}