利用redis实现golang的分布式锁

go使用redis锁

  • 基于Redis的SetNX方法,创建并使用redis锁
  • 曾经在一便文档中,有一句话,引发的我的思考:如果公司内已有可以使用的ZooKeeper、etcd或者Redis集群,那么就尽量在不引入新的技术栈的情况下满足业务需求。
  • 各种各样的分布式锁封装得实在太多,眼花缭乱,但如果项目使用到了redis,何不直接使用redis锁呢?

redis锁的封装

package redisLock

import (
	"errors"
	"github.com/go-redis/redis"
	"github.com/google/uuid"
	"sync"
	"time"
)

type (
	RedisLock struct {
		lockChan  chan struct{}
		rwLock    sync.RWMutex
		lockKey   string //锁的key
		lockValue string //锁的value
		client    *redis.Client
	}
	InvokeMethod func(args ...interface{}) (interface{}, error)
)

func NewRedisLock(client *redis.Client) *RedisLock {
	return &RedisLock{
		lockChan: make(chan struct{}, 1),
		lockKey:  uuid.NewString(),         
		client:   client,
	}
}

func (this *RedisLock) TryLock(method InvokeMethod, timeout time.Duration) (interface{}, error) {
	this.rwLock.Lock()
	defer this.rwLock.Unlock()
	var err error
	go func() {
		for {
			//如果这个lockValue此时有值,说明上一个人在使用中,那么就一直等待,等待的间隔是5* time.Millisecond
			if this.lockValue == "" {
				//此时没有值,说明redis已经将这个值释放了,这时创建一个锁的value,放到reids里
				this.lockValue = uuid.New().String()
				//这里利用redis的SetNX方法,为lock设置一个超时时间,一定时间后,锁会自己清掉数据
				//这样做的目的是,比如进程挂了,没有执行UnLock,但利用redis可以把锁给清掉,防止死锁
				hasSet, setErr := this.client.SetNX(this.lockKey, this.lockValue, timeout).Result()
				//锁发生错误,也要退出
				if setErr != nil {
					err = setErr
					this.lockChan <- struct{}{}
					return
				}
				//当值设置成功后,通知主协程,执行相应方法method,同时整个goroutine退出,return
				if hasSet {
					this.lockChan <- struct{}{}
					return
				}
			}
			//这里是单个锁执行最小的时间间隔,间隔5毫秒相当于每秒最多处理200个任务
			//如果太快了,整个goroutine轮巡次数太快,可能上一个任务还没处理完
			//可以自己看情况设定,其实这个时间间隔差不多刚刚好
			time.Sleep(5 * time.Millisecond)
		}
	}()

	select {
	case <-this.lockChan:
		if err != nil {
			return nil, err
		}
		return method()
	case <-time.After(timeout):
		return nil, errors.New("lock timeout")
	}
}

func (r *RedisLock) UnLock() (bool, error) {
	if r.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 := r.client.Do("EVAL", script, 1, r.lockKey, r.lockValue).Bool()
	if err != nil {
		return false, err
	}
	if !result {
		return false, errors.New("出现分布式并发释放锁错误")
	}
	r.lockValue = ""
	return true, nil
}

redis锁的使用

package main

import (
	"fmt"
	"github.com/go-redis/redis"
	"redisLock/redisLock"
	"sync"
	"time"
)

var cli *redis.Client
var testKey = "counter"

func main() {
	//step1:连接redis
	cli = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "",
		DB:       0,
	})
	//step2:在redis中,为要测试的teskey写入一个默认值
	cli.Set(testKey, 1000, 0)

	test()
}

func test() {
	//step1:初始化一个redis锁
	lock := redisLock.NewRedisLock(cli)
	//step2:模拟多个协程同时作动作
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			result, _ := counterAdd(lock)
			fmt.Println("result=", result)
		}()
	}
	wg.Wait()
}

func counterAdd(lock *redisLock.RedisLock) (int64, error) {
	//这个方法是要加锁执行的方法
	withLockDo := func(args ...interface{}) (interface{}, error) {
		cmd1 := cli.Get(testKey)
		counterValue, _ := cmd1.Int64()
		fmt.Println("计数累加前, counterValue=", counterValue)
		counterValue++

		//这里可以设置一个等待,模拟非常耗时的读或写
		//time.Sleep(time.Second)

		cmd2 := cli.Set(testKey, counterValue, 0)
		if _, err := cmd2.Result(); err != nil {
			println("set value error!")
		}
		fmt.Println("计数累加后, counterValue=", counterValue)
		return counterValue, nil
	}

	defer lock.UnLock()
	if result, err := lock.TryLock(withLockDo, 3*time.Second); err != nil {
		fmt.Printf("reidis lock,error:%s", err.Error())
		return 0, err
	} else {
		return result.(int64), nil
	}
}

参考

  • https://segmentfault.com/a/1190000041120021
  • https://chai2010.cn/advanced-go-programming-book/ch6-cloud/ch6-02-lock.html

你可能感兴趣的:(游戏服务器,golang,游戏开发,go,redis,分布式锁,go-redis)