网站优化

网站优化

Products

当前位置:首页 > 网站优化 >

如何用Redis实现分布式锁,攻略在手?

GG网络技术分享 2026-04-16 09:58 1


哎哟喂,分布式锁这玩意儿到底是个啥?

兄弟们, 咱们今天来聊点硬核的,真的,这玩意儿要是搞不懂,面试的时候面试官随便问两句,你就得在那儿干瞪眼,心里慌得一批。咱们之前在01篇文章里其实已经把单机锁给扒得底裤都不剩了大家应该都还记得吧?什么`synchronized`啊,`ReentrantLock`啊,那都是玩得溜溜的。但是!注意了啊,我要说但是了!一旦你上了分布式,上了微服务,那些个JVM锁,瞬间就变成了废铁,真的,一点用都没有,我跟你交个底...。

你想啊,JVM锁只能锁住存在多少个JVM锁。这就像什么呢?就像你家大门上了锁,但是你把窗户大开,小偷随便进。这哪行啊?那肯定不行啊!那咋办呢?别急,没有什么是加一层解决不了的,如果有,那就加两层。我们只需要在服务实例和数据库之间再加一层作为分布式锁即可,这思路是不是瞬间就清晰了,奥利给!?

基于Redis实现的分布式锁

我们可以依靠中间件来实现加的这一层, 常见的有redisZookeeperEtcd等,本篇我们将以redis分布式锁的实现展开讲解,其他实现也会在后续篇中陆续讲解。为啥选Redis?没办法,它快啊,大家都爱用,咱们也得随大流,是不是这个理儿?

Redis实现分布式锁的思路, 其实也就那么回事

在开始实现前,我们先来聊聊为什么选择redis来实现分布式锁。这里做技术选型, 自然离不开对中间件本身的特点进行分析,redis的以下特点足够支持它来实现分布式锁:高性能啊,支持各种数据结构啊,最主要的是它有个命令叫`setnx`。 一针见血。 除了上述特性, redis客户端提供的一个命令让我们设置锁也变得更为简单,即setnx区别于set命令,使用它来设置键值对,如果键已存在就不会设置成功。所以使用这个命令来获取锁的话,我们可以省去很多判断逻辑。

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

public void lock {
    //1.使用setnx指令进行加锁
    while  {
        Boolean result = .setIfAbsent;
        if  {
          break;
        }
    }
}
@Override
public void unlock {
    ;
}

你看,这代码是不是很简单?但是!千万别高兴得太早,这玩意儿要是直接扔到生产环境, 我CPU干烧了。 你估计第二天就得卷铺盖走人。为啥?咱们接着往下看。

这代码写得太烂了 性能简直感人

启动JMeter观察施行报告,会发现吞吐量很低,这里读者可以自行对比01篇中的数据。最直接的体现就是这里的扣减库存施行了差不多20s左右才完成,如下:这数据看着都让人头大。这个施行效率如果放到线上肯定是不行的, 前面也讲过我们选择redis是奔着高性能去的,可是为什么表现却这么差呢?我们看下加锁的逻辑, 如下:,我个人认为...

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

public void lock {
    //1.使用setnx指令进行加锁
    while  {
        Boolean result = .setIfAbsent;
        if  {
          break;
        }
        try {
            ;
        } catch  {
            throw new RuntimeException;
        }
    }
}

你看,加了个`sleep`,CPU瞬间就不那么烫了。但是这还不是最要命的。最要命的是啥?是死锁!对,你没听错,就是死锁,最终的最终。。

死锁?这玩意儿比死还难受

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

private final long defaultExpireTime = 30000;
@Override
public void lock {
    lock;
}
@Override
public void lock {
    //1.使用setnx指令进行加锁
    while  {
        Boolean result = .setIfAbsent;
        if  {
            break;
        }
        try {
            ;
        } catch  {
            throw new RuntimeException;
        }
    }
}

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

完了锁被别人删了!这咋整?

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

public String deductStockRedisLock {
    AbstractLock lock = null;
    try {
        lock = new RedisLock;
        ;
        //1.查询商品库存数量
        String stock = .get;
        if ) {
            return "商品不存在!";
        }
        int lastStock = ;
        //2.判断库存数量是否足够
        if  {
            return "库存不足!";
        }
        //3.如果库存数量足够, 则去扣减库存
        .set);
        return "扣减库存成功";
    } finally {
        if  {
            ;
        }
    }
}

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

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

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

