【高并发】高并发分布式锁架构解密,不是所有的锁都是分布式锁(升级版)

最后

针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。

最新整理面试题
在这里插入图片描述

上述的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题

最新整理电子书

在这里插入图片描述

最新整理大厂面试文档

在这里插入图片描述

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

电商超卖问题


这里,我们可以列举一个简单的业务场景。比如,在电子商务(商城)的业务场景中,提交订单购买商品时,首先需要查询相应商品的库存是否足够,只有在商品库存数量足够的前提下,才能让用户成功的下单。下单时,我们需要在库存数量中减去用户下单的商品数量,并将库存操作的结果数据更新到数据库中。整个流程我们可以简化成下图所示。

在这里插入图片描述

很多小伙伴也留言说,让我给出代码,这样能够更好的学习和掌握相关的知识。好吧,这里,我也给出相应的代码片段吧。我们可以使用下面的代码片段来表示用户的下单操作,我这里将商品的库存信息保存在了Redis中。

@RequestMapping(“/submitOrder”)

public String submitOrder(){

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(“stock”));

if(stock > 0){

stock -= 1;

stringRedisTemplate.opsForValue().set(“stock”, String.valueOf(stock));

logger.debug(“库存扣减成功,当前库存为:{}”, stock);

}else{

logger.debug(“库存不足,扣减库存失败”);

throw new OrderException(“库存不足,扣减库存失败”);

}

return “success”;

}

注意:上述代码片段比较简单,只是为了方便大家理解,真正项目中的代码就不能这么写了。

上述的代码看似是没啥问题的,但是我们不能只从代码表面上来观察代码的执行顺序。这是因为在JVM中代码的执行顺序未必是按照我们书写代码的顺序执行的。即使在JVM中代码是按照我们书写的顺序执行,那我们对外提供的接口一旦暴露出去,就会有成千上万的客户端来访问我们的接口。所以说,我们暴露出去的接口是会被并发访问的。

试问,上面的代码在高并发环境下是线程安全的吗?答案肯定不是线程安全的,因为上述扣减库存的操作会出现并行执行的情况。

我们可以使用Apache JMeter来对上述接口进行测试,这里,我使用Apache JMeter对上述接口进行测试。

在这里插入图片描述

在Jmeter中,我将线程的并发度设置为3,接下来的配置如下所示。

在这里插入图片描述

以HTTP GET请求的方式来并发访问提交订单的接口。此时,运行JMeter来访问接口,命令行会打印出下面的日志信息。

库存扣减成功,当前库存为:49

库存扣减成功,当前库存为:49

库存扣减成功,当前库存为:49

这里,我们明明请求了3次,也就是说,提交了3笔订单,为什么扣减后的库存都是一样的呢?这种现象在电商领域有一个专业的名词叫做 “超卖”

如果一个大型的高并发电商系统,比如淘宝、天猫、京东等,出现了超卖现象,那损失就无法估量了!架构设计和开发电商系统的人员估计就要通通下岗了。所以,作为技术人员,我们一定要严谨的对待技术,严格做好系统的每一个技术环节。

JVM中提供的锁


JVM中提供的synchronized和Lock锁,相信大家并不陌生了,很多小伙伴都会使用这些锁,也能使用这些锁来实现一些简单的线程互斥功能。那么,作为立志要成为架构师的你,是否了解过JVM锁的底层原理呢?

JVM锁原理

说到JVM锁的原理,我们就不得不限说说Java中的对象头了。

Java中的对象头

每个Java对象都有对象头。如果是⾮数组类型,则⽤2个字宽来存储对象头,如果是数组,则会⽤3个字宽来存储对象头。在32位处理器中,⼀个字宽是32位;在64位虚拟机中,⼀个字宽是64位。

对象头的内容如下表 。

| 长度 | 内容 | 说明 |

| — | — | — |

| 32/64bit | Mark Word | 存储对象的hashCode或锁信息等 |

| 32/64bit | Class Metadata Access | 存储到对象类型数据的指针 |

| 32/64bit | Array length | 数组的长度(如果是数组) |

Mark Work的格式如下所示。

| 锁状态 | 29bit或61bit | 1bit是否是偏向锁? | 2bit锁标志位 |

| — | — | — | — |

| 无锁 | | 0 | 01 |

| 偏向锁 | 线程ID | 1 | 01 |

