之前梳理了一篇用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
}