当前位置:首页 > 文章列表 > 文章 > java教程 > Redis分布式锁原理与使用详解

Redis分布式锁原理与使用详解

2025-07-14 14:57:27 0浏览 收藏

本篇文章主要是结合我之前面试的各种经历和实战开发中遇到的问题解决经验整理的,希望这篇《Redis分布式锁原理与使用教程详解》对你有很大帮助!欢迎收藏,分享给更多的需要的朋友学习~

分布式锁在分布式系统中确保同一时间只有一个进程能操作共享资源,Redis因其高性能和原子操作特性成为实现分布式锁的优选。核心实现基于SETNX命令,通过SET resource_name my_unique_id NX PX 10000设置锁,其中resource_name为资源名,my_unique_id为唯一标识,NX保证键不存在时才设置成功,PX设定过期时间防止死锁;释放锁需使用Lua脚本确保判断与删除操作的原子性,避免误删他人锁。注意事项包括合理选择my_unique_id(如UUID)、设置过期时间、考虑锁续期机制及集群模式下的Redlock算法以提高安全性。示例代码展示了Java中获取锁、执行业务逻辑及释放锁的完整流程,并强调异常处理与锁释放的可靠性。

Redis分布式锁实现原理与完整使用教程

分布式锁,说白了,就是在分布式系统里,大家得有个规矩,确保同一时间只有一个进程能操作某个共享资源。它就像是给一个热门资源发号施令,谁拿到号,谁才能进去,避免了多个人同时挤进去,把事情搞砸。Redis因为其高性能和原子操作特性,成了实现这种“发号施令”机制的绝佳选择。

Redis分布式锁实现原理与完整使用教程

解决方案

实现Redis分布式锁的核心思路,其实并不复杂,但要做到健壮可靠,就得考虑一些细节。最基础的实现,是利用Redis的SETNX(SET if Not eXists)命令。

当你需要获取锁时,尝试执行: SET resource_name my_unique_id NX PX 10000

Redis分布式锁实现原理与完整使用教程

这里面每个参数都有它的讲究:

  • resource_name:这就是你想要保护的那个共享资源的名字,比如“订单创建锁”、“库存更新锁”。
  • my_unique_id:这个非常关键!它不是随便设的,而是一个能唯一标识你当前这个请求(或者说,这个锁的持有者)的值,通常是一个UUID。为什么需要它?因为你释放锁的时候,得确保是你自己加的锁才能删,不能把别人加的锁给误删了。
  • NX:这是“Not eXists”的缩写。它的作用是,只有当resource_name这个键不存在的时候,才设置成功。这保证了原子性:如果锁已经存在,你的操作就会失败,你也就拿不到锁。
  • PX 10000:这是设置键的过期时间,单位是毫秒。这里是10秒。这个过期时间是用来防止死锁的。你想想,如果一个客户端拿到了锁,结果它崩了,或者网络断了,没来得及释放锁,那这个锁就会一直被它霸占着。有了过期时间,即使客户端挂了,锁到期也会自动释放,其他客户端就能重新获取了。

当你需要释放锁时,就不能简单地DEL resource_name了。因为可能出现这种情况:你拿到了锁,但业务逻辑执行时间太长,导致锁自动过期了。这时候,另一个客户端又拿到了这个锁。如果你再直接DEL,就误删了别人的锁。所以,释放锁必须是“判断-删除”的原子操作。Redis提供了Lua脚本来保证这种原子性:

Redis分布式锁实现原理与完整使用教程
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

这个Lua脚本的逻辑很清晰:先检查当前锁的值是不是我之前设置的那个my_unique_id。如果是,就删除它并返回1;如果不是(说明锁已经过期或者被别人拿走了),就什么也不做,返回0。将这个脚本发送给Redis执行,整个过程就是原子的,不会有中间状态被其他客户端干扰。

为什么我们需要分布式锁?

说实话,这问题我刚开始接触分布式系统的时候也挺困惑的。我们平时写代码,一个方法里用个synchronized或者ReentrantLock不就行了吗?但仔细想想,在单机应用里,这些内置的锁确实能解决多线程并发问题。可一旦你的服务不再是单机了,而是部署在好几台机器上,或者你的业务拆成了好几个微服务,它们可能同时去操作同一个数据库记录、同一个文件,或者同一个外部API,那单机锁就彻底失效了。

举个例子,一个电商网站,用户下单购买商品。如果库存只有一件,同时有两个人几乎在同一时间点击了“购买”。如果你的库存扣减逻辑没有分布式锁保护,很可能出现两个人同时判断库存充足,然后都成功扣减了库存,最终导致库存变为负数,超卖了。这在业务上是绝对不能接受的。

再比如,一个定时任务,你部署了好几个实例,为了高可用。但这个任务可能需要生成一份报表,如果多个实例同时生成,那就重复工作,甚至导致数据混乱。

