为了保证我们线上服务的并发性和安全性,目前我们的服务一般抛弃了单体应用,采用的都是扩展性很强的分布式架构。
对于可变共享资源的访问,同一时刻,只能由一个线程或者进程去访问操作。这时候我们就需要做个标识,如果当前有线程或者进程在操作共享变量,我们就做个标记,标识当前资源正在被操作中, 其它的线程或者进程,就不能进行操作了。当前操作完成之后,删除标记,这样其他的线程或者进程,就能来申请共享变量的操作。通过上面的标记来保证同一时刻共享变量只能由一个线程或者进行持有。
这里来聊聊如何使用 Redis 实现分布式锁
Redis 中分布式锁一般会用 set key value px milliseconds nx 或者 SETNX+Lua 来实现。
因为 SETNX 命令,需要配合 EXPIRE 设置过期时间,Redis 中单命令的执行是原子性的,组合命令就需要使用 Lua 才能保证原子性了。
看下如何实现
使用 set key value px milliseconds nx 实现
因为这个命令同时能够设置键值和过期时间,同时Redis中的单命令都是原子性的,所以加锁的时候使用这个命令即可
func (r *Redis) TryLock(ctx context.Context, key, value string, expire time.Duration) (isGetLock bool, err error) {
// 使用 set nx
res, err := r.Do(ctx, "set", key, value, "px", expire.Milliseconds(), "nx").Result()
if err != nil {
return false, err
}
if res == "OK" {
return true, nil
}
return false, nil
}
如果使用 SETNX 命令,这个命令不能设置过期时间,需要配合 EXPIRE 命令来使用。
因为是用到了两个命令,这时候两个命令的组合使用是不能保障原子性的,在一些并发比较大的时候,需要配合使用 Lua 脚本来保证命令的原子性。
func tryLockScript() string {
script := `
local key = KEYS[1]
local value = ARGV[1]
local expireTime = ARGV[2]
local isSuccess = redis.call('SETNX', key, value)
if isSuccess == 1 then
redis.call('EXPIRE', key, expireTime)
return "OK"
end
return "unLock" `
return script
}
func (r *Redis) TryLock(ctx context.Context, key, value string, expire time.Duration) (isGetLock bool, err error) {
// 使用 Lua + SETNX
res, err := r.Eval(ctx, tryLockScript(), []string{key}, value, expire.Seconds()).Result()
if err != nil {
return false, err
}
if res == "OK" {
return true, nil
}
return false, nil
}
除了上面加锁两个命令的区别之外,在解锁的时候需要注意下不能误删除别的线程持有的锁
为什么会出现这种情况呢,这里来分析下
举个栗子
1、线程1获取了锁,锁的过期时间为1s;
2、线程1完成了业务操作,用时1.5s ,这时候线程1的锁已经被过期时间自动释放了,这把锁已经被别的线程获取了;
3、但是线程1不知道,接着去释放锁,这时候就会将别的线程的锁,错误的释放掉。