Redis——分布式锁
分布式锁
线程之间资源共享,天然就可以操作同一把锁,所以多线程之间使用锁来确保并发安全是比较容易的。而进程与进程之间的并发安全问题就比较复杂,进程之间相互独立,需要使用进程间通信机制来让不同的进程看到同一把锁。
在分布式结构中,需要让不同主机上的进程之间并发安全,其实就相当于让进程与进程之间的并发安全,只不过使用了“网络通信”这种特殊的通信方式来让不同主机上的进程看到同一把锁。在分布式锁中,锁不是一个变量,他是一个公共网络服务。
分布式锁的应用场景
如下图。买票的逻辑:{if(查看票数是否为0)票数--;},这里分为两步,所以会有并发安全问题,即:多个买票服务器并发访问数据库可能会出现“超卖”。
有的同学可能会想:这好办,Mysql一类的数据库有事务,可以把多个操作变成原子的,可惜的是,这并不是一个好主意,在Mysql中就算是“串行化”也不是完全串行的,两个事务同时读一条数据总是被允许的,假设只剩一张票,两个事务同时读它发现有票,然后都进入下一步:修改票数,就算他们的修改是串行的,也会都修改一次,还是“超卖”。
这种情况下我们只能使用分布式锁来解决,充当锁的公共服务可以是redis也可以是我们自己写的服务,总之,它要可以充当锁,所以架构就变成下面这样(以redis为例):
我们完全可以通过命令setnx和del进行加锁和解锁。由于redis单线程,执行这些命令都是原子的。
- 加锁的时候就是setnx:如果执行成功就表示加锁成功;如果已经存在就执行失败,表示加锁失败。
- 解锁的时候就是del删除相应的键值对
考虑这样一种情况,某个买票服务加锁成功后就宕机了,这样锁就会一直存在,其他买票服务也无法进行加锁。
- 为了避免这种情况,在加锁成功的同时,也要给这个键值对设置一个过期时间,让它超时自动删除。set ex nx 命令就支持同时设置键值对和过期时间。但要注意,不可以把他们分成两个命令,redis的事务是不安全的,假设键值对设置失败单过期时间设置成功也是不会回滚的,所以要避免这样做。
上述方案仍然存在一个重要问题:当设置了 key 的过期时间(比如 10s)后,仍有可能在任务尚未执行完时,key 就已过期,导致锁提前失效。
那么,把过期时间设得足够长(比如 30s)是否能解决呢?显然,设置多长合适是个无止境的问题,即便设再长,也无法完全保证不会提前失效。而且,设得太长的话,万一对应服务器宕机,其他服务器也无法及时获取到锁。因此,相较于固定一个较长的过期时间,不如动态调整时间更为合适。
所谓Watch Dog(看门狗),本质上是加锁服务器上的一个独立线程,通过该线程对锁过期时间进行“续约”。注意,这个线程是业务服务器上的,而非 Redis 服务器。
举个具体例子:
初始设置过期时间为 10s,同时设定看门狗线程每隔 3s 检测一次。当 3s 到达时,看门狗会判断当前任务是否完成:
- 如果任务已完成,则直接通过 Lua 脚本释放锁(删除 key);
- 如果任务未完成,则将过期时间重新设置为 10s(即“续约”)。
这样一来,既不必担心锁提前失效;另一方面,如果该服务器宕机,看门狗线程也随之消失,无人续约,key 自然能快速过期,从而让其他服务器得以获取锁。
再考虑这样一种情况,一个买票服务加锁,另一个买票服务直接给把锁del了。比如,服务器1 写入一个"001": 1这样的键值对,服务器2 完全可以把"001"给删除掉。当然,服务器2 不会进行这样的“恶意删除”操作,不过不能保证因为一些 bug 导致服务器2 把锁误删除。
- 为了解决上述问题,我们可以引入一个校验 id。比如,可以把设置的键值对的值,不再是简单地设为
1,而是设成服务器的编号,形如"001": "服务器1"。这样就可以在删除 key(解锁)的时候,先校验当前删除 key 的服务器是否是当初加锁的服务器,如果是,才能真正删除;不是,则不能删除。逻辑用伪代码描述如下:String key = [要加锁的资源 id]; String serverId = [服务器的编号]; // 加锁,设置过期时间为 10s redis.set(key, serverId, "NX", "EX", "10s"); // 执行各种业务逻辑,比如修改数据库数据 doSomeThing(); // 解锁,删除 key。但是删除前要检验下 serverId 是否匹配 if (redis.get(key) == serverId) { redis.del(key); }但是很明显,解锁逻辑是两步操作
get和del,这样做并非原子的。但是我们可以使用redis支持的lua脚本执行这段逻辑,redis会先把这段程序执行完才去执行其他命令,保证了原子性。
在考虑最后一种情况,如果redis服务也就是锁服务本身挂掉了怎么办。
首先我们同学们可能会想到用主从架构,但是主从架构是有延迟的,没同步之前主节点就挂了这也是可能得。redis作者给出了一种redlok算法:直接搞多台锁服务,应用服务需要对它们轮流加锁和解锁,超过总的锁服务的数目的一半才算加锁/解锁成功。
