什么是分布式锁?
分布式锁是一种机制,用于在分布式系统中控制对共享资源的访问。它确保在同一时间只有一个进程可以访问特定资源,从而避免数据不一致和竞争条件。分布式锁通常用于以下场景:
- 限制对数据库的写入操作,防止出现脏读。
- 控制对文件的并发访问,防止文件损坏。
- 在微服务架构中协调服务之间的操作。
- 分布式任务场景中只有一台机器执行。
Redis 分布式锁的实现原理
Redis 提供了简单而高效的分布式锁实现,主要依赖于其原子性操作和过期时间设置。基本的实现思路如下:
-
加锁:客户端尝试使用
SETNX
命令设置一个锁的键(例如lock:resource
)。如果该键不存在,则设置成功,返回 1,表示获得锁;如果该键已存在,则返回 0,表示锁已被其他客户端占用。 -
设置过期时间:为了防止死锁情况,通常在加锁时会同时设置一个过期时间。这样,如果持锁的客户端崩溃或未能释放锁,锁会在过期后自动释放。
-
释放锁:客户端在完成操作后,需要删除锁的键。释放锁时要确保只有持锁的客户端才能释放锁,这通常通过比较锁的值(如 UUID)来实现。
加锁、释放锁情况分析(接口定义)
加锁情况分析
加锁的结果有两种状态,成功或者失败。成功只有一种情况,就是与redis连接正常、并且能够成功执行加锁命令。而失败就有两种情况了,情况一是客户端与 redis 连接就失败了,更不用说成功加锁了;情况二就是与 redis 连接正常,但执行加锁命令返回结果失败。
对于加锁失败的两种情况,客户端可能会有不同的处理方案,因此在设计加锁的返回值时,应考虑到失败的这两种情况,让客户端做针对性处理。
比如在分布式任务的场景当中,为了避免服务器单点故障,通常由多个服务器定时执行一个任务,通过分布式锁保证同一时刻只有一个机器获取到锁执行任务。如果是由于连接异常而导致的失败,客户端可能会采取一定的重试策略,因为网络波动导致的问题重试可能会解决掉;而如果是连接正常、获取锁失败,表明有其他的客户端获取锁成功了,这种情况,就不应该重试。
释放锁情况分析
释放锁的情况稍微和加锁有些不一样,因为无论是客户端与 redis 连接异常,还是释放锁失败,都是没有成功释放锁。所以,只需返回 true 或者 false 即可。
总结
基于以上两种情况,接口定义如下:
import java.net.SocketException;
/**
* 分布式锁
*/
public interface IDistributeLock {
/**
* 尝试加锁,在网络正常的情况下:加锁成功返回true,否则返回false
* @param releaseTimeOut 超时锁释放的时间,单位秒
* @return
* @throws SocketException 网络异常时,会抛出异常
*/
boolean tryLock(int releaseTimeOut) throws SocketException;
/**
* 尝试释放锁:释放锁成功返回true,否则返回false
* @param lockName 锁名称
* @return
*/
boolean tryRelease(String lockName);
}
Redis 分布式锁实现推演
普通的加锁、释放锁
加锁
SETNX
是 Redis 中的一个命令,用于在键不存在的情况下设置一个键的值。其全名是 “SET if Not eXists”,意思是“如果不存在则设置”。这个命令在实现分布式锁时非常有用,因为它能够确保只有一个客户端可以成功设置某个键,从而获得锁。
- 如果键成功设置(即键之前不存在),返回
1
。 - 如果键已经存在,返回
0
。
127.0.0.1:6379> setnx lock 1
(integer) 1
127.0.0.1:6379> setnx lock 1
(integer) 0
解锁
通过 del 命令将锁删除即可。
127.0.0.1:6379> del lock
(integer) 1
总结
通过上述流程,即可实现基础的加锁、解锁操作,在没有出现意外的情况下,基本能够正常工作。但在一些异常情况下可能会存在问题:
- 如果加锁成功但解锁失败,会导致锁一直无法释放,产生业务死锁问题。为了解决这个问题,可在加锁时设置超时时间,即便没有正常解锁,一段时间后也能自动释放。
加锁时设置超时时间
我们可以通过expire 命令来设置超时时间,但这样就导致加锁、设置超时时间是一个非原子操作,在极端情况下,仍有可能存在加锁成功、设置超时时间失败的情况。
127.0.0.1:6379> set lock 1
OK
127.0.0.1:6379> expire lock 5000
(integer) 1
我们可以使用 redis 提供的 nx 条件来实现加锁、设置超时时间是一组原子操作。下面的命令表示:在赋值的同时、进行超时时间的设置,能够保证原子性,并且,使用了 nx 能够保证同一时刻仅有一个客户端加锁成功。
127.0.0.1:6379> set lock 1 ex 5000 nx
OK
127.0.0.1:6379> set lock 1 ex 5000 nx
(nil)
总结
通过给加锁设置超时时间,能够保证锁即便由于意外情况(如服务器宕机)也能够正常释放。现在看来加锁没有什么问题了,但解锁时候,在某些情况下可能会导致一定的异常情况。比如误删除锁,误删的情况是指由于业务代码执行时间过长而导致了锁的自动释放,由下图表格我们可以看到,在某一时刻,会存在t1、t2 两个线程同时执行业务逻辑代码,导致分布式问题。
锁持有情况 | t1 | t2 |
---|---|---|
t1 | 获取锁 | |
t1 |