| 轻量级锁 | 指向栈中锁记录的指针 | 此时这一位不用于标识偏向锁 | 00 |

| 重量级锁 | 指向互斥量(重量级锁)的指针 | 此时这一位不用于标识偏向锁 | 10 |

| GC标记 | | 此时这一位不用于标识偏向锁 | 11 |

可以看到,当对象状态为偏向锁时, Mark Word 存储的是偏向的线程ID;当状态为轻量级锁时, Mark Word 存储的是指向线程栈中 Lock Record 的指针;当状态为重量级锁时, Mark Word 为指向堆中的monitor对象的指针 。

有关Java对象头的知识,参考《深入浅出Java多线程》。

JVM锁原理

简单点来说,JVM中锁的原理如下。

在Java对象的对象头上,有一个锁的标记,比如,第一个线程执行程序时,检查Java对象头中的锁标记,发现Java对象头中的锁标记为未加锁状态,于是为Java对象进行了加锁操作,将对象头中的锁标记设置为锁定状态。第二个线程执行同样的程序时,也会检查Java对象头中的锁标记,此时会发现Java对象头中的锁标记的状态为锁定状态。于是,第二个线程会进入相应的阻塞队列中进行等待。

这里有一个关键点就是Java对象头中的锁标记如何实现。

JVM锁的短板

JVM中提供的synchronized和Lock锁都是JVM级别的,大家都知道,当运行一个Java程序时,会启动一个JVM进程来运行我们的应用程序。synchronized和Lock在JVM级别有效,也就是说,synchronized和Lock在同一Java进程内有效。如果我们开发的应用程序是分布式的,那么只是使用synchronized和Lock来解决分布式场景下的高并发问题,就会显得有点力不从心了。

synchronized和Lock支持JVM同一进程内部的线程互斥

synchronized和Lock在JVM级别能够保证高并发程序的互斥,我们可以使用下图来表示。

在这里插入图片描述

但是,当我们将应用程序部署成分布式架构,或者将应用程序在不同的JVM进程中运行时,synchronized和Lock就不能保证分布式架构和多JVM进程下应用程序的互斥性了。

synchronized和Lock不能实现多JVM进程之间的线程互斥

分布式架构和多JVM进程的本质都是将应用程序部署在不同的JVM实例中,也就是说,其本质还是多JVM进程。

在这里插入图片描述

分布式锁


我们在实现分布式锁时,可以参照JVM锁实现的思想,JVM锁在为对象加锁时,通过改变Java对象的对象头中的锁的标志位来实现,也就是说,所有的线程都会访问这个Java对象的对象头中的锁标志位。

在这里插入图片描述

我们同样以这种思想来实现分布式锁,当我们将应用程序进行拆分并部署成分布式架构时,所有应用程序中的线程访问共享变量时,都到同一个地方去检查当前程序的临界区是否进行了加锁操作,而是否进行了加锁操作,我们在统一的地方使用相应的状态来进行标记。

在这里插入图片描述

可以看到,在分布式锁的实现思想上,与JVM锁相差不大。而在实现分布式锁中,保存加锁状态的服务可以使用MySQL、Redis和Zookeeper实现。

但是,在互联网高并发环境中, 使用Redis实现分布式锁的方案是使用的最多的。 接下来,我们就使用Redis来深入解密分布式锁的架构设计。

Redis如何实现分布式锁


Redis命令

在Redis中,有一个不常使用的命令如下所示。

SETNX key value

这条命令的含义就是“SET if Not Exists”,即不存在的时候才会设置值。

只有在key不存在的情况下,将键key的值设置为value。如果key已经存在,则SETNX命令不做任何操作。

这个命令的返回值如下。

  • 命令在设置成功时返回1。

  • 命令在设置失败时返回0。

所以,我们在分布式高并发环境下,可以使用Redis的SETNX命令来实现分布式锁。假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行SETNX命令设置加锁状态后继续向下执行。

引入分布式锁

了解了如何使用Redis中的命令实现分布式锁后,我们就可以对下单接口进行改造了,加入分布式锁,如下所示。

/**

  • 为了演示方便,我这里就简单定义了一个常量作为商品的id

  • 实际工作中,这个商品id是前端进行下单操作传递过来的参数

*/

public static final String PRODUCT_ID = “100001”;

@RequestMapping(“/submitOrder”)

