Redis分布式锁及其常见问题解决方案

news2025/2/26 8:05:53

Redis 是一种内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理。由于其高性能和灵活的数据结构,Redis 被广泛应用在各种场景中,包括实现分布式锁。

分布式锁是一种在分布式系统中实现互斥访问的技术。在许多实际应用场景中,我们需要确保某些操作在同一时间只能被一个节点执行,例如更新共享资源、处理任务队列等。这时,我们就需要使用到分布式锁。

Redis 提供了一种简单有效的分布式锁实现方式。其基本思想是使用 Redis 的 SETNX 命令,这个命令可以在键不存在时设置值,如果键已存在则不做任何操作。通过这个原子操作,我们可以实现在多个节点之间的互斥访问。

然而,虽然 Redis 分布式锁的实现相对简单,但在实际使用中还需要考虑很多问题,例如锁的超时和续期问题、锁的公平性问题、网络分区的问题等。在接下来的文章中,我们将详细介绍这些问题以及解决方案。


文章目录

        • 1、Redis分布式锁简介
          • 1.1、关于分布式锁
          • 1.2、Redis分布式锁概述
        • 2、Redis分布式锁的问题及解决方案
          • 2.1、锁超时机制
          • 2.2、锁续期机制
          • 2.3、误删锁问题
          • 2.4、脑裂问题与Redlock
          • 2.5、公平性问题
        • 3、Java下Redis分布式锁实现
          • 3.1、Jedis实现
          • 3.2、SpringBoot实现


1、Redis分布式锁简介
1.1、关于分布式锁

在一个分布式系统中,当一个线程去读取数据并修改的时候,因为读取和更新保存不是一个原子操作,在并发时就很容易遇到并发问题,进而导致数据的不正确。这种场景很常见,比如电商秒杀活动,库存数量的更新就会遇到。如果是单机应用,直接使用本地锁就可以避免。如果是分布式应用,本地锁派不上用场,这时就需要引入分布式锁来解决。

一般来说,实现分布式锁的方式有以下几种:

  1. 使用 MySQL:这种方式是通过在数据库中创建一个唯一索引的表,然后通过插入一条数据来获取锁,如果插入成功则获取锁成功,否则获取锁失败。释放锁的操作就是删除这条数据。这种方式的优点是实现简单,缺点是性能较低,因为涉及到数据库的操作。
  2. 使用 ZooKeeper:ZooKeeper 提供了一个原生的分布式锁实现。其基本思想是创建一个临时有序节点,然后判断自己是否是所有子节点中序号最小的,如果是则获取锁成功,否则监听比自己序号小的节点,当该节点删除时再次尝试获取锁。这种方式的优点是能够保证公平性,缺点是实现较为复杂。
  3. 使用 Redis:这种方式是通过 Redis 的 SETNX 命令来实现的,这个命令可以在键不存在时设置值,如果键已存在则不做任何操作。通过这个原子操作,我们可以实现在多个节点之间的互斥访问。这种方式的优点是性能高,实现简单,缺点是需要处理锁的超时和续期问题。
1.2、Redis分布式锁概述

在 Redis 中,我们可以使用 SETNX 命令来实现分布式锁。以下是具体的步骤:

  1. 加锁:客户端使用 SETNX key value 命令尝试设置一个键,其中 key 是锁的名称,value 是一个唯一标识符(例如 UUID),用于标识加锁的客户端。如果键不存在,SETNX 命令会设置键的值并返回 1,表示加锁成功;如果键已存在,SETNX 命令不会改变键的值并返回 0,表示加锁失败。

image-20230916113717663

  1. 执行业务操作:客户端在成功获取锁后,可以执行需要保护的业务操作。
  2. 解锁:客户端在完成业务操作后,需要释放锁以让其他客户端可以获取锁。为了确保只有加锁的客户端可以解锁,客户端需要先获取锁的值(即唯一标识符),然后比较锁的值和自己的唯一标识符是否相同,如果相同则使用 DEL key 命令删除键以释放锁。

2、Redis分布式锁的问题及解决方案
2.1、锁超时机制

