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