public String submitOrder(){

//通过stringRedisTemplate来调用Redis的SETNX命令,key为商品的id,value为字符串“binghe”

//实际上,value可以为任意的字符换

Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, “binghe”);

//没有拿到锁,返回下单失败

if(!isLock){

return “failure”;

}

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(“stock”));

if(stock > 0){

stock -= 1;

stringRedisTemplate.opsForValue().set(“stock”, String.valueOf(stock));

logger.debug(“库存扣减成功,当前库存为:{}”, stock);

}else{

logger.debug(“库存不足,扣减库存失败”);

throw new OrderException(“库存不足,扣减库存失败”);

}

//业务执行完成,删除PRODUCT_ID key

stringRedisTemplate.delete(PRODUCT_ID);

return “success”;

}

那么,在上述代码中,我们加入了分布式锁的操作,那上述代码是否能够在高并发场景下保证业务的原子性呢?答案是可以保证业务的原子性。但是,在实际场景中,上面实现分布式锁的代码是不可用的!!

假设当线程A首先执行stringRedisTemplate.opsForValue()的setIfAbsent()方法返回true,继续向下执行,正在执行业务代码时,抛出了异常,线程A直接退出了JVM。此时,stringRedisTemplate.delete(PRODUCT_ID);代码还没来得及执行,之后所有的线程进入提交订单的方法时,调用stringRedisTemplate.opsForValue()的setIfAbsent()方法都会返回false。导致后续的所有下单操作都会失败。这就是分布式场景下的死锁问题。

所以,上述代码中实现分布式锁的方式在实际场景下是不可取的!!

引入try-finally代码块

说到这,相信小伙伴们都能够想到,使用try-finall代码块啊,接下来,我们为下单接口的方法加上try-finally代码块。

/**

  • 为了演示方便,我这里就简单定义了一个常量作为商品的id

  • 实际工作中,这个商品id是前端进行下单操作传递过来的参数

*/

public static final String PRODUCT_ID = “100001”;

@RequestMapping(“/submitOrder”)

public String submitOrder(){

//通过stringRedisTemplate来调用Redis的SETNX命令,key为商品的id,value为字符串“binghe”

//实际上,value可以为任意的字符换

Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, “binghe”);

//没有拿到锁,返回下单失败

if(!isLock){

return “failure”;

}

try{

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(“stock”));

if(stock > 0){

stock -= 1;

stringRedisTemplate.opsForValue().set(“stock”, String.valueOf(stock));

logger.debug(“库存扣减成功,当前库存为:{}”, stock);

}else{

logger.debug(“库存不足,扣减库存失败”);

throw new OrderException(“库存不足,扣减库存失败”);

}

}finally{

//业务执行完成,删除PRODUCT_ID key

stringRedisTemplate.delete(PRODUCT_ID);

}

return “success”;

}

那么,上述代码是否真正解决了死锁的问题呢?我们在写代码时,不能只盯着代码本身,觉得上述代码没啥问题了。实际上,生产环境是非常复杂的。如果线程在成功加锁之后,执行业务代码时,还没来得及执行删除锁标志的代码,此时,服务器宕机了,程序并没有优雅的退出JVM。也会使得后续的线程进入提交订单的方法时,因无法成功的设置锁标志位而下单失败。所以说,上述的代码仍然存在问题。

引入Redis超时机制

在Redis中可以设置缓存的自动过期时间,我们可以将其引入到分布式锁的实现中,如下代码所示。

/**

  • 为了演示方便,我这里就简单定义了一个常量作为商品的id

  • 实际工作中,这个商品id是前端进行下单操作传递过来的参数

*/

public static final String PRODUCT_ID = “100001”;

@RequestMapping(“/submitOrder”)

public String submitOrder(){

//通过stringRedisTemplate来调用Redis的SETNX命令,key为商品的id,value为字符串“binghe”

//实际上,value可以为任意的字符换

Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, “binghe”);

//没有拿到锁,返回下单失败

if(!isLock){

return “failure”;

}

try{

stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(“stock”));

if(stock > 0){

stock -= 1;

stringRedisTemplate.opsForValue().set(“stock”, String.valueOf(stock));

logger.debug(“库存扣减成功,当前库存为:{}”, stock);

}else{

logger.debug(“库存不足,扣减库存失败”);

throw new OrderException(“库存不足,扣减库存失败”);

}

}finally{

//业务执行完成,删除PRODUCT_ID key

stringRedisTemplate.delete(PRODUCT_ID);

}