@Override
public void lock {
    //1.使用setnx指令进行加锁
    while  {
        Boolean result = .setIfAbsent;
        if  {
            break;
        }
        try {
            ;
        } catch  {
            throw new RuntimeException;
        }
    }
}
@Override
public void unlock {
    //1.判断当前持有锁线程是否等于本线程
    String result = .get;
    if ) {
        ;
    }
}

市面上常见的分布式锁方案对比, 别选错了

说了这么多,咱们来看看市面上都有哪些方案,别到时候面试官问你,你只知道Redis,那多尴尬啊。我特意整理了一个表格,大家看看,心里有个数,扯后腿。。

方案 实现原理 优点 缺点 推荐指数
Redis SetNX + 过期时间 / Redisson / Lua脚本 性能高, 部署方便,支持多种数据结构 主从切换时可能丢失锁,实现复杂 ⭐⭐⭐⭐
Zookeeper 临时顺序节点 + Watcher机制 强一致性,锁可靠性高,不会丢失锁 性能不如Redis,部署维护成本高 ⭐⭐⭐
Etcd 类似Zookeeper,但更现代 强一致性,Go语言编写性能尚可 学习成本高,不如Redis普及 ⭐⭐⭐
数据库 唯一索引 / for update行锁 简单,不用引入新组件 性能极差,数据库压力大,不适合高并发

看这表格,Redis虽然有点小缺点,但是架不住它快啊,所以大部分场景下咱们还是首选Redis。但是如果你对数据一致性要求极高,比如涉及到钱的,那还是老老实实用Zookeeper吧,太水了。。

别自己造轮子了Redisson它不香吗?

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

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

这时候,就该轮到我们的主角——Redisson登场了!这玩意儿简直就是神器,人家把那些乱七八糟的细节都给你封装好了。 你我共勉。 什么看门狗机制啊,什么Lua脚本原子性啊,统统都有。

没法说。 源码剖析redisson可重入锁之加锁 在上篇中,我们基于spring boot整合redisson实现了分布式锁,接下来我会带领大家花一些时间来学习redisson如何实现各种锁,所以我们需要先从....源码剖析redisson可重入锁之释放及阻塞与非阻塞获取 有加锁自然就有解锁,本篇则将围绕锁的释放锁Lua脚本进行,再说一个,还将对阻...

咱们来看看Redisson是怎么加锁的, 我无法认同... 这源码一摆出来是不是瞬间感觉逼格就上来了?

private void lock throws InterruptedException { 
    // 获取当前线程ID
    long threadId = Thread.currentThread.getId; 
    // 尝试获取锁
    Long ttl = tryAcquire; 
    // 如果ttl为空,表示获取锁成功
    if  {
        return;
    }
    // 如果获取锁失败, 这里还有订阅机制,等待锁释放通知,比傻傻的while循环强多了
    // ...省略后续逻辑
}

捡漏。 深入分析了Redisson可重入锁的加锁流程与核心Lua脚本,从用户调用lock方法到锁的获取、等待及重入逻辑,详细解读了其内部工作机制,旨在帮助开发者更好地理解并应用Redisson分布式锁...在 Redisson 源码里,MultiLock的主要实现类是org.redisson.RedissonMultiLock.这种 整锁整放 的方式,能更好地满足某些高要求的分布式业务场景。

再说说再来唠叨两句

翻车了。 接着我们启动熟悉的JMeter来进行测试, 在开始前,我们先往redis里set一个key为stock1value为6000的键值对来表示id为1的商品有6000库存,如下:测试后来啊我就不贴了反正用了Redisson之后那速度,嗖嗖的。

图灵学院,图灵学院-李白老师,+++ 加入技术交流群:993070439,即享受以下福利: +++ 1.免费获取其他相关课程视频、课件等资料 2.免费领取内部教材 1.高并发场景库存重复扣减问题分析 2.分布式架构下如何实现Redis分布式锁上 3.分布式架构下如何实现Redis分布式锁下 4.Redis主从架构锁失效问题解析 5.基于Redisson框架实现分布式锁 适用人群:1到5年java开发经验的程序员。

总之吧,这分布式锁的水真的很深,咱们今天讲的也就是个皮毛。什么集群锁失效啊,什么RedLock算法啊,那都是后面要讲的大招。大家先把基础打牢, 图啥呢? 别到时候连个`setnx`都写不明白,那就真的尴尬了。好了今天的废话就说到这里大家赶紧去动手试试吧,别光看不练假把式啊!


提交需求或反馈

Demand feedback