一文学会基于 Redis 的分布式锁实现

什么是分布式锁?

分布式锁是一种机制,用于在分布式系统中控制对共享资源的访问。它确保在同一时间只有一个进程可以访问特定资源,从而避免数据不一致和竞争条件。分布式锁通常用于以下场景:

  • 限制对数据库的写入操作,防止出现脏读。
  • 控制对文件的并发访问,防止文件损坏。
  • 在微服务架构中协调服务之间的操作。
  • 分布式任务场景中只有一台机器执行。

Redis 分布式锁的实现原理

Redis 提供了简单而高效的分布式锁实现,主要依赖于其原子性操作和过期时间设置。基本的实现思路如下:

  1. 加锁:客户端尝试使用 SETNX 命令设置一个锁的键(例如 lock:resource)。如果该键不存在,则设置成功,返回 1,表示获得锁;如果该键已存在,则返回 0,表示锁已被其他客户端占用。

  2. 设置过期时间:为了防止死锁情况,通常在加锁时会同时设置一个过期时间。这样,如果持锁的客户端崩溃或未能释放锁,锁会在过期后自动释放。

  3. 释放锁:客户端在完成操作后,需要删除锁的键。释放锁时要确保只有持锁的客户端才能释放锁,这通常通过比较锁的值(如 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值