return “success”;

}

在上述代码中,我们加入了如下一行代码来为Redis中的锁标志设置过期时间。

stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS);

此时,我们设置的过期时间为30秒。

那么问题来了,这样是否就真正的解决了问题呢?上述程序就真的没有坑了吗?答案是还是有坑的!!

“坑位”分析

我们在下单操作的方法中为分布式锁引入了超时机制,此时的代码还是无法真正避免死锁的问题,那“坑位”到底在哪里呢?试想,当程序执行完stringRedisTemplate.opsForValue().setIfAbsent()方法后,正要执行stringRedisTemplate.expire(PRODUCT_ID, 30, TimeUnit.SECONDS)代码时,服务器宕机了,你还别说,生产坏境的情况非常复杂,就是这么巧,服务器就宕机了。此时,后续请求进入提交订单的方法时,都会因为无法成功设置锁标志而导致后续下单流程无法正常执行。

既然我们找到了上述代码的“坑位”,那我们如何将这个”坑“填上?如何解决这个问题呢?别急,Redis已经提供了这样的功能。我们可以在向Redis中保存数据的时候,可以同时指定数据的超时时间。所以,我们可以将代码改造成如下所示。

/**

  • 为了演示方便,我这里就简单定义了一个常量作为商品的id

  • 实际工作中,这个商品id是前端进行下单操作传递过来的参数

*/

public static final String PRODUCT_ID = “100001”;

@RequestMapping(“/submitOrder”)

public String submitOrder(){

//通过stringRedisTemplate来调用Redis的SETNX命令,key为商品的id,value为字符串“binghe”

//实际上,value可以为任意的字符换

Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, “binghe”, 30, TimeUnit.SECONDS);

//没有拿到锁,返回下单失败

if(!isLock){

return “failure”;

}

try{

int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(“stock”));

if(stock > 0){

stock -= 1;

stringRedisTemplate.opsForValue().set(“stock”, String.valueOf(stock));

logger.debug(“库存扣减成功,当前库存为:{}”, stock);

}else{

logger.debug(“库存不足,扣减库存失败”);

throw new OrderException(“库存不足,扣减库存失败”);

}

}finally{

//业务执行完成,删除PRODUCT_ID key

stringRedisTemplate.delete(PRODUCT_ID);

}

return “success”;

}

在上述代码中,我们在向Redis中设置锁标志位的时候就设置了超时时间。此时,只要向Redis中成功设置了数据,则即使我们的业务系统宕机,Redis中的数据过期后,也会自动删除。后续的线程进入提交订单的方法后,就会成功的设置锁标志位,并向下执行正常的下单流程。

到此,上述的代码基本上在功能角度解决了程序的死锁问题,那么,上述程序真的就完美了吗?哈哈,很多小伙伴肯定会说不完美!确实,上面的代码还不是完美的,那大家知道哪里不完美吗?接下来,我们继续分析。

在开发集成角度分析代码

在我们开发公共的系统组件时,比如我们这里说的分布式锁,我们肯定会抽取一些公共的类来完成相应的功能来供系统使用。

这里,假设我们定义了一个RedisLock接口,如下所示。

public interface RedisLock{

//加锁操作

boolean tryLock(String key, long timeout, TimeUnit unit);

//解锁操作

void releaseLock(String key);

}

接下来,使用RedisLockImpl类实现RedisLock接口,提供具体的加锁和解锁实现,如下所示。

public class RedisLockImpl implements RedisLock{

@Autowired

private StringRedisTemplate stringRedisTemplate;

@Override

public boolean tryLock(String key, long timeout, TimeUnit unit){

return stringRedisTemplate.opsForValue().setIfAbsent(key, “binghe”, timeout, unit);

}

@Override

public void releaseLock(String key){

stringRedisTemplate.delete(key);

}

}

在开发集成的角度来说,当一个线程从上到下执行时,首先对程序进行加锁操作,然后执行业务代码,执行完成后,再进行释放锁的操作。理论上,加锁和释放锁时,操作的Redis Key都是一样的。但是,如果其他开发人员在编写代码时,并没有调用tryLock()方法,而是直接调用了releaseLock()方法,并且他调用releaseLock()方法传递的key与你调用tryLock()方法传递的key是一样的。那此时就会出现问题了,他在编写代码时,硬生生的将你加的锁释放了!!!

