Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【📕分布式锁通关指南 02】基于Redis实现的分布式锁

【📕分布式锁通关指南 02】基于Redis实现的分布式锁

原创
作者头像
别惹CC
发布于 2025-02-19 02:08:24
发布于 2025-02-19 02:08:24
2250
举报

引言

在01篇文章中,我们深入探讨了单机锁的多种实现方式,并相信各位读者已经对它们有了较为全面的了解。然而,随着我们对单机锁的深入了解,不难发现它们所固有的一些局限性。因此,从本篇开始,我们将开始探讨分布式锁的相关内容。

认识分布式锁

首先,先来看它的概念-控制分布式系统之间同步访问共享资源的一种方式。所以,它需要满足以下四个特性:互斥性可重入性锁超时防死锁锁释放正确防误删。而01篇中提到的JVM锁在分布式场景中就会存在问题,比如,我们当前有两个服务实例,它们都访问商品库存表进行扣减库存,如果使用JVM锁,其实并没有效果,如图:

JVM锁只能锁所在服务的实例,所以在分布式场景下,有多少个服务实例自然也会存在多少个JVM锁。那么有解决办法吗?当然是有的。没有什么是加一层解决不了的,我们只需要在服务实例和数据库之间再加一层作为分布式锁即可,如图:

我们可以依靠中间件来实现加的这一层,常见的有reidsZookeeperEtcd等,本篇我们将以redis分布式锁的实现展开讲解,其他实现也会在后续篇中陆续讲解。

redis实现分布式锁的思路

在开始实现前,我们先来聊聊为什么选择redis来实现分布式锁。这里做技术选型,自然离不开对中间件本身的特点进行分析,redis的以下特点足够支持它来实现分布式锁:

  • 1.Redis是高性能的内存数据库,满足高并发的需求;
  • 2.Redis支持原子性操作,保证操作的原子性和一致性;
  • 3.Redis支持分布式部署,支持多节点间的数据同步和复制,从而满足高可用性和容错性。

除了上述特性,redis客户端提供的一个命令让我们设置锁也变得更为简单,即setnx,区别于set命令,使用它来设置键值对,如果键已存在,就不会设置成功。所以使用这个命令来获取锁的话,我们可以省去很多判断逻辑。

redis实现简化版分布式锁

有了思路,我们可以尝试用代码来实现下。首先,使用redisTemplate来实现下加锁和解锁的方法。加锁就是用setnx命令设置个键值对,key根据业务场景设置,value随意;解锁就是根据key删除指定的键值对,如下:

代码语言:java
AI代码解释
复制
@Override
public void lock() {
    //1.使用setnx指令进行加锁
    while (true) {
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockName, "1");
        if (result != null && result) {
          break;
        }
    }
}

@Override
public void unlock() {
    stringRedisTemplate.delete(this.lockName);
}

接着我们继续以扣减库存为例,大致逻辑应该是先获取锁,锁的key就是商品id,拿到锁之后先判断库存数量是否足够,如果足够,则去扣减库存。如下:

代码语言:java
AI代码解释
复制
public String deductStockRedisLock(Long goodsId,Integer count) {

    AbstractLock lock = null;
    try {
        lock = new RedisLock(template, "stock" + goodsId);
        lock.lock();
        //1.查询商品库存数量
        String stock = template.opsForValue().get("stock" + goodsId);
        if (StringUtil.isNullOrEmpty(stock)) {
            return "商品不存在!";
        }
        int lastStock = Integer.parseInt(stock);
        //2.判断库存数量是否足够
        if (lastStock < count) {
            return "库存不足!";
        }
        //3.如果库存数量足够,则去扣减库存
        template.opsForValue().set("stock" + goodsId, String.valueOf(lastStock - count));
        return "扣减库存成功";
    } finally {
        if (lock != null) {
            lock.unlock();
        }
    }
}

接着我们启动熟悉的JMeter来进行测试,在开始前,我们先往redis里set一个key为stock1,value为6000的键值对来表示id为1的商品有6000库存,如下:

启动JMeter观察执行报告,会发现吞吐量很低,这里读者可以自行对比01篇中的数据。最直接的体现就是这里的扣减库存执行了差不多20s左右才完成,如下:

这个执行效率如果放到线上肯定是不行的,前面也讲过我们选择redis是奔着高性能去的,可是为什么表现却这么差呢?我们看下加锁的逻辑,如下:

代码语言:java
AI代码解释
复制
public void lock() {
    //1.使用setnx指令进行加锁
    while (true) {
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockName, "1");
        if (result != null && result) {
          break;
        }
    }
}

我们这里的加锁逻辑是只要没获取到锁就去重试,而redis的写命令执行的也比较快,所有这里在高并发场景下就变成了低效重试,那么有没有解决办法呢?当然是有的,很简单,我们只需要在获取失败后,让当前线程先停一下即可,如下:

代码语言:java
AI代码解释
复制
public void lock() {
    //1.使用setnx指令进行加锁
    while (true) {
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockName, "1");
        if (result != null && result) {
          break;
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

简化版分布式锁存在的问题

在上面的代码里,我们基于redis手撸了一个简化版的分布式锁,那么它是否就满足日常业务使用了呢?当然不行,既然是简化版的自然就存在问题。我们先来分析一下前文中提到的四个特性中的其中两个-锁超时防死锁锁释放正确防误删,那么我们的简化版能否满足呢?显然是不行的,因此就需要我们继续迭代了。

1.锁超时怎么办?

锁超时的情况可能有很多,比如扣减库存获取锁之后代码执行到一半服务挂掉了,由于是异常关闭,所以finally中释放锁的逻辑也没来得及执行,这个时候锁就被永久的持有了。所以为了解决这个问题,我们就需要为锁加上过期时间,这样可以保证无论业务或者服务是否出现异常,最终都可以保证锁的释放,代码如下:

代码语言:java
AI代码解释
复制
private final long defaultExpireTime = 30000;


@Override
public void lock() {
    lock(TimeUnit.MILLISECONDS, defaultExpireTime);
}

@Override
public void lock(TimeUnit timeUnit, Long expireTime) {
    //1.使用setnx指令进行加锁
    while (true) {
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockName, "1", expireTime, timeUnit);
        if (result != null && result) {
            break;
        }

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

所以其实很简单,只需要给锁加个过期时间就可以了,这个时间根据自己的业务场景定。因为如果你定的少了,假如我们定的过期时间是500毫秒,但是相应的业务逻辑执行完成需要800毫秒,那么就会造成业务逻辑还没执行完成,锁就被释放了,这锁就是加了个寂寞。

2.锁被误删了怎么办?

首先,我们来定义下什么叫锁误删,即某个线程持有的锁被别的线程删了。那么这里肯定就有同学疑惑了,按照我们上面的代码逻辑,假设现在有个A线程获取到锁了,在它没释放的情况下,其他线程应该是一直循环获取才对,也就是说这个时候其他线程根本就拿不到这把锁,又怎么能给它释放了呢。

其实问题就出在我们上面为了解决锁超时问题而给锁加了过期时间,我们假设A线程的业务逻辑处理的时间超过了锁超时释放的时间,就造成了A线程还没执行完,锁就自己释放了,这个时候B线程获取到了锁开始执行,而A线程继续执行到了释放锁的逻辑。注意:此时按照我们的设计,锁的key是商品id,也就是说A、B两线程拿到的是同一把锁,那么这个时候A线程的释放锁反而把B线程拿到的给释放了,最终肯定会造成并发问题的。那么知道了问题所在,我们怎么解决呢?很简单,只需要在释放锁之前判断下当前释放锁的线程是否是拿到锁的线程不就好了,只有一致的情况下才可以释放锁,代码如下:

代码语言:java
AI代码解释
复制
@Override
public void lock(TimeUnit timeUnit, Long expireTime) {
    //1.使用setnx指令进行加锁
    while (true) {
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockName, uuid, expireTime, timeUnit);
        if (result != null && result) {
            break;
        }

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

@Override
public void unlock() {
    //1.判断当前持有锁线程是否等于本线程
    String result = stringRedisTemplate.opsForValue().get(this.lockName);
    if (this.uuid.equals(result)) {
        stringRedisTemplate.delete(this.lockName);
    }
}

我们这里的做法是在获取锁的时候给value设置一个uuid,并在删除之前先判断当前线程的uuid和锁对应的uuid是否一致。

小结

本章节通过redis实现了一套简易的分布式锁,看似我们现在的设计已经非常完美,解决了锁超时和锁误删的问题,但实际上还有一些问题没有解决,比如释放锁那里,如果线程A执行过判断后刚好到了锁自然释放的时间,于是释放掉了,而正要执行删除锁的时候,线程B已经拿到锁了,但此时线程A肯定也不知道uuid已经发生变化了,于是执行删除顺利地把线程B刚拿到的锁给释放了,顺利地造成了后续的并发问题。因此,我们将在下一章解决这样的问题。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
先来回顾一下: 我们前面为了解决锁因异常情况(例如执行完加锁逻辑服务宕机了)未执行到释放,从而造成锁一直被占用的情况。而为了解决这个问题,我们给每个锁加上了过期时间,但是这又引申出了新的问题:如果锁到期了,而业务还没执行完,此时就给释放了,锁又被新的线程拿到了,那么就又会产生并发问题了。所以,我们是不希望锁在一定时间后自动过期掉的。那么,为了解决这个问题,我们应该在线程拿到锁后一直延长过期时间,直到业务执行完成后才释放这把锁。我们分析下可以怎么做:
别惹CC
2025/02/27
1590
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
利用 Redis 实现分布式锁
对于这个问题,我们可以简单将锁分为两种——内存级锁以及分布式锁,内存级锁即我们在 Java 中的 synchronized 关键字(或许加上进程级锁修饰更恰当些),而分布式锁则是应用在分布式系统中的一种锁机制。分布式锁的应用场景举例以下几种:
烂猪皮
2020/11/25
6300
利用 Redis 实现分布式锁
基于redis实现分布式锁思考
synchronized虽然能够解决同步问题,但是每次只有一个线程访问,并且synchronized锁属于JVM锁,仅适用于单点部署;然而分布式需要部署多台实例,属于不同的JVM线程对象
沁溪源
2020/12/28
8950
基于redis实现分布式锁思考
【高并发】高并发分布式锁架构解密,不是所有的锁都是分布式锁!!
作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了精准定时调度方案,经受住了生产环境的考验。为使更多童鞋受益,现给出开源框架地址:
冰河
2020/10/29
7430
【高并发】高并发分布式锁架构解密,不是所有的锁都是分布式锁!!
Redis分布式锁实战
我们学习 Java 都知道锁的概念,例如基于 JVM 实现的同步锁 synchronized,以及 jdk 提供的一套代码级别的锁机制 lock,我们在并发编程中会经常用这两种锁去保证代码在多线程环境下运行的正确性。但是这些锁机制在分布式场景下是不适用的,原因是在分布式业务场景下,我们的代码都是跑在不同的JVM甚至是不同的机器上,synchronized 和 lock 只能在同一个 JVM 环境下起作用。所以这时候就需要用到分布式锁了。
编程大道
2020/07/15
6420
Redis分布式锁实战
【高并发】高并发分布式锁架构解密,不是所有的锁都是分布式锁(升级版)!!
作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了精准定时调度方案,经受住了生产环境的考验。为使更多童鞋受益,现给出开源框架地址:
冰河
2020/10/29
6580
【高并发】高并发分布式锁架构解密,不是所有的锁都是分布式锁(升级版)!!
分布式锁-redis实现
为什么要分布式锁 在单机的情况下,可以通过jvm提供的系列线程安全的操作来处理高并发的情况,但是在分布式的环境下,jvm提供的线程安全操作明显是不能满足要求的。在一些小型的互联网公司经常做的crud操作如果在高并发的情况下会出现很大的问题,比如: //伪代码:下订单 1、查库存:getStock() 2、判断库存:stock>0下单 3、下单:addOrder() 4、减库存 仅仅以上三步,如果在高并发的情况下,无论是单机或者集群,如果不加锁一定会出现超卖的情况。一瞬间成千上万个请求过来,如何能够确保查询
爱撒谎的男孩
2020/03/09
5740
Redis进阶-细说分布式锁
Redis进阶-核心数据结构进阶实战 中我们讲 strings 数据结构的时候,举了一个例子
小小工匠
2021/08/17
4510
[Redis] 分布式缓存中间件 Redis 之 分布式锁实战
环境准备Redis 如何实现分布式锁线程不安全单机锁分布式锁代码实现Redisson 集成和源码分析Redisson 集成源码分析 `RedissonLock`加锁解锁集群分布式锁失效判断机制总结REFERENCES更多
架构探险之道
2020/03/19
8290
[Redis] 分布式缓存中间件 Redis 之 分布式锁实战
分布式锁:5个案例,附源码
常见的synchronized、Lock等这些锁都是基于单个JVM的实现的,如果分布式场景下怎么办呢?这时候分布式锁就出现了。
田维常
2021/11/26
4850
基于redis的分布式锁
两个微服务,synchronized关键字只能锁住一个微服务,跨微服务是锁不住的。
CBeann
2023/12/25
1990
基于redis的分布式锁
蚂蚁金服面试:如何优雅的用Redis实现分布式锁?
上述代码可以看到,当前锁的失效时间为10s,如果当前扣减库存的业务逻辑执行需要15s时,高并发时会出现问题:
Java程序猿
2021/02/25
5900
基于redis实现的分布式锁
借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发 送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false)。
一个风轻云淡
2023/12/05
4670
Redis高并发分布式锁详解
  1.为了解决Java共享内存模型带来的线程安全问题,我们可以通过加锁来保证资源访问的单一,如JVM内置锁synchronized,类级别的锁ReentrantLock。
忧愁的chafry
2022/10/30
1.1K0
Redis高并发分布式锁详解
手撸一把分布式锁
使用Java提供的synchronized关键字简简单单的就为我们的奖品兑换程序添加了一把锁,同步的只有一个线程可以对我们的数据库进行减库存的操作,安全的不行,但是姜同学突然想到这个奖品兑换的服务是部署在两台服务器的是分布式的,因为synchronized是基于JAVA虚拟机的进程锁,当我们的系统变为分布式以后如果还是使用这种方式可是要出问题的哦。
姜同学
2022/10/27
2050
【分布式进阶】我们来填填Redis分布式锁中的那些坑。
  大家好,我是Coder哥,最近在准备面试鸽了一段时间,面试告一段落了,今天我们来聊一下基于Redis锁中的那些坑。这篇分析比较全面,记得点赞收藏哟!!!
TodoCoder
2022/09/23
6370
【分布式进阶】我们来填填Redis分布式锁中的那些坑。
我们所了解的Redis分布式锁真的就万无一失吗?
在单体架构中,我们处理并发的手段有多种,例如synchronized或使用ReentrantLock等常用手段,但是在分布式架构中,上述所说的就不能解决某些业务的并发问题了,那么接下来我们就开始聊聊分布式锁。
黎明大大
2021/03/09
4190
Redis 分布式锁
一般电商网站都会遇到秒杀、特价之类的活动,大促活动有一个共同特点就是访问量激增,在高并发下会出现成千上万人抢购一个商品的场景。虽然在系统设计时会通过限流、异步、排队等方式优化,但整体的并发还是平时的数倍以上,参加活动的商品一般都是限量库存,如何防止库存超卖,避免并发问题呢?分布式锁就是一个解决方案。
张云飞Vir
2022/09/29
4640
java架构之路-(Redis专题)简单聊聊redis分布式锁
  这次我们来简单说说分布式锁,我记得过去我也过一篇JMM的内存一致性算法,就是说拿到锁的可以继续操作,没拿到的自旋等待。
小菜的不能再菜
2019/10/29
3790
java架构之路-(Redis专题)简单聊聊redis分布式锁
redis 分布式锁的 5个坑,真是又大又深
最近项目上线的频率颇高,连着几天加班熬夜,身体有点吃不消精神也有些萎靡,无奈业务方催的紧,工期就在眼前只能硬着头皮上了。脑子浑浑噩噩的时候,写的就不能叫代码,可以直接叫做Bug。我就熬夜写了一个bug被骂惨了。
程序员小富
2020/04/22
2.3K0
推荐阅读
相关推荐
【📕分布式锁通关指南 04】redis分布式锁的细节问题以及RedLock算法原理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文