分布式系统中,多个执行进程间如何进行协作来保证共享数据的一致性,是一个常见的话题。通常我们可以通过redis提供的setnx命令来实现一个分布式的锁。setnx命令格式如下:
setnx key value
- 只在键 key 不存在的情况下, 将键 key 的值设置为 value 。
- 若键 key 已经存在, 则 setnx 命令不做任何动作。
- 命令执行成功时返回1,执行失败返回0
但是一个分布式的锁还需要考虑以下几种情况:
- 某个执行进程在获取锁之后退出,如何保其他节点能正常获取锁
- A进程获取的锁已经过期,在更新过期时间过程中,锁被其他执行进程获取
下面我用golang给出一个完整的分布式锁方案:
func lock(lockName string) (int64, error) {
deadline := time.Now().Add(LockTimeout).Unix()
redisConn := appconfig.RedisMiscConn.Get()
defer redisConn.Close()
// step 1:通过setnx尝试获取锁
setnxResult, err := redis.Int(redisConn.Do("SETNX", lockName, deadline))
// step 1.1 setnx执行失败,直接返回失败
if err != nil {
return -1, err
}
// step 1.2 setnx返回1,说明获取锁成功了
if setnxResult == 1 {
return deadline, nil
}
// step 2 前面setnx返回不为1,表示当前锁被占用,需要进一步检查锁是否过期
getResult, err := redis.String(redisConn.Do("GET", lockName))
if err != nil {
return -1, err
}
// step 3 使用get获取lock的过期时间,没有过期直接返回获取锁失败
getOld, _ := strconv.ParseInt(getResult, 10, 64)
if getOld > time.Now().Unix() {
return -1, ErrLockTaken
}
// step 4 如果过期了,尝试用getset命令更新锁的过期时间
oldVal, err := redis.String(redisConn.Do("GETSET", lockName, strconv.FormatInt(deadline, 10)))
if err != nil {
return -1, err
}
getSetOld, _ := strconv.ParseInt(oldVal, 10, 64)
// step 5 如果仍旧是未过期,可能是有其他节点拿到lock,返回获取锁失败
if getSetOld > time.Now().Unix() {
return -1, ErrLockTaken
}
return deadline, nil
}