Redis 实现分布式锁

news2024/12/14 5:21:38

单实例条件下的分布式锁

-- 加锁操作
-- KEYS[1]: 锁的键(lock_key)
-- ARGV[1]: 当前客户端的标识(client_id)
-- ARGV[2]: 锁的过期时间(毫秒)

if (redis.call('EXISTS', KEYS[1]) == 0) then
    -- 如果锁不存在,则进行加锁
    redis.call('SET', KEYS[1], ARGV[1])
    redis.call('PEXPIRE', KEYS[1], ARGV[2])
    return 1
elseif (redis.call('GET', KEYS[1]) == ARGV[1]) then
    -- 如果锁已存在且是当前客户端持有的,则续期
    redis.call('PEXPIRE', KEYS[1], ARGV[2])
    return 1
else
    -- 如果锁已存在且不是当前客户端持有的,返回失败
    return 0
end
-- 解锁操作
-- KEYS[1]: 锁的键(lock_key)
-- ARGV[1]: 当前客户端的标识(client_id)

if (redis.call('GET', KEYS[1]) == ARGV[1]) then
    -- 如果当前客户端持有锁,则解锁
    redis.call('DEL', KEYS[1])
    return 1
else
    -- 如果不是当前客户端持有锁,返回失败
    return 0
end

使用 Redisson 执行 Lua 脚本

@Service
public class RedisLockService {

    @Autowired
    private RedissonClient redissonClient;

    // 加锁操作
    public boolean lock(String lockKey, String clientId, long expireTime) {
        String script =
            "if (redis.call('EXISTS', KEYS[1]) == 0) then " +
            "    redis.call('SET', KEYS[1], ARGV[1]); " +
            "    redis.call('PEXPIRE', KEYS[1], ARGV[2]); " +
            "    return 1; " +
            "elseif (redis.call('GET', KEYS[1]) == ARGV[1]) then " +
            "    redis.call('PEXPIRE', KEYS[1], ARGV[2]); " +
            "    return 1; " +
            "else " +
            "    return 0; " +
            "end";
        
        Long result = (Long) redissonClient.getScript().eval(
            RScript.Mode.READ_WRITE,
            script,
            RScript.ReturnType.INTEGER,
            java.util.Collections.singletonList(lockKey),
            clientId,
            String.valueOf(expireTime)
        );

        return result != null && result == 1;
    }

    // 解锁操作
    public boolean unlock(String lockKey, String clientId) {
        String script =
            "if (redis.call('GET', KEYS[1]) == ARGV[1]) then " +
            "    redis.call('DEL', KEYS[1]); " +
            "    return 1; " +
            "else " +
            "    return 0; " +
            "end";
        
        Long result = (Long) redissonClient.getScript().eval(
            RScript.Mode.READ_WRITE,
            script,
            RScript.ReturnType.INTEGER,
            java.util.Collections.singletonList(lockKey),
            clientId
        );

        return result != null && result == 1;
    }
}

使用 lua 脚本实现可重入分布式锁
加锁脚本需要检测当前锁的持有者是否是同一个客户端。如果是同一个客户端,则增加计数器,否则返回失败。

-- 加锁操作
-- KEYS[1]: 锁的键(lock_key)
-- ARGV[1]: 当前客户端的标识(client_id)
-- ARGV[2]: 锁的过期时间(毫秒)

-- 检查锁的持有者
local lockOwner = redis.call('HGET', KEYS[1], 'owner')
if lockOwner == false then
    -- 如果锁不存在,则初始化锁的持有者和计数器
    redis.call('HSET', KEYS[1], 'owner', ARGV[1])
    redis.call('HSET', KEYS[1], 'count', 1)
    redis.call('PEXPIRE', KEYS[1], ARGV[2])
    return 1
elseif lockOwner == ARGV[1] then
    -- 如果锁已存在且是当前客户端持有的,则增加计数器
    local count = redis.call('HINCRBY', KEYS[1], 'count', 1)
    redis.call('PEXPIRE', KEYS[1], ARGV[2])
    return count
else
    -- 如果锁已存在且不是当前客户端持有的,返回失败
    return 0
end

解锁脚本需要检测当前锁的持有者是否是当前客户端。如果是,则减少计数器,当计数器减到 0 时,才释放锁。