这时候,分布式锁就成了必需品。它提供了一种跨进程、跨机器的协调机制,确保在任何时刻,只有一个“玩家”能够进入关键的业务逻辑区域。它不是为了替代你代码里的那些synchronized,而是为了在更高维度上,解决系统间的并发冲突。这就像是把一个单人使用的厕所,升级成一个多隔间的厕所,每个隔间都有个“有人/无人”的标志,有人占着,其他人就得等。

Redis分布式锁的实现细节与注意事项

聊到实现,除了上面提到的基本原理,还有些“坑”和“技巧”是需要特别注意的。

首先是那个my_unique_id。我个人习惯用UUID,因为它够随机,冲突的概率极低。别小看这个ID,它是你释放锁时的“身份证明”。没有它,你的锁释放逻辑就是危险的。

其次是过期时间PX的选择。这个值定多少合适?这没有标准答案,得看你的业务场景。如果你的业务逻辑执行很快,比如几百毫秒,那锁的过期时间可以设短一点,比如5秒。如果你的业务逻辑可能涉及IO操作,或者耗时较长,比如几秒钟,那过期时间就得相应调长,比如30秒甚至更久。但也不能无限长,太长就失去了防止死锁的意义。这是一个权衡:太短可能业务还没跑完锁就过期了,太长又增加了死锁的风险。

这里有个进阶的思路:锁续期(Watchdog)。有些分布式锁的实现库,比如Redisson,就内置了“看门狗”机制。它会在你获取锁后,启动一个后台线程,每隔一段时间(比如过期时间的三分之一),就去检查你是否还持有锁。如果持有,它就自动帮你延长锁的过期时间。这样,即使你的业务逻辑执行时间超过了最初设定的过期时间,只要你还在正常运行,锁就不会被意外释放。这解决了“锁过期但业务未完成”的痛点,但同时也增加了系统的复杂性。

再说说那个Lua脚本,它的重要性在于保证了“检查-删除”的原子性。没有Lua脚本,你先GETDEL,中间只要有那么一毫秒的间隔,另一个客户端就可能在这毫秒内拿到了锁,然后你再DEL,就误删了。Redis的单线程模型和Lua脚本的原子性,完美解决了这个问题。

当然,如果你的Redis是集群模式,比如主从或者哨兵模式,当主节点宕机,从节点晋升为主节点时,可能会出现一个问题:锁在旧主节点上被成功设置,但还没来得及同步到从节点,旧主节点就挂了。新的主节点上就没有这个锁,导致另一个客户端可以再次获取到锁,这就造成了“双重加锁”。对于这种极端情况,Redis官方提出了Redlock算法。它需要多个独立的Redis实例(通常是奇数个),客户端需要向半数以上的实例成功加锁才算获取成功。释放锁也需要向所有实例发送释放命令。Redlock比单实例锁复杂得多,性能也有所下降,但它在理论上提供了更高的安全性。不过,对于大多数场景,单实例的SET NX PX配合Lua脚本,已经足够应对了。是否需要Redlock,得看你的业务对一致性的要求有多高,以及你是否能接受其带来的复杂性和性能开销。我个人觉得,除非是金融级别或者对数据一致性有极高要求的场景,否则可能不需要一开始就上Redlock。

完整的Redis分布式锁使用示例

