分布式锁线程之间资源共享天然就可以操作同一把锁所以多线程之间使用锁来确保并发安全是比较容易的。而进程与进程之间的并发安全问题就比较复杂进程之间相互独立需要使用进程间通信机制来让不同的进程看到同一把锁。在分布式结构中需要让不同主机上的进程之间并发安全其实就相当于让进程与进程之间的并发安全只不过使用了“网络通信”这种特殊的通信方式来让不同主机上的进程看到同一把锁。在分布式锁中锁不是一个变量他是一个公共网络服务。分布式锁的应用场景如下图。买票的逻辑{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算法直接搞多台锁服务应用服务需要对它们轮流加锁和解锁超过总的锁服务的数目的一半才算加锁/解锁成功。
Redis——分布式锁
发布时间:2026/7/6 3:45:18
分布式锁线程之间资源共享天然就可以操作同一把锁所以多线程之间使用锁来确保并发安全是比较容易的。而进程与进程之间的并发安全问题就比较复杂进程之间相互独立需要使用进程间通信机制来让不同的进程看到同一把锁。在分布式结构中需要让不同主机上的进程之间并发安全其实就相当于让进程与进程之间的并发安全只不过使用了“网络通信”这种特殊的通信方式来让不同主机上的进程看到同一把锁。在分布式锁中锁不是一个变量他是一个公共网络服务。分布式锁的应用场景如下图。买票的逻辑{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算法直接搞多台锁服务应用服务需要对它们轮流加锁和解锁超过总的锁服务的数目的一半才算加锁/解锁成功。