分布式锁是什么?为什么需要它
分布式锁就是在两个系统进程(可以不在同一台机器上)之间的加锁操作,使其实现原子操作
上面这样讲,可能还是不明白。下面从一个案例出发:
比如常见的电商系统,一般采用微服务架构。我们以其中的库存服务为例。在并发量不大的时候,一个库存服务就可以搞定。如下图,系统A代表一个库存服务:
但是在并发量上来之后,我们需要水平扩展库存微服务。
假设现在有两个库存服务,同一样商品有两个订单生成。但是这样一来会产生一个问题:假如某个时刻,redis里面的某个商品库存为1,此时两个请求同时到来,其中一个请求执行到上图的第3步,更新数据库的库存为0,但是第4步还没有执行。而另外一个请求执行到了第2步,发现库存还是1,就继续执行第3步。这样的结果,是导致卖出了2个商品,然而其实库存只有1个。很明显不对啊!这就是典型的库存超卖问题此时。如果两个请求在一个进程里,我们很容易想到解决方案:用语言层面的锁把2、3、4步锁住,让他们执行完之后,另一个线程才能进来执行第2步。
但是现在两个请求在两个进程里,怎么办呢?我们还是要保证两个进程加的锁是同一个锁,问题不就解决啦。这就是我们要讲的分布式锁
。在整个系统中提供一个全局,唯一的获取锁的"东西",然后每个系统在需要加锁时,都去问这个"东西"拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个"东西",可以是Redis,Zookeeper,也可以是数据库。这里我们介绍***用单节点redis实现分布式锁***
基于单节点Redis实现分布式锁
如上所说,两个库存服务以共用的一个redis作为统一锁的存放地址。
一个服务在redis中设置一个值表示加了锁,然后把这个key删除的时候代表释放锁
具体代码是这样的:
// 获取锁
// NX是指如果key不存在就成功,key存在返回false,PX可以指定过期时间
SET anyLock unique_value NX PX 30000
// 释放锁:通过执行一段lua脚本
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这种方式有几大要点:
- 一定要用SET key value NX PX milliseconds 命令
如果不用,先设置了值,再设置过期时间,这个不是原子性操作
,有可能在设置过期时间之前宕机,会造成死锁(key永久存在) value要具有唯一性
这个是为了在解锁的时候,需要验证value是和加锁的一致才删除key。
这是避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。
总结:
一个系统A在准备检查库存时,先用约定好的key去设置value。如果返回结果是false,说明有其他系统正在执行库存相关业务逻辑,也就是加了锁,这个时候,系统A就不能再去执行库存相关逻辑,以免造成库存超卖问题。系统A就应该定时去设置value,如果返回结果为true,这说明上一个系统处理完了库存相关业务逻辑(也就相当于释放了锁)。即执行"SET anyLock unique_value NX PX 30000"去获取了锁,执行自己的业务逻辑,同时开一个线程定时去执行上面的命令,防止在自己的业务逻辑执行完毕之前,因自动过期释放了锁,导致并发问题。处理完业务逻辑之后,获取value如果和自己unique value一致的话删除该KEY,释放锁
参考:
Go:分布式锁实现原理与最佳实践