所以,上述代码是不安全的,别人能够随随便便的将你加的锁删除,这就是锁的误删操作,这是非常危险的,所以,上述的程序存在很严重的问题!!

那如何实现只有加锁的线程才能进行相应的解锁操作呢? 继续向下看。

如何实现加锁和解锁的归一化?

什么是加锁和解锁的归一化呢?简单点来说,就是一个线程执行了加锁操作后,后续必须由这个线程执行解锁操作,加锁和解锁操作由同一个线程来完成。

为了解决只有加锁的线程才能进行相应的解锁操作的问题,那么,我们就需要将加锁和解锁操作绑定到同一个线程中,那么,如何将加锁操作和解锁操作绑定到同一个线程呢?其实很简单,相信很多小伙伴都想到了—— 使用ThreadLocal实现 。没错,使用ThreadLocal类确实能够解决这个问题。

此时,我们将RedisLockImpl类的代码修改成如下所示。

public class RedisLockImpl implements RedisLock{

@Autowired

private StringRedisTemplate stringRedisTemplate;

private ThreadLocal threadLocal = new ThreadLocal();

@Override

public boolean tryLock(String key, long timeout, TimeUnit unit){

String uuid = UUID.randomUUID().toString();

threadLocal.set(uuid);

return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);

}

@Override

public void releaseLock(String key){

//当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作

if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){

stringRedisTemplate.delete(key);

}

}

}

上述代码的主要逻辑为:在对程序执行尝试加锁操作时,首先生成一个uuid,将生成的uuid绑定到当前线程,并将传递的key参数操作Redis中的key,生成的uuid作为Redis中的Value,保存到Redis中,同时设置超时时间。当执行解锁操作时,首先,判断当前线程中绑定的uuid是否和Redis中存储的uuid相等,只有二者相等时,才会执行删除锁标志位的操作。这就避免了一个线程对程序进行了加锁操作后,其他线程对这个锁进行了解锁操作的问题。

继续分析

我们将加锁和解锁的方法改成如下所示。

public class RedisLockImpl implements RedisLock{

@Autowired

private StringRedisTemplate stringRedisTemplate;

private ThreadLocal threadLocal = new ThreadLocal();

private String lockUUID;

@Override

public boolean tryLock(String key, long timeout, TimeUnit unit){

String uuid = UUID.randomUUID().toString();

threadLocal.set(uuid);

lockUUID = uuid;

return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);

}

@Override

public void releaseLock(String key){

//当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作

if(lockUUID.equals(stringRedisTemplate.opsForValue().get(key))){

stringRedisTemplate.delete(key);

}

}

}

相信很多小伙伴都会看出上述代码存在什么问题了!! 没错,那就是 线程安全的问题。

所以,这里,我们需要使用ThreadLocal来解决线程安全问题。

可重入性分析

在上面的代码中,当一个线程成功设置了锁标志位后,其他的线程再设置锁标志位时,就会返回失败。还有一种场景就是在提交订单的接口方法中,调用了服务A,服务A调用了服务B,而服务B的方法中存在对同一个商品的加锁和解锁操作。

所以,服务B成功设置锁标志位后,提交订单的接口方法继续执行时,也不能成功设置锁标志位了。也就是说,目前实现的分布式锁没有可重入性。

这里,就存在可重入性的问题了。我们希望设计的分布式锁 具有可重入性 ,那什么是可重入性呢?简单点来说,就是同一个线程,能够多次获取同一把锁,并且能够按照顺序进行解决操作。

其实,在JDK 1.5之后提供的锁很多都支持可重入性,比如synchronized和Lock。

如何实现可重入性呢?

映射到我们加锁和解锁方法时,我们如何支持同一个线程能够多次获取到锁(设置锁标志位)呢?可以这样简单的设计:如果当前线程没有绑定uuid,则生成uuid绑定到当前线程,并且在Redis中设置锁标志位。如果当前线程已经绑定了uuid,则直接返回true,证明当前线程之前已经设置了锁标志位,也就是说已经获取到了锁,直接返回true。

结合以上分析,我们将提交订单的接口方法代码改造成如下所示。

