当前位置:

首页 > 编程开发 > 使用Redis实现分布式锁的方法

使用Redis实现分布式锁的方法

Redis Assistant
Redis Assistant

Redis Assistant 是一款为Redis设计的高效可视化管理与监控工具,提供多种操作模式,高效处理服务器数据。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

立即下载
¥19
Windows/macOS/Linux 2026-08-22
正版软件 Windows macOS软件 Linux 数据库管理工具

啥是分布式锁?我们学习Java都知道锁的概念,例如基于JVM实现的同步锁synchronized,以及jdk提供的一套代码级别的锁机制lock,我们在并发编程中会经常用这两种锁去保证代码在多线程环境下运行的正确性。但是这些锁机制在分布式场景下是不适用的,原因是在分布式业务场景下,我们的代码都是跑在不同的JVM甚至是不同的机器上,synchronized和lock只能在同一个JVM环境下起作用。所以这时候就需要用到分布式锁了。例如,现在有个场景就是整点抢消费券(疫情的原因,支付宝最近在8点、12点整点开放抢

本文目录

    啥是分布式锁?

    我们学习 Java 都知道锁的概念,例如基于 JVM 实现的同步锁 synchronized,以及 jdk 提供的一套代码级别的锁机制 lock,我们在并发编程中会经常用这两种锁去保证代码在多线程环境下运行的正确性。但是这些锁机制在分布式场景下是不适用的,原因是在分布式业务场景下,我们的代码都是跑在不同的JVM甚至是不同的机器上,synchronized 和 lock 只能在同一个 JVM 环境下起作用。所以这时候就需要用到分布式锁了。

    例如,现在有个场景就是整点抢消费券(疫情的原因,支付宝最近在8点、12点整点开放抢消费券),消费券有一个固定的量,先到先得,抢完就没了,线上的服务都是部署多个的,大致架构如下:

     使用Redis实现分布式锁的方法

     所以这个时候我们就得用分布式锁来保证共享资源的访问的正确性。

    回到顶部

    为什么要用分布式锁嗯?

    假设不使用分布式锁,我们看看 synchronized 能不能保证?其实是不能的,我们来演示一下。

    下面我写了一个简单的 springboot 项目来模拟这个抢消费券的场景,代码很简单,大致意思是先从 Redis 获取剩余消费券数,然后判断大于0,则减一模拟被某个用户抢到一个,然后减一后再修改 Redis 的剩余消费券数量,打印扣减成功,剩余还有多少,否则扣减失败,就没抢到。整块代码被 synchronized 包裹,Redis 设置的库存数量为50。

    //假设库存编号是00001private String key = "stock:00001";
    @Autowiredprivate StringRedisTemplate stringRedisTemplate;/**
     * 扣减库存 synchronized同步锁*/@RequestMapping("/deductStock")public String deductStock(){    synchronized (this){        //获取当前库存
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));        if(stock>0){            int afterStock = stock-1;
                stringRedisTemplate.opsForValue().set(key,afterStock+"");//修改库存
                System.out.println("扣减库存成功,剩余库存"+afterStock);
            }else {
                System.out.println("扣减库存失败");
            }
        }    return "ok";
    }

    然后启动两个springboot项目,端口分别为8080,8081,然后在nginx里配置负载均衡

    upstream redislock{
        server 127.0.0.1:8080;
        server 127.0.0.1:8081;
    }
    server {
        listen       80;
        server_name  127.0.0.1;
        location / {
            root   html;
            index  index.html index.htm;
            proxy_pass http://redislock;    }
    }

     然后用jmeter压测工具进行测试

     使用Redis实现分布式锁的方法

     使用Redis实现分布式锁的方法

    然后我们看一下控制台输出,可以看到我们运行的两个web实例,很多同样的消费券被不同的线程抢到,证明synchronized在这样的情况下是不起作用的,所以就需要使用分布式锁来保证资源的正确性。

     使用Redis实现分布式锁的方法

    回到顶部

    如何用Redis实现分布式锁?

    在实现分布式锁之前,我们先考虑如何实现,以及都要实现锁的哪些功能。

    1、分布式特性(部署在多个机器上的实例都能够访问这把锁)

    2、排他性(同一时间只能有一个线程持有锁)

    3、超时自动释放的特性(持有锁的线程需要给定一定的持有锁的最大时间,防止线程死掉无法释放锁而造成死锁)

    4、...

    基于以上列出的分布式锁需要拥有的基本特性,我们思考一下使用Redis该如何实现?

    1、第一个分布式的特性Redis已经支持,多个实例连同一个Redis即可

    2、第二个排他性,也就是要实现一个独占锁,可以使用Redis的setnx命令实现

    3、第三个超时自动释放特性,Redis可以针对某个key设置过期时间

    4、执行完毕释放分布式锁

    科普时间

    Redis Setnx 命令

    Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值

    语法

    redis Setnx 命令基本语法如下:

    redis 127.0.0.1:6379> SETNX KEY_NAME VALUE

    可用版本:>= 1.0.0

    返回值:设置成功,返回1, 设置失败,返回0

    @RequestMapping("/stock_redis_lock")public String stock_redis_lock(){    //底层使用setnx命令
        Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key, "true");
        stringRedisTemplate.expire(lock_key,10, TimeUnit.SECONDS);//设置过期时间10秒
        if (!aTrue) {//设置失败则表示没有拿到分布式锁
            return "error";//这里可以给用户一个友好的提示    }    //获取当前库存
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));    if(stock>0){        int afterStock = stock-1;
            stringRedisTemplate.opsForValue().set(key,afterStock+"");
            System.out.println("扣减库存成功,剩余库存"+afterStock);
        }else {
            System.out.println("扣减库存失败");
        }
        stringRedisTemplate.delete(lock_key);//执行完毕释放分布式锁
        return "ok";
    }

    仍然设置库存数量为50,我们再用jmeter测试一下,把jmeter的测试地址改为127.0.0.1/stock_redis_lock,同样的设置再来测一次。

     使用Redis实现分布式锁的方法

    测试了5次没有出现脏数据,把发送时间改为0,测了5次也没问题,然后又把线程数改为600,时间为0 ,循环4次,测了几次也是正常的。

    上面实现分布式锁的代码已经是一个较为成熟的分布式锁的实现了,对大多数软件公司来说都已经满足需求了。但是上面代码还是有优化的空间,例如:

    1)上面的代码我们是没有考虑异常情况的,实际情况下代码没有这么简单,可能还会有别的很多复杂的操作,都有可能会出现异常,所以我们释放锁的代码需要放在finally块里来保证即使是代码抛异常了释放锁的代码他依然会被执行。

    2)还有,你有没有注意到,上面我们的分布式锁的代码的获取和设置过期时间的代码是两步操作第4行和第5行,即非原子操作,就有可能刚执行了第4行还没来得及执行第5行这台机器挂了,那么这个锁就没有设置超时时间,其他线程就一直无法获取,除非人工干预,所以这是一步优化的地方,Redis也提供了原子操作,那就是SET key value EX seconds  NX

    科普时间

    SET key value [EX seconds] [PX milliseconds] [NX|XX]  将字符串值 value 关联到 key

    可选参数

    从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:

    • EX second :设置键的过期时间为 second 秒。SET key value EX second 效果等同于 SETEX key second value

    • PX millisecond :设置键的过期时间为 millisecond 毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecond value

    • NX :只在键不存在时,才对键进行设置操作。SET key value NX 效果等同于 SETNX key value

    • XX :只在键已经存在时,才对键进行设置操作

    SpringBoot的StringRedisTemplate也有对应的方法实现,如下代码: 

    //假设库存编号是00001private String key = "stock:00001";private String lock_key = "lock_key:00001";
    @Autowiredprivate StringRedisTemplate stringRedisTemplate;
    @RequestMapping("/stock_redis_lock")public String stock_redis_lock() {
        String uuid = UUID.randomUUID().toString();    try {        //原子的设置key及超时时间
            Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key, "true", 30, TimeUnit.SECONDS);        if (!aTrue) {            return "error";
            }        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));        if (stock > 0) {            int afterStock = stock - 1;
                stringRedisTemplate.opsForValue().set(key, afterStock + "");
                System.out.println("扣减库存成功,剩余库存" + afterStock);
            } else {
                System.out.println("扣减库存失败");
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        } finally {        //避免死锁
            if (uuid.equals(stringRedisTemplate.opsForValue().get(lock_key))) {
                stringRedisTemplate.delete(lock_key);
            }
        }    return "ok";
    }

    这样实现是否就完美了呢?嗯,对于并发量要求不高或者非大并发的场景的话这样实现已经可以了。但是对于抢购 ,秒杀这样的场景,当流量很大,这时候服务器网卡、磁盘IO、CPU负载都可能会达到极限,那么服务器对于一个请求的的响应时间势必变得比正常情况下慢很多,那么假设就刚才设置的锁的超时时间为10秒,如果某一个线程拿到锁之后因为某些原因没能在10秒内执行完毕锁就失效了,这时候其他线程就会抢占到分布式锁去执行业务逻辑,然后之前的线程执行完了,会去执行 finally 里的释放锁的代码就会把正在占有分布式锁的线程的锁给释放掉,实际上刚刚正在占有锁的线程还没执行完,那么其他线程就又有机会获得锁了...这样整个分布式锁就失效了,将会产生意想不到的后果。如下图模拟了这个场景。

    使用Redis实现分布式锁的方法

    所以这个问题总结一下,就是因为锁的过期时间设置的不合适或因为某些原因导致代码执行时间大于锁过期时间而导致并发问题以及锁被别的线程释放,以至于分布式锁混乱。在简单的说就是两个问题,1)自己的锁被别人释放 2)锁超时无法续时间。

    第一个问题很好解决,在设置分布式锁时,我们在当前线程中生产一个唯一串将value设置为这个唯一值,然后在finally块里判断当前锁的value和自己设置的一样时再去执行delete,如下:

    String uuid = UUID.randomUUID().toString();try {    //原子的设置key及超时时间,锁唯一值
        Boolean aTrue = stringRedisTemplate.opsForValue().setIfAbsent(lock_key,uuid,30,TimeUnit.SECONDS);    //...} finally {    //是自己设置的锁再执行delete
        if(uuid.equals(stringRedisTemplate.opsForValue().get(lock_key))){
            stringRedisTemplate.delete(lock_key);//避免死锁    }
    }

    问题一解决了(设想一下上述代码还有什么问题,一会儿讲),那锁的超时时间就很关键了,不能太大也不能太小,这就需要评估业务代码的执行时间,比如设置个10秒,20秒。即使是你的锁设置了合适的超时时间,也避免不了可能会发生上述分析的因为某些原因代码没在正常评估的时间内执行完毕,所以这时候的解决方案就是给锁续超时时间。大致思路就是,业务线程单独起一个分线程,定时去监听业务线程设置的分布式锁是否还存在,存在就说明业务线程还没执行完,那么就延长锁的超时时间,若锁已不存在则业务线程执行完毕,然后就结束自己。

    “锁续命”的这套逻辑属实有点复杂啊,要考虑的问题太多了,稍不注意就会有bug。不要看上面实现分布式锁的代码没有几行,就认为实现起来很简单,如果说自己去实现的时候没有实际高并发的经验,肯定也会踩很多坑,例如,

    1)锁的设置和过期时间的设置是非原子操作的,就可能会导致死锁。

    2)还有上面遗留的一个,在finally块里判断锁是否是自己设置的,是的话再删除锁,这两步操作也不是原子的,假设刚判断完为true服务就挂了,那么删除锁的代码不会执行,就会造成死锁,即使是设置了过期时间,在没过期这段时间也会死锁。所以这里也是一个注意的点,要保证原子操作的话,Redis提供了执行Lua脚本的功能来保证操作的原子性,具体怎么使用不再展开。

    所以,“锁续命”的这套逻辑实现起来还是有点复杂的,好在市面上已经有现成的开源框架帮我们实现了,那就是Redisson。

    回到顶部

    Redisson分布式锁的实现原理

    使用Redis实现分布式锁的方法

    实现原理:

    1、首先Redisson会尝试进行加锁,加锁的原理也是使用类似Redis的setnx命令原子的加锁,加锁成功的话其内部会开启一个子线程

    2、子线程主要负责监听,其实就是一个定时器,定时监听主线程是否还持有锁,持有则将锁的时间延时,否则结束线程

    3、如果加锁失败则自旋不断尝试加锁

    4、执行完代码主线程主动释放锁

    那我们看一下使用后Redisson后的代码是什么样的。

    1、首先在pom.xml文件添加Redisson的maven坐标

    使用Redis实现分布式锁的方法

    
        org.redisson
        redisson
        3.12.5

    使用Redis实现分布式锁的方法

    2、我们要拿到Redisson的这个对象,如下配置Bean

    使用Redis实现分布式锁的方法

    @SpringBootApplicationpublic class RedisLockApplication {    public static void main(String[] args) {
            SpringApplication.run(RedisLockApplication.class, args);
        }
        @Bean    public Redisson redisson(){
            Config config = new Config();
            config.useSingleServer().setAddress("redis://localhost:6379")
                    .setDatabase(0);        return (Redisson) Redisson.create(config);
        }
    }

    使用Redis实现分布式锁的方法

    3、然后我们获取Redisson的实例,使用其API进行加锁释放锁操作

    使用Redis实现分布式锁的方法

    //假设库存编号是00001private String key = "stock:00001";private String lock_key = "lock_key:00001";
    @Autowiredprivate StringRedisTemplate stringRedisTemplate;/**
     * 使用Redisson实现分布式锁
     * @return
     */@RequestMapping("/stock_redisson_lock")public String stock_redisson_lock() {
        RLock redissonLock = redisson.getLock(lock_key);    try {
            redissonLock.lock();        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get(key));        if (stock > 0) {            int afterStock = stock - 1;
                stringRedisTemplate.opsForValue().set(key, afterStock + "");
                System.out.println("扣减库存成功,剩余库存" + afterStock);
            } else {
                System.out.println("扣减库存失败");
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        } finally {
            redissonLock.unlock();
        }    return "ok";
    }

    使用Redis实现分布式锁的方法

    看这个Redisson的分布式锁提供的API是不是非常的简单?就像Java并发变成里AQS那套Lock机制一样,如下获取一把RedissonLock

    RLock redissonLock = redisson.getLock(lock_key);

    默认返回的是RedissonLock的对象,该对象实现了RLock接口,而RLock接口继承了JDK并发编程报包里的Lock接口

    使用Redis实现分布式锁的方法

    在使用Redisson加锁时,它也提供了很多API,如下

    使用Redis实现分布式锁的方法

    现在我们选择使用的是最简单的无参lock方法,简单的点进去跟一下看看他的源码,我们找到最终的执行加锁的代码如下:

    使用Redis实现分布式锁的方法

    我们可以看到其底层使用了Lua脚本来保证原子性,使用Redis的hash结构实现的加锁,以及可重入锁。

    比我们自己实现分布式锁看起来还要简单,但是我们自己写的锁功能他都有,我们没有的他也有。比如,他实现的分布式锁是支持可重入的,也支持可等待,即尝试等待一定时间,没拿到锁就返回false。上述代码中的redissonLock.lock();是一直等待,内部自旋尝试加锁。

    Distributed Java locks and synchronizers 

    Lock 

    FairLock 

    MultiLock 

    RedLock 

    ReadWriteLock 

    Semaphore 

    PermitExpirableSemaphore 

    CountDownLatch

    redisson.org

    Redisson提供了丰富的API,内部运用了大量的Lua脚本保证原子操作,篇幅原因redisson实现锁的代码暂不分析了。

    注意:在上述示例代码中,为了方便演示,查询redis库存、修改库存并非原子操作,实际这两部操作也得保证原子行,可以用redis自带的Lua脚本功能去实现

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发
    相关文章 更多
    php72w redis,docker php7安装php-redis
    php72w redis,docker php7安装php-redis

    在Docker环境下为PHP安装Redis扩展:手动编译需下载源码、安装phpize、配置编译并安装,生成redis.so并在php.d目录新建redis.ini加载,重启PHP-FPM;也可直接yum安装php72w-pecl-redis,免手动编译。

    php 如何配置 redis,php 如何设置redis
    php 如何配置 redis,php 如何设置redis

    配置PHP的Redis扩展:先安装Redis服务,从GitHub下载phpredis源码,用phpize编译安装,修改php.ini添加extension=redis.so,重启php-fpm或Apache,通过phpinfo验证并连接Redis。

    c++如何将结构体序列化为Redis兼容的RESP协议格式【深度】
    c++如何将结构体序列化为Redis兼容的RESP协议格式【深度】

    C++结构体无法直接序列化为RESP协议,需手动将字段映射为*N数组嵌套$lenbulkstring形式。整数需转换为字符串并加:前缀,空字符串用$0\r\n,null用$-1\r\n。长度计算需精确到字节数,特别注意二进制安全,同时确保字段顺序与结构体定义一致。

    如何在Golang架构中部署Redict作为高能数据临时存储中转站
    如何在Golang架构中部署Redict作为高能数据临时存储中转站

    Redis作为跨服务临时状态枢纽,需按消息队列、缓存和原子状态三重角色设计。选型用List实现任务分发及延迟重试,Stream用于事件广播与位点回溯。go-redis/v8需配置MinIdleConns、MaxRetries及超时。数据写入应JSON序列化并带type标签、设合理TTL、将关键字段提至顶层。消费通过原子GET+DEL或processedkey

    TP8.0 高并发下库存超卖?Redis 原子递减结合数据库乐观锁控制
    TP8.0 高并发下库存超卖?Redis 原子递减结合数据库乐观锁控制

    在高并发秒杀场景下,利用Redis的减库存原子操作预扣减库存拦截超卖请求,再通过数据库乐观锁(版本号与库存校验)确保最终一致性,异常时回滚Redis库存并借助Lua脚本保证原子性,从而有效防止库存超卖。

    如何在 Go 中利用 go-redis 执行 Lua 脚本优化逻辑
    如何在 Go 中利用 go-redis 执行 Lua 脚本优化逻辑

    在Go中使用go-redis执行Lua脚本可借助原子性解决竞态问题,通过redis.Script封装脚本并自动处理SHA1缓存与重试。需注意KEYS必须为非空切片且所有键在Cluster模式下落在同一slot,ARGV以字符串传入并由tonumber转换。返回值需先检查错误再调用Val(),避免panic。

    TP6.0 基于 Redis SetNX 实现的简单互斥锁机制【并发】
    TP6.0 基于 Redis SetNX 实现的简单互斥锁机制【并发】

    先看一个典型场景:电商秒杀、库存扣减、定时任务重复调度——这些并发冲突在 TP6 应用里屡见不鲜。用 Redis 的 SETNX 做互斥锁,确实是轻量又直接的方案,但“能用”和“稳妥”之间,隔着三层硬功夫:原子加锁、身份校验、合理超时。这三条,差一条就翻车。 很多人第一次上手,直接写 SETNX 加

    TP6.0 如何处理高并发抢购?Redis 原子锁与队列削峰详解【秒杀】
    TP6.0 如何处理高并发抢购?Redis 原子锁与队列削峰详解【秒杀】

    高并发抢购中,Redis的DECR原子操作可安全扣库存;配合think-queue异步削峰,接口仅扣库存和推消息;分布式锁需用SETNXEX加唯一标识,释放时用Lua脚本校验;成功资格后须以数据库事务双写为准,失败时补回Redis库存。

    Golang 实现基于 Redis 的延时任务队列
    Golang 实现基于 Redis 的延时任务队列

    基于Redis的ZSET实现延时队列需解决并发重复消费、失败重试混乱、精度不足及内存爆炸等问题。通过Lua脚本保证取删原子性,使用毫秒级时间戳,对临时错误采用指数退避重试,永久错误记入独立哈希表,并限制最大延迟、定期清理过期任务、使用紧凑序列化格式。

    RedisInsight怎么修改Key的过期时间 Redis操作方法【步骤】
    RedisInsight怎么修改Key的过期时间 Redis操作方法【步骤】

    操作 Redis 的时候,改个 Key 的过期时间其实是个高频场景。特别是做运维排查或者临时调试那会儿,谁也不想反复切命令行窗口、背那些命令参数。好在 RedisInsight 这个图形工具把这事儿做得很顺手——三种方式任选,总有一种合你胃口。 在动手前,先确认一下 Key 当前的 TTL 状态是个

    查看更多
    精品专题 更多
    装机必备
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows
    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程
    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    Mac软件 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    Aperty macOS版
    Aperty macOS版
    macOS

    Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

    WINDOWS 更多
    讯飞智作 网页版
    讯飞智作 网页版
    Windows/ macOS/ Linux

    讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

    火山引擎 网页版
    火山引擎 网页版
    Windows/ macOS/ Linux

    火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

    PixTV 网页版
    PixTV 网页版
    Windows

    PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。

    即将离开本站
    您即将前往第三方网站,请确认是否继续?