以下是一个基本的 Redis 分布式锁的使用流程:

  1. 客户端 A 发送一个 SETNX lock.key 命令,如果返回 1,那么客户端 A 获得锁。
  2. 客户端 A 执行完毕后,通过 DEL lock.key 命令释放锁。

然而,这种最基本的锁存在一个问题,那就是如果客户端 A 在执行完毕后,因为某些原因(比如崩溃或网络问题)无法发送 DEL 命令来释放锁,那么其他客户端将永远无法获得锁。为了解决这个问题,我们需要引入锁的超时机制。

image-20230916130718524

下是一个带有超时机制的 Redis 分布式锁的使用流程:

  1. 客户端 A 发送一个 SETNX lock.key 命令,如果返回 1,那么客户端 A 获得锁。
  2. 客户端 A 通过 EXPIRE lock.key timeout 命令设置锁的超时时间。
  3. 客户端 A 执行完毕后,通过 DEL lock.key 命令释放锁。

这样,即使客户端 A 在执行完毕后无法释放锁,其他客户端也可以在锁超时后获得锁。

2.2、锁续期机制

然而,这种带有超时机制的锁还存在一个问题,那就是如果客户端 A 在锁即将超时时仍在执行,那么锁可能会被其他客户端获得,从而导致多个客户端同时持有锁。为了解决这个问题,我们需要引入锁的续期机制。

image-20230916130800910

以下是一个带有续期机制的 Redis 分布式锁的使用流程:

  1. 客户端 A 发送一个 SETNX lock.key 命令,如果返回 1,那么客户端 A 获得锁。
  2. 客户端 A 通过 EXPIRE lock.key timeout 命令设置锁的超时时间。
  3. 客户端 A 在执行过程中,定期通过 EXPIRE lock.key timeout 命令续期锁。
  4. 客户端 A 执行完毕后,通过 DEL lock.key 命令释放锁。

这样,即使客户端 A 的执行时间超过了最初的超时时间,也可以通过续期机制保证锁的互斥性。

2.3、误删锁问题

引入锁的续期机制可以解决锁提前过期的问题,但是并不能解决解锁时可能删除其他线程锁的问题。这是因为,即使有了续期机制,仍然存在这样一种情况:线程 A 在锁即将过期时仍在执行业务逻辑,此时锁过期,线程 B 获取到了锁,然后线程 A 执行完业务逻辑,尝试去删除锁,结果删除的是线程 B 的锁。

为了解决这个问题,我们可以使用 Redis 的 Lua 脚本功能,将这三个操作封装在一个 Lua 脚本中,然后使用 EVAL 命令执行这个 Lua 脚本。由于 Redis 会单线程顺序执行所有命令,因此 EVAL 命令可以保证 Lua 脚本中的操作是原子的。

以下是一个使用 Lua 脚本实现解锁的例子:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

在这个 Lua 脚本中,我们首先使用 get 命令获取锁的值,然后比较锁的值和客户端的唯一标识符,如果相同则使用 del 命令删除锁。

客户端可以使用以下命令执行这个 Lua 脚本:

EVAL script 1 key value

其中,script 是 Lua 脚本的内容,key 是锁的名称,value 是客户端的唯一标识符

2.4、脑裂问题与Redlock

在 Redis 集群中,如果主节点在同步锁到从节点之前挂掉,那么从节点在升级为主节点后可能会误认为锁不存在,从而允许其他客户端获取锁,这就导致了同一把锁被多个客户端同时持有的问题。

为了解决这个问题,我们可以使用 RedLock 算法。RedLock 是 Redis 官方推荐的一种分布式锁实现算法,其基本思想是在多个独立的 Redis 节点上同时尝试获取锁,只有当大多数的 Redis 节点都成功获取到锁时,才认为整个操作成功。

以下是 RedLock 算法的基本步骤:

  1. 获取当前时间,以毫秒为单位。
  2. 依次尝试在所有 Redis 节点上获取锁,每个尝试都有一个固定的超时时间。如果获取锁失败,立即返回,不再尝试其他节点。
  3. 如果成功获取到了大多数的 Redis 节点的锁,并且获取锁的总时间小于锁的有效期,那么整个操作成功。
  4. 如果获取锁的总时间大于锁的有效期,或者没有成功获取到大多数的 Redis 节点的锁,那么在所有 Redis 节点上释放锁。
  5. 如果整个操作成功,那么锁的有效期就是原来的有效期减去获取锁的总时间。

