【大话Java面试】-如何通俗易懂的理解Redis分布式锁?

本文介绍了在分布式集群环境中如何保证不同节点间的线程同步执行。主要探讨了三种分布式锁的实现方式:Memcached分布式锁、Redis分布式锁和Zookeeper分布式锁。重点讲解了使用Redis实现分布式锁的具体步骤和注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【大话Java面试】-如何通俗易懂的理解分布式锁?

一、前言

首先我们先考虑以下这个问题:

在多线程并发的情况下,我们如何保证一个代码块在同一时间只能由一个线程访问呢?

答案:通常来说,我们可以用来保证。比如java的synchronized用法以及ReentrantLock等等。这样就可以保证同一个JVM进程内的多个线程同步执行。

那么如果在分布式的集群环境中,如何保证不同结点的线程同步执行呢?

对于分布式场景,我们可以尝试用分布式锁。

二、分布式锁的实现有哪些呢?

a.Memcached分布式锁

利用Memcached的add命令。此命令是原子性操作,只有在key不存在的情况下,才能add成功,也就意味着线程得到了锁。

b.Redis分布式锁

和Memcached的方式类似,利用Redis的setnx命令。此命令同样是原子性操作,只有在key不存在的情况下,才能set成功。(setnx命令并不完善,后续会介绍替代方案)

c.Zookeeper分布式锁

利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper设计的初衷,就是为了实现分布式锁服务的。

三、如何用Redis来实现分布式锁呢?

3.1 分布式锁的三个核心要素

1.加锁

使用setnx来加锁。key是锁的唯一标识,按业务来决定命名,value这里设置为test。

setx key test

当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败;

2.解锁

有加锁就得有解锁。当得到的锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式就是执行del指令。

del key

释放锁之后,其他线程就可以继续执行setnx命令来获得锁。

3.锁超时

锁超时知道的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程北向进来。

所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一段时间后自动释放。setnx不支持超时参数,所以需要额外指令,

expire key 30

3.2 上述分布式锁存在的问题

通过上述setnxdelexpire实现的分布式锁还是存在着一些问题。下面将具体举例说明该问题:

1.setnx和expire的非原子性问题

假设一个场景中,某一个线程刚执行setnx,成功得到了锁。此时setnx刚执行成功,还未来得及执行expire命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。

解决措施:

由于setnx指令本身是不支持传入超时时间的,而在Redis2.6.12版本上为set指令增加了可选参数, 用法如下:

SET key value [EX seconds][PX milliseconds] [NX|XX]

  • EX second: 设置键的过期时间为second秒;
  • PX millisecond:设置键的过期时间为millisecond毫秒;
  • NX:只在键不存在时,才对键进行设置操作;
  • XX:只在键已经存在时,才对键进行设置操作;
  • SET操作完成时,返回OK,否则返回nil。
2.del导致误删

假设在这样一个场景当中,假如某线程成功获得了锁,并且设置的超时时间是30秒。

但是如果由于某些原因导致线程A执行的速度很慢,过了30s都还没执行完成,这时候锁过期自动释放,线程B得到了锁。

随后,线程A执行完了任务,线程A接着执行del指令来释放锁,但这时候线程B还没执行,线程A实际上删除的是线程B加的锁,

解决办法:

在del释放锁之前加一个判断,验证当前的锁是不是自己加的锁。

具体在加锁的时候把当前线程的id当做value,在删除之前验证key对应的value是不是自己线程的id;

3.出现并发的可能性

第三个问题其实还是刚才第二点所描述的场景。虽然避免了线程A误删掉key的情况,但是同一时间有A和B两个线程在访问代码块,还是不完美的。

解决办法:

可以让获得锁的线程开启一个守护线程,用来给快要过期的锁”续航“。

当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。当线程A执行完任务,会显式关掉守护线程。

另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

在Spring Boot中实现Redis分布式锁可以通过以下步骤: 1. 添加Redis依赖:在pom.xml文件中添加Spring Data Redis依赖。 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` 2. 配置Redis连接:在application.properties或application.yml文件中配置Redis连接信息。 ```yaml spring.redis.host=your-redis-host spring.redis.port=your-redis-port ``` 3. 创建分布式锁工具类:创建一个用于获取和释放分布式锁的工具类。 ```java @Component public class RedisLock { @Autowired private StringRedisTemplate redisTemplate; public boolean tryLock(String key, String value, long expireTime) { Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofMillis(expireTime)); return result != null && result; } public void releaseLock(String key, String value) { String currentValue = redisTemplate.opsForValue().get(key); if (currentValue != null && currentValue.equals(value)) { redisTemplate.delete(key); } } } ``` 4. 使用分布式锁:在需要加的地方使用分布式锁。 ```java @Autowired private RedisLock redisLock; public void doSomethingWithLock() { String key = "lockKey"; String value = UUID.randomUUID().toString(); long expireTime = 5000; // 的过期时间,单位为毫秒 try { if (redisLock.tryLock(key, value, expireTime)) { // 获得成功,执行业务逻辑 // ... } else { // 获得失败,执行其他逻辑 // ... } } finally { redisLock.releaseLock(key, value); } } ``` 这样就可以在Spring Boot中使用Redis实现分布式锁了。需要注意的是,分布式锁的实现可能涉及到更复杂的情况,比如的重入性、防止死等,需要根据具体的业务场景进行适当的扩展和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mind_programmonkey

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值