使用gin封装一个web脚手架(九):分布式锁(上)

1,378 阅读2分钟

在某些场景下,我们需要对某个请求和任务做加锁处理,保证它只被执行一次,当任务处理完毕后才可以处理下一个任务。如调用支付、发送邮件、增减库存等。

接下来我们将用redis实现一个分布式锁

在component/lock创建lock.go文件

package lock

import (
	"context"
	uuid "github.com/satori/go.uuid"
	"myGin/redis"
	"time"
)

type lock struct {
	key        string
	expiration time.Duration
	requestId  string
}

func NewLock(key string, expiration time.Duration) *lock {

	requestId := uuid.NewV4().String()

	return &lock{key: key, expiration: expiration, requestId: requestId}
}

实例化一个锁,将锁的标识、过期时间和请求id,请求id主要用于识别当前请求,按照分布式锁的标准,谁申请的锁,只能让申请锁的请求解锁。

接下来是获取锁

// Get 获取锁
func (lk *lock) Get() bool {

	cxt, cancel := context.WithTimeout(context.Background(), 3*time.Second)

	defer cancel()

	ok, err := redis.Client().SetNX(cxt, lk.key, lk.requestId, lk.expiration).Result()

	if err != nil {

		return false
	}

	return ok
}

redis的SetNX可以写入一个字符串并设置过期时间,如果该key存在则会返回false。

再是释放锁

// Release 释放锁
func (lk *lock) Release() bool {

	cxt, cancel := context.WithTimeout(context.Background(), 3*time.Second)

	defer cancel()

	realRequestId, err := redis.Client().Get(cxt, lk.key).Result()

	if err != nil {

		return false
	}

	if realRequestId == lk.requestId {

		redis.Client().Del(cxt, lk.key)

		return true

	}

	return false
}        

上面的代码乍一看没什么问题,先是取出锁的请求id,再和申请时的请求id做对比,如果两个请求id一直,就执行删除,解决了谁申请就谁解锁的问题。

想象这样一个场景,请求A发起了一个,申请到了一个锁,处理完任务后要释放这个锁,判断完请求id后准备删除redis中的数据,在这个时候redis的过期时间到了(你说巧不巧),然后正好这个时间请求b进来了申请到了锁,请求a正好把请求b的锁数据给删除了。

因为上方释放锁的代码不是一个操作,也就是不是原子操作,这里需要用lua脚本来处理一下,修改一下代码。

// Release 释放锁
func (lk *lock) Release() error {

	cxt, cancel := context.WithTimeout(context.Background(), 3*time.Second)

	defer cancel()

	const luaScript = `
	if redis.call('get', KEYS[1])==ARGV[1] then
		return redis.call('del', KEYS[1])
	else
		return 0
	end
	`

	script := goredis.NewScript(luaScript)

	_, err := script.Run(cxt, redis.Client(), []string{lk.key}, lk.requestId).Result()

	return err

}

完整代码

package lock

import (
	"context"
	goredis "github.com/go-redis/redis/v8"
	uuid "github.com/satori/go.uuid"
	"myGin/redis"
	"time"
)

type lock struct {
	key        string
	expiration time.Duration
	requestId  string
}

func NewLock(key string, expiration time.Duration) *lock {

	requestId := uuid.NewV4().String()

	return &lock{key: key, expiration: expiration, requestId: requestId}
}

// Get 获取锁
func (lk *lock) Get() bool {

	cxt, cancel := context.WithTimeout(context.Background(), 3*time.Second)

	defer cancel()

	ok, err := redis.Client().SetNX(cxt, lk.key, lk.requestId, lk.expiration).Result()

	if err != nil {

		return false
	}

	return ok
}

// Release 释放锁
func (lk *lock) Release() error {

	cxt, cancel := context.WithTimeout(context.Background(), 3*time.Second)

	defer cancel()

	const luaScript = `
	if redis.call('get', KEYS[1])==ARGV[1] then
		return redis.call('del', KEYS[1])
	else
		return 0
	end
	`

	script := goredis.NewScript(luaScript)

	_, err := script.Run(cxt, redis.Client(), []string{lk.key}, lk.requestId).Result()

	return err

}

源代码:github.com/PeterYangs/…