以上就是 RedLock 算法的基本步骤。通过在多个独立的 Redis 节点上同时尝试获取锁,RedLock 算法可以在一定程度上解决主节点挂掉导致的锁丢失问题。然而,需要注意的是,RedLock 算法并不能完全保证锁的安全性,因为在网络分区或者节点时间不同步的情况下,仍然可能出现同一把锁被多个客户端同时持有的问题。因此,在使用 RedLock 算法时,需要根据实际情况进行详细的设计和测试。

2.5、公平性问题

此外,在 Redis 分布式锁的实现中,锁的公平性可能会成为一个问题。所谓公平性,是指当多个客户端同时请求锁时,锁应该被按照请求的顺序分配。然而,由于网络延迟和 Redis 的单线程模型,Redis 分布式锁无法保证公平性。具体来说,当多个客户端同时请求锁时,由于网络延迟,这些请求可能会在不同的时间到达 Redis,而 Redis 会按照请求到达的顺序分配锁,这可能与客户端的请求顺序不同。此外,即使多个请求同时到达 Redis,由于 Redis 的单线程模型,Redis 也只能依次处理这些请求,而处理的顺序可能与客户端的请求顺序不同。

因此,如果你的应用需要公平的分布式锁,你可能需要使用其他的分布式锁实现,例如基于 ZooKeeper 的分布式锁。ZooKeeper 的分布式锁通过在锁的节点下创建顺序临时节点,并通过比较自己的节点是否为最小节点来判断是否获取到锁,从而保证了锁的公平性。


3、Java下Redis分布式锁实现
3.1、Jedis实现

在 Java 中,我们可以使用 Jedis 或 Lettuce 这样的 Redis 客户端库来实现 Redis 分布式锁。以下是一个基本的实现示例:

import redis.clients.jedis.Jedis;

public class RedisLock {
    private Jedis jedis;
    private String lockKey;
    private String lockValue;
    private int expireTime;
    private boolean locked = false;

    public RedisLock(Jedis jedis, String lockKey, int expireTime) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.expireTime = expireTime;
        this.lockValue = Thread.currentThread().getId() + "-" + System.nanoTime();
    }

    public boolean lock() {
        long startTime = System.currentTimeMillis();
        while (true) {
            String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
            if ("OK".equals(result)) {
                locked = true;
                return true;
            }
            // 如果没有获取到锁,需要稍微等待一下再尝试
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            // 如果尝试获取锁超过了expireTime,那么返回失败
            if (System.currentTimeMillis() - startTime > expireTime) {
                return false;
            }
        }
    }

    public void unlock() {
        if (!locked) {
            return;
        }
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, 1, lockKey, lockValue);
    }
}

在这个示例中,我们使用 set 命令的 NXPX 选项来实现锁的获取和超时设置,使用 Lua 脚本来实现安全的解锁操作。我们还使用了一个 while 循环来不断尝试获取锁,直到成功获取锁或者超过了尝试的时间。

然而,这个示例并没有实现锁的续期机制。为了实现续期机制,我们需要在另一个线程中定期检查锁的剩余时间,如果剩余时间不足,那么就需要使用 expire 命令来重新设置锁的超时时间。这需要更复杂的代码来实现,例如使用 Java 的 ScheduledExecutorService 来定期执行续期操作。

3.2、SpringBoot实现

在 Spring Boot 中,我们可以使用 Redisson 这个 Redis 客户端库来实现 Redis 分布式锁。Redisson 提供了一套丰富的分布式服务,包括分布式锁、分布式集合、分布式队列等,而且 Redisson 已经内置了锁的超时、续期机制,并解决了误删锁问题。

以下是一个基本的使用 Redisson 实现 Redis 分布式锁的示例:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
public class RedissonDistributedLocker {

    private RedissonClient redissonClient;

    @PostConstruct
    public void init() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redissonClient = Redisson.create(config);
    }

    public void lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        // Wait for 100 seconds and automatically unlock it after 10 seconds
        lock.lock(10, TimeUnit.SECONDS);
    }

    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }
}