下面我用一个Java的伪代码示例,来展示一个比较完整的Redis分布式锁的使用流程。这里我们假设你已经有了一个Redis连接池(比如JedisPool)。

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class DistributedLockExample {

    private JedisPool jedisPool;
    private String lockKey; // 锁的名称
    private String requestId; // 当前请求的唯一标识
    private long expireTime; // 锁的过期时间,毫秒

    // 构造函数,传入Redis连接池、锁名和过期时间
    public DistributedLockExample(JedisPool jedisPool, String lockKey, long expireTime) {
        this.jedisPool = jedisPool;
        this.lockKey = lockKey;
        this.requestId = UUID.randomUUID().toString(); // 生成唯一ID
        this.expireTime = expireTime;
    }

    /**
     * 尝试获取锁
     * @return true表示获取成功,false表示获取失败
     */
    public boolean tryLock() {
        try (Jedis jedis = jedisPool.getResource()) {
            // SET key value NX PX milliseconds
            // NX: 只在键不存在时设置
            // PX: 设置过期时间,单位毫秒
            String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
            return "OK".equals(result); // 如果返回OK,表示获取锁成功
        } catch (Exception e) {
            System.err.println("尝试获取锁时发生异常: " + e.getMessage());
            // 实际应用中应该记录日志
            return false;
        }
    }

    /**
     * 释放锁
     * @return true表示释放成功,false表示释放失败(可能锁已过期或不属于当前请求)
     */
    public boolean releaseLock() {
        // Lua脚本,保证原子性:先判断值是否一致,再删除
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        try (Jedis jedis = jedisPool.getResource()) {
            // eval方法执行Lua脚本,KEYS[1]对应lockKey,ARGV[1]对应requestId
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            return Long.valueOf(1).equals(result); // Lua脚本返回1表示删除成功
        } catch (Exception e) {
            System.err.println("释放锁时发生异常: " + e.getMessage());
            // 实际应用中应该记录日志
            return false;
        }
    }

    // 模拟业务逻辑执行
    public void doBusinessLogic() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + " 正在执行业务逻辑...");
        // 模拟耗时操作
        TimeUnit.SECONDS.sleep(2);
        System.out.println(Thread.currentThread().getName() + " 业务逻辑执行完毕。");
    }

    public static void main(String[] args) {
        // 假设Redis运行在本地6379端口
        JedisPool pool = new JedisPool("localhost", 6379);
        String sharedResourceLock = "my_critical_resource_lock";
        long lockExpireTime = 5000; // 锁的过期时间5秒

        // 模拟多个线程同时竞争锁
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                DistributedLockExample lock = new DistributedLockExample(pool, sharedResourceLock, lockExpireTime);
                if (lock.tryLock()) {
                    try {
                        System.out.println(Thread.currentThread().getName() + " 成功获取到锁。");
                        lock.doBusinessLogic(); // 执行受保护的业务逻辑
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                        System.err.println(Thread.currentThread().getName() + " 业务逻辑被中断。");
                    } finally {
                        // 确保锁在任何情况下都被尝试释放
                        if (lock.releaseLock()) {
                            System.out.println(Thread.currentThread().getName() + " 成功释放锁。");
                        } else {
                            System.out.println(Thread.currentThread().getName() + " 未能释放锁,可能已过期或不属于我。");
                        }
                    }
                } else {
                    System.out.println(Thread.currentThread().getName() + " 未能获取到锁,资源正在被占用,稍后重试或放弃。");
                    // 实际应用中这里可以加入重试机制,比如指数退避
                }
            }, "Worker-" + i).start();
        }

        // 等待所有线程执行完毕
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            pool.close(); // 关闭连接池
            System.out.println("所有工作完成,Redis连接池关闭。");
        }
    }
}

这个示例展示了获取锁、执行业务、释放锁的完整流程。尤其要注意try-finally块的使用,它保证了无论业务逻辑是否抛出异常,释放锁的逻辑都会被执行。这在实际生产环境中至关重要。如果业务逻辑执行失败,或者中途抛出异常,而你没有在finally里释放锁,那这个锁就可能一直被霸占着,直到过期,从而影响其他请求。对于未能获取到锁的客户端,你可以选择立即返回失败,或者实现一个重试机制(比如带随机延迟的重试),直到获取成功或达到最大重试次数。这都是根据具体业务需求来定的。

理论要掌握,实操不能落!以上关于《Redis分布式锁原理与使用详解》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!

Python操作SQLite:轻量数据库入门教程Python操作SQLite:轻量数据库入门教程
上一篇
Python操作SQLite:轻量数据库入门教程
豆包AI编程教程:轻松写代码指南
下一篇
豆包AI编程教程:轻松写代码指南
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    511次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    498次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • AI边界平台:智能对话、写作、画图,一站式解决方案
    边界AI平台
    探索AI边界平台,领先的智能AI对话、写作与画图生成工具。高效便捷,满足多样化需求。立即体验!
    416次使用
  • 讯飞AI大学堂免费AI认证证书:大模型工程师认证,提升您的职场竞争力
    免费AI认证证书
    科大讯飞AI大学堂推出免费大模型工程师认证,助力您掌握AI技能,提升职场竞争力。体系化学习,实战项目,权威认证,助您成为企业级大模型应用人才。
    424次使用
  • 茅茅虫AIGC检测:精准识别AI生成内容,保障学术诚信
    茅茅虫AIGC检测
    茅茅虫AIGC检测,湖南茅茅虫科技有限公司倾力打造,运用NLP技术精准识别AI生成文本,提供论文、专著等学术文本的AIGC检测服务。支持多种格式,生成可视化报告,保障您的学术诚信和内容质量。
    560次使用
  • 赛林匹克平台:科技赛事聚合,赋能AI、算力、量子计算创新
    赛林匹克平台(Challympics)
    探索赛林匹克平台Challympics,一个聚焦人工智能、算力算法、量子计算等前沿技术的赛事聚合平台。连接产学研用,助力科技创新与产业升级。
    662次使用
  • SEO  笔格AIPPT:AI智能PPT制作,免费生成,高效演示
    笔格AIPPT
    SEO 笔格AIPPT是135编辑器推出的AI智能PPT制作平台,依托DeepSeek大模型,实现智能大纲生成、一键PPT生成、AI文字优化、图像生成等功能。免费试用,提升PPT制作效率,适用于商务演示、教育培训等多种场景。
    569次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码