-- 解锁操作
-- KEYS[1]: 锁的键(lock_key)
-- ARGV[1]: 当前客户端的标识(client_id)

-- 检查锁的持有者
local lockOwner = redis.call('HGET', KEYS[1], 'owner')
if lockOwner == ARGV[1] then
    -- 如果是当前客户端持有锁,则减少计数器
    local count = redis.call('HINCRBY', KEYS[1], 'count', -1)
    if count == 0 then
        -- 如果计数器为 0,则删除锁
        redis.call('DEL', KEYS[1])
    else
        -- 如果计数器不为 0,则更新过期时间
        redis.call('PEXPIRE', KEYS[1], ARGV[2])
    end
    return count
else
    -- 如果不是当前客户端持有锁,返回失败
    return -1
end

使用 Redisson 执行 Lua 脚本

@Service
public class ReentrantLockService {

    @Autowired
    private RedissonClient redissonClient;

    // 加锁操作
    public boolean lock(String lockKey, String clientId, long expireTime) {
        String script =
            "local lockOwner = redis.call('HGET', KEYS[1], 'owner') " +
            "if lockOwner == false then " +
            "    redis.call('HSET', KEYS[1], 'owner', ARGV[1]) " +
            "    redis.call('HSET', KEYS[1], 'count', 1) " +
            "    redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
            "    return 1 " +
            "elseif lockOwner == ARGV[1] then " +
            "    local count = redis.call('HINCRBY', KEYS[1], 'count', 1) " +
            "    redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
            "    return count " +
            "else " +
            "    return 0 " +
            "end";

        Long result = (Long) redissonClient.getScript().eval(
            RScript.Mode.READ_WRITE,
            script,
            RScript.ReturnType.INTEGER,
            java.util.Collections.singletonList(lockKey),
            clientId,
            String.valueOf(expireTime)
        );

        return result != null && result > 0;
    }

    // 解锁操作
    public boolean unlock(String lockKey, String clientId, long expireTime) {
        String script =
            "local lockOwner = redis.call('HGET', KEYS[1], 'owner') " +
            "if lockOwner == ARGV[1] then " +
            "    local count = redis.call('HINCRBY', KEYS[1], 'count', -1) " +
            "    if count == 0 then " +
            "        redis.call('DEL', KEYS[1]) " +
            "    else " +
            "        redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
            "    end " +
            "    return count " +
            "else " +
            "    return -1 " +
            "end";

        Long result = (Long) redissonClient.getScript().eval(
            RScript.Mode.READ_WRITE,
            script,
            RScript.ReturnType.INTEGER,
            java.util.Collections.singletonList(lockKey),
            clientId,
            String.valueOf(expireTime)
        );

        return result != null && result >= 0;
    }
}