在这个示例中,我们首先在 init 方法中创建了一个 RedissonClient 实例,然后在 lock 方法中获取了一个 RLock 对象,并调用其 lock 方法来获取锁。在 unlock 方法中,我们同样获取了一个 RLock 对象,并调用其 unlock 方法来释放锁。

需要注意的是,Redisson 的 lock 方法会自动续期,只要持有锁的线程还在运行,锁就会一直被续期,直到线程结束或者显式调用 unlock 方法。因此,我们不需要手动实现续期机制。此外,Redisson 的 unlock 方法会检查当前线程是否持有锁,只有持有锁的线程才能释放锁,这解决了误删锁问题。

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

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

相关文章

MyBatis基础之执行SQL

文章目录 执行 SQL 语句1. 增删改操作insert 元素insert 过程中的主键回填delete 元素 和 update 元素 2. getMapper 方法3. 查操作select 元素select 与 聚合函数 4. 传递多个参数使用 Map 传递多参数使用 JavaBean 传递多参使用注解方式传递多参数 执行 SQL 语句 Mapper 是 …

1999-2018年地级市一般公共预算收入、支出(教育事业费、科技支出)

1999-2018年地级市一般公共预算收入、支出(教育事业费、科技支出) 1、时间:1999-2018年 2、来源:城市年鉴 3、指标:行政区划代码、城市、年份、地方一般公共预算收入_市辖区_万元、地方一般公共预算支出_市辖区_万元…

云效+主机部署解决方案(需求->开发->测试->发布->运维->运营)

文章目录 引言I Maven相关1.1 阿里云私有仓库-迁移本地仓库至私有仓库II 代码管理2.1 初始化仓库脚本2.2 分支模式:分支开发、主干发布“的模式III 集成3.1 开启分支模式IV 创建阿里云子账号(RAM)V 安全组端口访问规则配置IV 阿里云的日志服务 SLSsee also引言 Flow语言专项…

性能测试 —— Tomcat监控与调优:status页监控

Tomcat服务器是一个免费的开放源代码的Web 应用服务器,Tomcat是Apache 软件基金会(Apache Software Foundation)Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。 Tomcat是一个轻量级应用服务器,在中小型系统…

1787_函数指针的使用

全部学习汇总:GitHub - GreyZhang/c_basic: little bits of c. 前阵子似乎写了不少错代码,因为对函数指针的理解还不够。今天晚上似乎总算是梳理出了一点眉目,在先前自己写过的代码工程中做一下测试。 先前实现过一个归并排序算法&#xff0c…

Java并发编程第8讲——ThreadLocal详解

ThreadLocal无论是在项目开发还是面试中都会经常碰到,它的重要性可见一斑,本篇文章就从ThreadLocal的使用、实现原理、核心方法的源码、内存泄漏问题等展开介绍一下。 一、什么是ThreadLocal ThreadLocal是java.lang下面的一个类,在JDK 1.2版…

植隆业务中台与金蝶云星空对接集成服务工单查询接口连通应收单新增(6202-开票申请(代理商-销售类))

植隆业务中台与金蝶云星空对接集成服务工单查询接口连通应收单新增(6202-开票申请(代理商-销售类)) 数据源系统:植隆业务中台 承载了企业核心关键业务,是企业的核心业务能力,也是企业数字化转型的重点。业务中台的建设目标是&…

网络爬虫-----http和https的请求与响应原理