public class RedisLockImpl implements RedisLock{

@Autowired

private StringRedisTemplate stringRedisTemplate;

private ThreadLocal threadLocal = new ThreadLocal();

@Override

public boolean tryLock(String key, long timeout, TimeUnit unit){

Boolean isLocked = false;

if(threadLocal.get() == null){

String uuid = UUID.randomUUID().toString();

threadLocal.set(uuid);

isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);

}else{

isLocked = true;

}

return isLocked;

}

@Override

public void releaseLock(String key){

//当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作

if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){

stringRedisTemplate.delete(key);

}

}

}

这样写看似没有啥问题,但是大家细想一下,这样写就真的OK了吗?

可重入性的问题分析

既然上面分布式锁的可重入性是存在问题的,那我们就来分析下问题的根源在哪里!

假设我们提交订单的方法中,首先使用RedisLock接口对代码块添加了分布式锁,在加锁后的代码中调用了服务A,而服务A中也存在调用RedisLock接口的加锁和解锁操作。而多次调用RedisLock接口的加锁操作时,只要之前的锁没有失效,则会直接返回true,表示成功获取锁。也就是说,无论调用加锁操作多少次,最终只会成功加锁一次。而执行完服务A中的逻辑后,在服务A中调用RedisLock接口的解锁方法,此时,会将当前线程所有的加锁操作获得的锁全部释放掉。

我们可以使用下图来简单的表示这个过程。

在这里插入图片描述

那么问题来了,如何解决可重入性的问题呢?

解决可重入性问题

相信很多小伙伴都能够想出使用计数器的方式来解决上面可重入性的问题,没错,就是使用计数器来解决。 整体流程如下所示。

在这里插入图片描述

那么,体现在程序代码上是什么样子呢?我们来修改RedisLockImpl类的代码,如下所示。

public class RedisLockImpl implements RedisLock{

@Autowired

private StringRedisTemplate stringRedisTemplate;

private ThreadLocal threadLocal = new ThreadLocal();

private ThreadLocal threadLocalInteger = new ThreadLocal();

@Override

public boolean tryLock(String key, long timeout, TimeUnit unit){

Boolean isLocked = false;

if(threadLocal.get() == null){

String uuid = UUID.randomUUID().toString();

threadLocal.set(uuid);

isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);

}else{

isLocked = true;

}

//加锁成功后将计数器加1

if(isLocked){

Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();

threadLocalInteger.set(count++);

}

return isLocked;

}

@Override

public void releaseLock(String key){

//当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作

if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){

Integer count = threadLocalInteger.get();

//计数器减为0时释放锁

if(count == null || --count <= 0){

stringRedisTemplate.delete(key);

}

}

}

}

至此,我们基本上解决了分布式锁的可重入性问题。

说到这里,我还要问大家一句,上面的解决问题的方案真的没问题了吗?

阻塞与非阻塞锁

在提交订单的方法中,当获取Redis分布式锁失败时,我们直接返回了failure来表示当前请求下单的操作失败了。试想,在高并发环境下,一旦某个请求获得了分布式锁,那么,在这个请求释放锁之前,其他的请求调用下单方法时,都会返回下单失败的信息。在真实场景中,这是非常不友好的。我们可以将后续的请求进行阻塞,直到当前请求释放锁后,再唤醒阻塞的请求获得分布式锁来执行方法。

所以,我们设计的分布式锁需要支持 阻塞和非阻塞 的特性。

那么,如何实现阻塞呢?我们可以使用自旋来实现,继续修改RedisLockImpl的代码如下所示。

public class RedisLockImpl implements RedisLock{

@Autowired

private StringRedisTemplate stringRedisTemplate;

private ThreadLocal threadLocal = new ThreadLocal();

private ThreadLocal threadLocalInteger = new ThreadLocal();

@Override

public boolean tryLock(String key, long timeout, TimeUnit unit){

Boolean isLocked = false;

if(threadLocal.get() == null){

总结

我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

lass RedisLockImpl implements RedisLock{

@Autowired

private StringRedisTemplate stringRedisTemplate;

private ThreadLocal threadLocal = new ThreadLocal();

private ThreadLocal threadLocalInteger = new ThreadLocal();

@Override

public boolean tryLock(String key, long timeout, TimeUnit unit){

Boolean isLocked = false;

if(threadLocal.get() == null){

总结

我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

[外链图片转存中…(img-HxGIEkuq-1715490303414)]

[外链图片转存中…(img-flDuM3bj-1715490303415)]

[外链图片转存中…(img-RT80MwxY-1715490303415)]

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 15
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值