使用 Redisson 库实现分布式锁

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
              .setAddress("redis://localhost:6379"); // Redis 单实例地址
        return Redisson.create(config);
    }
}
@Service
public class DistributedLockService {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 加锁操作
     *
     * @param lockKey 锁的键
     * @param leaseTime 锁的过期时间(秒)
     * @return 是否加锁成功
     */
    public boolean lock(String lockKey, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试加锁,最多等待 10 秒,锁的租约时间为指定的 leaseTime
            return lock.tryLock(10, leaseTime, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    /**
     * 解锁操作
     *
     * @param lockKey 锁的键
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}
  • 使用 Redisson 非常方便,它自动处理了分布式锁的各种边缘情况,例如锁的过期时间、可重入性和网络分区问题。

  • RLock 默认是可重入锁,这意味着同一个客户端(同一线程)可以多次获取同一把锁,所以上面的代码也可以作为可重入锁的代码。

  • 如果业务逻辑涉及多线程或长时间操作,Redisson 的内置锁机制能有效管理锁的状态并防止死锁。

    • 在使用 Redis 设置分布式锁时,如果任务在锁的过期时间快到了但尚未完成,可以采取以下几种策略来处理:
      • 设置合理的过期时间:在任务开始时,根据预估的处理时间合理设置锁的过期时间。如果任务的处理时间通常较长,可以考虑设置一个较长的过期时间,以减少续期的需要。
      • 使用线程守护机制:在业务逻辑执行时,可以启动一个线程或使用定时器,在任务执行过程中,可以定期(如每隔一定时间)检查锁的状态,并在必要时延长锁的过期时间。这可以通过调用 PEXPIRE 命令实现。
      • 使用 Redisson 加锁可以做到自动续期。

集群条件下的分布式锁
使用 Redisson 对 redis 集群进行加锁,具有以下特点:

  • 高可靠性:Redisson 能够在 Redis 集群架构下使用主从复制模式,实现锁的高可靠性。
    • 这里的高可靠性是限于同一个节点的主从节点范围内的,如果加锁的主从节点都挂了话,那么锁就没有了。
  • 易用性:Redisson 封装了复杂的锁定机制,开发者只需要简单的 API 调用即可实现分布式锁。
  • 内置支持:Redisson 具有内置的锁过期机制和防止死锁的机制,避免了由于异常导致的锁未释放问题。
  • 可重入性:Redisson 对 Redis 集群添加的分布式锁是支持可重入性的。这意味着在同一个线程中,获取锁的线程可以多次获取同一个锁,而无需担心死锁或冲突。
    • 这里可重入性的参考维度是客户端,而不是线程

在 Redis 集群架构下,需要配置 Redisson 客户端连接 Redis 集群。可以在 Spring Boot 项目的配置文件中使用 application.yml 进行配置。

spring:
  redis:
    cluster:
      nodes:
        - 127.0.0.1:6379
        - 127.0.0.1:6380
        - 127.0.0.1:6381
redisson:
  config: |
    clusterServersConfig:
      idleConnectionTimeout: 10000
      connectTimeout: 10000
      timeout: 3000
      retryAttempts: 3
      retryInterval: 1500
      scanInterval: 2000
      nodeAddresses:
        - redis://127.0.0.1:6379
        - redis://127.0.0.1:6380
        - redis://127.0.0.1:6381
      password: null
      subscriptionsPerConnection: 5
      clientName: null
      idleConnectionTimeout: 10000
      pingTimeout: 1000
      keepAlive: true
      tcpNoDelay: true
      dnsMonitoringInterval: 5000

创建一个配置类,初始化 RedissonClient:

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        
        // 配置 Redis 集群节点
        config.useClusterServers()
                .addNodeAddress("redis://127.0.0.1:6379", "redis://127.0.0.1:6380", "redis://127.0.0.1:6381")
                .setScanInterval(2000) // 集群节点扫描间隔
                .setRetryAttempts(3)    // 重试次数
                .setRetryInterval(1500) // 重试间隔
                .setConnectTimeout(10000) // 连接超时
                .setTimeout(3000);     // 命令等待响应超时

        return Redisson.create(config);
    }
}

使用 Redisson 分布式锁

@Service
public class MyService {

    @Autowired
    private RedissonClient redissonClient;

    public void doSomethingWithLock() {
        // 获取可重入锁
        RLock lock = redisson.getLock("myReentrantLock");

        try {
            // 尝试获取锁,等待最多 10 秒,锁定时间为 5 秒
            if (lock.tryLock(10, 5, TimeUnit.SECONDS)) {
                try {
                    // 第一次加锁成功,执行业务逻辑
                    System.out.println("Lock acquired for the first time!");

                    // 再次获取同一把锁(可重入)
                    lock.lock();
                    System.out.println("Lock acquired again (reentrant)!");
                    
                    // 业务逻辑处理
                    // ...
                    
                } finally {
                    // 释放锁(可重入锁的计数器递减)
                    lock.unlock();
                    System.out.println("Lock released once!");

                    // 再次释放锁
                    lock.unlock();
                    System.out.println("Lock fully released!");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

可以使用 Lua 脚本在 Redis 集群中实现分布式锁。Lua 脚本在 Redis 中是原子执行的,因此能够有效地确保分布式锁的原子性和一致性。通过 Lua 脚本,可以精确控制锁的获取和释放逻辑,从而避免在高并发环境下的竞态条件。

不过在 Redis 集群架构下,使用 Lua 脚本进行分布式锁需要特别注意一些事项,因为 Redis 集群的数据是分片存储的,每个键可能位于不同的分片节点上。这会导致以下几点挑战:

  • Redis 集群不支持跨节点事务:在 Redis 集群模式下,如果 Lua 脚本涉及多个键,这些键必须位于同一个分片上才能保证 Lua 脚本的原子性执行。
  • 哈希标签(Hash Tag)解决方案:为了确保多个键位于同一分片,可以使用 哈希标签。哈希标签是指在键名中使用 {} 包裹的部分,Redis 会对这个部分进行一致哈希计算,将具有相同哈希标签的键存储在同一个分片上。

Redis (单例或者集群)添加可重入性分布式锁时,参考维度通常是客户端,而不是线程。这是因为在 Redis 的上下文中,分布式锁是针对客户端的,而不是针对单个线程。

  • 单线程模型:Redis 是单线程的,所有的命令在一个线程中处理。因此,Redis 处理的所有操作都是在一个事件循环中执行的。即使在多线程的应用程序中,所有对 Redis 的请求实际上是通过同一个客户端连接发送的。
  • 客户端标识:在分布式环境中,锁的持有者通常是一个客户端,而不是单个线程。一个客户端可以有多个线程在操作,但 Redis 只关心哪个客户端持有锁。
  • 锁的设计:在实现可重入锁时,需要一个机制来跟踪锁的持有者。这个持有者应该是客户端的标识符(例如 UUID 或其他唯一标识符),以便确保同一个客户端可以多次获取锁,而不会出现死锁。
  • 多线程和多进程:如果一个客户端的多个线程或进程尝试获取同一个锁,可以通过使用相同的客户端标识符来实现可重入性。这种设计方式允许同一客户端的不同线程安全地获取和释放锁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2259124.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解决navicat 导出excel数字为科学计数法问题

一、原因分析 用程序导出的csv文件,当字段中有比较长的数字字段存在时,在用excel软件查看csv文件时就会变成科学技术法的表现形式。 其实这个问题跟用什么语言导出csv文件没有关系。Excel显示数字时,如果数字大于12位,它会自动转化…

C++3--内联函数、auto

1.内联函数 1.1概念 以inline修饰的函数叫做内联函数,编译时C编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序的效率 如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函…

AES 与 SM4 加密算法:深度解析与对比

🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,…

视频怎么转音频mp3?5种视频转音频的方法

在视频剪辑时,将视频中的音频提取出来并转换为MP3格式已成为许多人的需求。无论是为了制作音乐播放列表、剪辑音频片段,还是为了在其他设备上更方便地播放,将视频转换为音频MP3都显得尤为重要。下面将介绍五种实用的方法,帮助你轻…

Maven学习(传统Jar包管理、Maven依赖管理(导入坐标)、快速下载指定jar包)

目录 一、传统Jar包管理。 (1)基本介绍。 (2)传统的Jar包导入方法。 1、手动寻找Jar包。并放置到指定目录下。 2、使用IDEA的库管理功能。 3、配置环境变量。 (3)传统的Jar包管理缺点。 二、Maven。 &#…

【机器学习】分类器

在机器学习(Machine Learning,ML)中,分类器泛指算法或模型,用于将输入数据分为不同的类别或标签。分类器是监督学习的一部分,它依据已知的数据集中的特征和标签进行训练,并根据这些学习到的知识对新的未标记数据进行分…

uni-app在image上绘制点位并回显

在 Uni-app 中绘制多边形可以通过使用 Canvas API 来实现。Uni-app 是一个使用 Vue.js 开发所有前端应用的框架,同时支持编译为 H5、小程序等多个平台。由于 Canvas 是 H5 和小程序中都支持的 API,所以通过 Canvas 绘制多边形是一个比较通用的方法。 1.…

【机器学习与数据挖掘实战】案例01:基于支持向量回归的市财政收入分析

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈机器学习与数据挖掘实战 ⌋ ⌋ ⌋ 机器学习是人工智能的一个分支,专注于让计算机系统通过数据学习和改进。它利用统计和计算方法,使模型能够从数据中自动提取特征并做出预测或决策。数据挖掘则是从大型数…

Spire.PDF for .NET【页面设置】演示:向 PDF 文档添加页码

在 PDF 文档中添加页码不仅实用,而且美观,因为它提供了类似于专业出版材料的精美外观。无论您处理的是小说、报告还是任何其他类型的长文档的数字副本,添加页码都可以显著提高其可读性和实用性。在本文中,您将学习如何使用Spire.P…

【iOS】OC高级编程 iOS多线程与内存管理阅读笔记——自动引用计数(三)

目录 ARC规则 概要 所有权修饰符 __strong修饰符 __weak修饰符 __unsafe_unretained修饰符 __autoreleasing修饰符 ARC规则 概要 “引用计数式内存管理”的本质部分在ARC中并没有改变,ARC只是自动地帮助我们处理“引用计数”的相关部分。 在编译单位上可以…

An error happened while trying to locate the file on the Hub and we cannot f

An error happened while trying to locate the file on the Hub and we cannot find the requested files in the local cache. Please check your connection and try again or make sure your Internet connection is on. 关于上述comfy ui使用control net预处理器的报错问…

angular19-官方教程学习

周日了解到angular已经更新到19了,想按官方教程学习一遍,工欲善其事必先利其器,先更新工具: 安装新版版本 卸载老的nodejs 20.10.0,安装最新的LTS版本 https://nodejs.org 最新LTS版本已经是22.12.0 C:\Program File…

计算机毕业设计Python+Vue.js游戏推荐系统 Steam游戏推荐系统 Django Flask 游 戏可视化 游戏数据分析 游戏大数据 爬虫 机

温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…

上海亚商投顾:创业板指震荡调整 机器人概念股再度爆发

上海亚商投顾前言:无惧大盘涨跌,解密龙虎榜资金,跟踪一线游资和机构资金动向,识别短期热点和强势个股。 一.市场情绪 沪指昨日冲高回落,深成指、创业板指盘中跌超1%,尾盘跌幅有所收窄。机器人概念股逆势爆…

粘贴可运行:Java调用大模型(LLM) 流式Flux stream 输出;基于spring ai alibaba

在Java中,使用Spring AI Alibaba框架调用国产大模型通义千问,实现流式输出,是一种高效的方式。通过Spring AI Alibaba,开发者可以轻松地集成通义千问模型,并利用其流式处理能力,实时获取模型生成的文本。这…

【CSS in Depth 2 精译_070】11.3 利用 OKLCH 颜色值来处理 CSS 中的颜色问题(下):从页面其他颜色衍生出新颜色

当前内容所在位置(可进入专栏查看其他译好的章节内容) 第四部分 视觉增强技术 ✔️【第 11 章 颜色与对比】 ✔️ 11.1 通过对比进行交流 11.1.1 模式的建立11.1.2 还原设计稿 11.2 颜色的定义 11.2.1 色域与色彩空间11.2.2 CSS 颜色表示法 11.2.2.1 RGB…

Ajax--实现检测用户名是否存在功能

目录 (一)什么是Ajax (二)同步交互与异步交互 (三)AJAX常见应用情景 (四)AJAX的优缺点 (五)使用jQuery实现AJAX 1.使用JQuery中的ajax方法实现步骤&#xf…

【PSINS】以速度和位置作为观测量(即6维观测量)的组合导航滤波,EKF实现,提供可直接运行的MATLAB代码

原有的代码是以位置作为观测量的,这里提供位置+速度,共6维的观测量,状态量还是15维不变 文章目录 源代码运行结果PS源代码 源代码如下: % 【PSINS】位置与速度为观测的153,EKF。从速度观测的EKF153改进而来 % 2024-12-11/Ver1:位置与速度为观测量% 清空工作空间,清除命…

探索云原生安全解决方案的未来

我们是否充分意识到云端所面临的网络安全威胁? 在当今互联互通的世界中,维护安全的环境至关重要。云的出现扩大了潜在威胁的范围,因为它催生了机器身份(称为非人类身份 (NHI))及其秘密。随着组织越来越多地转向云原生…

无法正常启动此程序,因为计算机丢失wlanapi.dll

wlanapi.dll丢失怎么办?有没有什么靠谱的修复wlanapi.dll方法_无法启动此程序,因为计算机中丢失wlanapi.dll-CSDN博客 wlanapi.dll是 Windows 操作系统中的一个动态链接库文件,主要与 Windows 无线 LAN (WLAN) API 相关。该DLL提供了许多必要的函数&…