目录 前言 简介 HTTP的请求与响应 浏览器发送HTTP请求的过程: HTTP请求主要分为Get和Post两种方法 查看网页请求 常用的请求报头 1. Host (主机和端口号) 2. Connection (链接类型) 3. Upgrade-Insecure-Requests (升级为HTTPS请求) 4. User-Agent (浏览…

原生js之script基本属性

async:异步执行脚本 defer:延迟脚本下载 src:要执行的代码外部文件地址 noscript:表示浏览器不支持或拒绝支持script脚本时出现的内容 async和defer async和defer本质都是为了让脚本推迟到整个页面解析后再下载,不同的是async是异步无序的,而defer是同…

基于微信小程序的在线小说阅读系统,附数据库、教程

1 功能简介 Java基于微信小程序的在线小说阅读系统 微信小程序的在线小说阅读系统,系统的整体功能需求分为两部分,第一部分主要是后台的功能,后台功能主要有小说信息管理、注册用户管理、系统系统等功能。微信小程序主要分为首页、分类和我的…

【开发】视频监控系统/视频汇聚平台EasyCVR对国标类型编码进行判断的实现方式

视频监控平台/视频存储/视频分析平台EasyCVR基于云边端一体化管理,支持多类型设备、多协议方式接入,具体包括:国标GB28181协议、RTMP、RTSP/Onvif、海康Ehome,以及海康SDK、大华SDK、华为SDK、宇视SDK、乐橙SDK、萤石SDK等&#x…

利用免费的敏捷研发管理工具管理端到端敏捷研发流程

Leangoo领歌是Scrum中文网(scrum.cn)旗下的一款永久免费的敏捷研发管理工具。 Leangoo领歌覆盖了敏捷研发全流程,它提供端到端敏捷研发管理解决方案,包括小型团队敏捷开发,规模化敏捷SAFe,Scrum of Scrums…

进一步观察扩散模型中的参数有效调整

摘要: 像Stable diffusion[31]这样的大规模扩散模型非常强大,可以找到各种真实世界的应用程序,而通过微调来定制这样的模型会降低内存和时间的效率。受自然语言处理最新进展的推动,我们通过插入小型可学习模块adapters(称为适配器…

链表实现稀疏多项式相加(C++)

#include<iostream> using namespace std; typedef struct node {float coef;//系数int expn;//指数struct node* next; }list,*linklist; void Createlist(linklist& l) {l new list;l->next NULL;linklist p,q;q l;cout << "输入多项式项数&#…

ARM 汇编指令集——汇编中三种符号(汇编指令、伪指令、伪操作)、汇编基本格式、数据操作指令、跳转指令、特殊功能寄存器操作指令、内存操作指令、混合编程

目录 一、汇编中三种符号&#xff08;汇编指令、伪指令、伪操作&#xff09; 二、汇编基本格式 三、数据操作指令 3.1 数据搬移指令mov/mvn ① 示例 ② 立即数 3.2 移位操作指令lsl/lsr/asr/ror 示例 3.3 位运算操作指令and/orr/eor/bic ① 示例1 ② 示例2 3.4 算数…

第32节——useReducer——了解

一、概念 useReducer 是在 react V 16.8 推出的钩子函数&#xff0c;从用法层面来说是可以代替useState。众所周知&#xff0c;useState 常用在单个组件中进行状态管理&#xff0c;但是遇到状态全局管理的时候&#xff0c;useState 显然不能满足我们的需求&#xff0c;这个时候…

4G工业路由器高效数据传输助力光伏发电站管理

光伏发电站是能源产业中一种利用太阳能技术将光转化为电能的常见设施。随着物联网技术与环保能源的不断进步和应用的普及&#xff0c;光伏发电站的管理也变得更加便捷高效。 光伏发电站结合4G工业路由器实现远程监控管理&#xff0c;并用于采集发电站中的传感器数据和监控信息…

vue watch 侦听器 监视器

vue watch 侦听器 监视器 变量 变化的时候&#xff0c;自动调用处理函数 vue watch 侦听器 监视器

/node_modules/XXX/index.js:XXX XXX ??= X;SyntaxError: Unexpected token ‘??=‘

这问题 老实说有点奇葩 不影响运行 反倒运行提交了 不解决这个问题提交不了代码 这个错误是由于语法不兼容导致的。?? 是一个相对较新的 JavaScript 语法&#xff0c;也就是空值合并赋值操作符&#xff0c;它在 Node.js 版本低于 15 或者某些浏览器中不被支持。 那么 了解…

日常生活中的常用命令及操作

目录 一、Windows11 中查看网卡名称 及ip地址 二、查看硬件的详细信息 三、查看显卡声卡详细信息及厂商 四、C盘清理 第一步 输入wini 开启Windows设置主界面 第二步 存储中还有一个叫存储感知的功能 第三步 更改新内容的保存位置 第四步 怕误C盘内的东西可以 查看详细的…