【Redis】实现及优化分布式锁:实现、解决误删锁问题以及lua脚本确保redis操作原子性

news2024/10/6 16:16:10

目录

一、概念及不同分布式锁实现的对比

1、概念

2、特征

3、不同分布式锁实现的对比

二、Redis实现分布式锁的思路

1、获取锁思路

2、释放锁思路

三、代码实现分布式锁

1、准备

2、获取锁

2、释放锁

四、分布式锁的误删锁问题

1、问题 

 2、原因

五、误删锁的解决方案

1、解决思路

2、代码实现

1.获取锁

2.释放锁

六、分布式锁的原子性问题

1、问题

2、原因

3、解决思路

七、Java操作Redis执行lua脚本相关的API

八、原子性问题的解决方案

1、写入.lua文件

2、准备DefaultRedisScript对象

3、释放锁代码修改

九、基于setnx实现分布式锁的使用

十、基于setnx实现分布式锁的问题

1、不可重入

2、不可重试

3、超时释放

4、主从一致性

5、总结


一、概念及不同分布式锁实现的对比

1、概念

满足分布式系统或集群模式下多进程可且互斥的锁就叫分布式锁,传统的Synchronized锁,它是在JVM中有一个锁监视器,这个锁监视器仅对当前进程适用,如果将该服务多台服务器进行部署使用该锁则不能保证线程安全问题,此时我们可以使用分布式锁来代替它。

2、特征

它具有以下特征:多进程可见、互斥、高可用、高性能、安全性。为了保证安全性,后续实现时我们需要考虑异常、死锁等情况

3、不同分布式锁实现的对比

MySQLRedisZookeeper
互斥利用它本身的互斥锁机制实现利用setnx互斥命令实现利用节点的唯一性和有序性实现
高可用                好           好           好
高性能             一般           好        一般
安全性断开连接自动释放锁利用ttl到期自动释放,存在问题:如果ttl过长则无效等待时间过长,,如果ttl过短则存在线程安全问题临时节点,断开连接自动释放

二、Redis实现分布式锁的思路

1、获取锁思路

通过setnx这一命令实现获取锁,如果setnx成功则获取到锁,后续线程只尝试获取一次,如果setnx失败则返回false,为了保证安全性我们setnx后还要给这个锁加过期时间expire ttl,但是如果我们刚执行完setnx还没有执行expire ttl操作时redis宕机则死锁,为了防止我们将setnx 与 expire操作改为一个原子操作:set lock value EX ttl NX 即可

2、释放锁思路

释放锁有两种,一种是通过ttl过期后自动释放,一种是del lock删除即可手动释放,此处我们使用手动删除的方式来实现

三、代码实现分布式锁

1、准备

首先我们先要创建分布式锁的工具类,在类中定义所需要的字段


import org.springframework.data.redis.core.StringRedisTemplate;

public class RedisDistributedLock {
    private String name;       // 需要加锁的业务key
    private StringRedisTemplate stringRedisTemplate; // 操作redis
    private static final String KEY_PREFIX = "lock"; // 加锁key的前缀

    // 提供构造方法 
    public RedisDistributedLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
}

2、获取锁

接着我们来实现获取锁的操作

import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

/**
 * 基于redis实现分布式锁 
 */
public class RedisDistributedLock {
    private String name;       // 需要加锁的业务key
    private StringRedisTemplate stringRedisTemplate; // 操作redis
    private static final String KEY_PREFIX = "lock"; // 加锁key的前缀

    // 提供构造方法
    public RedisDistributedLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 加锁方法
     * @param time  过期时间
     * @return  加锁是否成功
     */
    public boolean tryLock(long time) {
        // 获取线程ID
        long threadId = Thread.currentThread().getId();
        
        // 获取锁
        Boolean isLock = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name,threadId + "",time, TimeUnit.SECONDS);
        
        // 返回结果   此处需要注意如果直接返回boolean类型会产生拆箱,可能会发送异常,所以使用以下操作
        return Boolean.TRUE.equals(isLock);
    }
}

2、释放锁

/**
     * 释放锁
     * 将key删除即可
     */
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }

四、分布式锁的误删锁问题

1、问题 

针对上述代码,如果有一个线程1获取到了这个锁,正常情况下其他线程获取该锁时则会失败,但是如果由于某些原因,线程1获取到了该锁后执行业务时发送了阻塞,直到锁的过期时间到了自动删除后线程1还在阻塞,此时线程2尝试获取锁时获取成功了,于是他去执行他的业务,这个时候线程1执行完了业务开始尝试去释放锁(删除key)于是它删除成功了,但是它删除的不是自己的锁而是线程2的锁,这个时候线程3又获取到了锁,线程3去执行了业务逻辑,此时多个线程并发执行产生线程安全问题

 2、原因

产生上述线程安全问题的原因注意是线程1把不属于自己的别人的锁给释放了

五、误删锁的解决方案

1、解决思路

由于上述问题产生原因是线程把不属于自己的锁给释放了,此时我们可以在线程释放锁的时候进行判断,判断这个锁是不是属于自己的,也就是获取一下这个key的value与自己的value进行对比,但是此时又存在了一个问题就是我们之前存入key的value时是获取了线程的id,在同一个进程下每个线程的id都是递增的不会重复,但是如果不同进程,也就是服务多服务器部署的情况下,就可能存在线程id相同但是线程不同,所以我们需要将value修改一下给它加一个唯一的前缀来区别不同的进程,我们可以通过uuid来实现

 private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";

2、代码实现

1.获取锁

  /**
     * 加锁方法
     * @param time  过期时间
     * @return  加锁是否成功
     */
    public boolean tryLock(long time) {
        // 获取线程ID
        String threadId = ID_PREFIX + Thread.currentThread().getId();

        // 获取锁
        Boolean isLock = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name,threadId,time, TimeUnit.SECONDS);

        // 返回结果   此处需要注意如果直接返回boolean类型会产生拆箱,可能会发送异常,所以使用以下操作
        return Boolean.TRUE.equals(isLock);
    }

2.释放锁

我们需要修改释放锁的代码

/**
     * 释放锁
     * 将key删除即可
     */
    public void unlock() {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        
        // 获取锁的线程标识
        String lockId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        
        // 判断是否一致
        if (threadId.equals(lockId)) {
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }

六、分布式锁的原子性问题

1、问题

针对上述代码,如果此时线程1获取到了锁,线程1业务阻塞了,发送了上面误删的前置问题,但是它删除锁时进行判断发现锁与自己的线程id不一样,则不会去删除锁,一切正常。但是如果线程1获取到了锁执行完业务后删除锁时,先去reids查询了当前锁的标识,然后在准备执行删除操作时由于如JVM垃圾回收等的问题发送了阻塞,而恰好此时在还没执行删除操作前,这个锁到了过期时间于是自动删除,此时线程2获取到了锁开始执行业务,这个时候线程1的删除锁操作开始执行,将线程2的锁删除了,这个时候线程3又趁虚而入获取到了锁执行业务,又发送了多个线程并发执行的线程安全问题

2、原因

由于上述释放锁操作中的查询与删除操作不是原子性的

3、解决思路

我们可以通过操作使得这两个操作变为一个原子性的操作,Redis提供了Lua脚本功能,在一个lua脚本中我们可以编写多条redis指令,这些指令在执行时是原子性的,在redis中可以通过EVAL命令来调用lua脚本

EVAL script numkeys     [key ……] [arg……]

命令    脚本    key个数    多个key    其他参数

在script中也就lua脚本中可以通过KEYS[下标]来获取后面的key,也可以通过ARGV[下标]来获取后面的其他参数,要注意的是lua中下标是从1开始的

不了解lua的话可以去这个网站学习它的基础操作,在redis中我们也是常用基础操作多

Lua 教程 | 菜鸟教程 (runoob.com)icon-default.png?t=N3I4https://www.runoob.com/lua/lua-tutorial.html

下面我们将释放锁的逻辑写为lua脚本

-- 查询操作获取锁的线程标识
local id = redis.call('get',KEYS[1])
-- 与当前线程标识进行对比
if (id == ARGV[1]) then
    -- 当前线程持有锁,进行删除
    return redis.call('del',KEYS[1])
end
return 0

七、Java操作Redis执行lua脚本相关的API

在Java中我们如何操作redis中的EVAL命令让它去执行上述lua脚本呢,在StringRedisTemplate提供了相关的API

public <T> T execute(RedisScript<T> script,List<K> keys,Object... args)

RedisScript<T> script:lua脚本,其中T返回值类型

List<K> keys: EVAL命令后的keys

Object... args:EVAL命令后的arg

八、原子性问题的解决方案

1、写入.lua文件

我们需要将上述lua脚本写入一个.lua文件(创建一个)

需要记住该路径 

2、准备DefaultRedisScript对象

在我们的类中需要准备DefaultRedisScript对象来提前加载这个lua脚本

 private static final DefaultRedisScript<Long> UNLOCK_SCRIPT; // 加载lua脚本
    static {
        // 类加载时就初始化
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua存放.lua文件的路径")); // 指定录取去加载
        UNLOCK_SCRIPT.setResultType(Long.class);                        // 返回值类型
    }

3、释放锁代码修改

/**
     * 释放锁
     * 将key删除即可
     */
    public void unlock() {
        // 构建keys
        List<String> keys = new ArrayList<>();
        keys.add(KEY_PREFIX + name);

        // 执行lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT,keys,ID_PREFIX + Thread.currentThread().getId());
    }

九、基于setnx实现分布式锁的使用

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    public void useLock() {
        // 构造业务名称作为锁的key
        String key = "order";
        
        // 首先创建出对象
        RedisDistributedLock lock = new RedisDistributedLock(key,stringRedisTemplate);
        
        // 获取锁
        boolean isLock = lock.tryLock(20);
        if (!isLock) {
            // 没有获取到,可进行结束或重试操作,此处结束
            return;
        }
        
        try {
            // 获取到了锁执行相关的业务代码
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

十、基于setnx实现分布式锁的问题

1、不可重入

线程1在方法a中获取到了锁,然后在方法a中调用方法b,方法b中也需要获取同一把锁,此时由于方法a中获取了锁,该处锁不可重入则导致死锁,发送线程安全问题

2、不可重试

获取锁只获取一次,如果没有获取到锁没有重试机制

3、超时释放

虽然超时释放解决了死锁的问题,但是如果业务时间过长大于锁的过期时间则会发送操作安全问题

4、主从一致性

如果Redis提供了主从集群,主从的同步会有延迟,如果线程1从主节点获取了锁,主节点还没有来的及将锁同步给从节点就忽然宕机,此时从节点没有锁的信息,其他线程就可以获取到锁,从而产生线程安全问题

5、总结

后续我们使用分布式锁会使用比较成熟的存在的组件

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

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

相关文章

分布式配置中心

一、Config概述 Spring Cloud Config 解决了在分布式场景下多环境配置文件的管理和维护 好处&#xff1a; 集中管理配置文件 不同环境不同配置&#xff0c;动态化的配置更新 配置信息改变时&#xff0c;不需要重启即可更新配置信息到服务 二、Config 快速入门 1、使用git…

5.10-5.11总结

我教的课中 课程双击事件&#xff0c;跳转到课程界面 输入学生姓名和学号&#xff0c;添加学生 加载学生名单&#xff0c;双击学生&#xff0c;弹出学生资料&#xff0c;并且可以删除学生 但删除学生还有bug。

LeetCode - 1552 两球之间的磁力

目录 题目来源 题目描述 示例 提示 题目解析 算法源码 题目来源 1552. 两球之间的磁力 - 力扣&#xff08;LeetCode&#xff09; 题目描述 在代号为 C-137 的地球上&#xff0c;Rick 发现如果他将两个球放在他新发明的篮子里&#xff0c;它们之间会形成特殊形式的磁力。…

二维各向同性介质弹性波数值模拟(交错网格有限差分法)

一、一阶速度-应力弹性波方程 在二维各向同介质xoz平面内&#xff0c;假定体力为0。 从上面方程当中&#xff0c;我们为了得到各点的应力和速度值&#xff0c;就需要得到关于对时间t和空间x&#xff0c;z的偏导。 二、时间上的2M阶差分 由Taylor公式得 三、空间2N阶近似差分…

知识推理——CNN模型总结(持续更新)

记录一下我看过的利用CNN实现知识推理的论文。 最后修改时间&#xff1a;2023.05.10 目录 1.ConvE 1.1.解决的问题 1.2.优势 1.3.贡献与创新点 1.4.方法 1.4.1 为什么用二维卷积&#xff0c;而不是一维卷积&#xff1f; 1.4.2.ConvE具体实现 1.4.3.1-N scoring 1.5.…

RK3568平台开发系列讲解(驱动基础篇)GPIO控制方式

🚀返回专栏总目录 文章目录 一、使用GPIO sysfs接口控制IO二、使用libgpiod控制IO沉淀、分享、成长,让自己和他人都能有所收获!😄 📢GPIO是 General Purpose I/O 的缩写,即通用输入输出端口,简单来说就是MCU/CPU可控制的引脚, 这些引脚通常有多种功能,最基本的是高…

3D点云在线查看利器【LasViewer】

LasViewer是一个免费的3D点云在线查看工具&#xff0c;支持LAS/LAZ格式的3D点云文件在浏览器中的直接渲染。访问地址&#xff1a;LasViewer。 推荐&#xff1a;用 NSDT设计器 快速搭建可编程3D场景。 1、LasViewer快速上手 和BimAnt的其他在线工具一样&#xff0c;LasViewer的…

Windows11使用Cpython 编译文件 报错 error: Unable to find vcvarsall.bat 完美解决方法

开发环境说明&#xff1a; python 3.6.2Vs studio 2017 (已经安装C桌面开发&#xff09; 我的 vcvarsall.bat 路径为&#xff1a; "D:\vsstudio\VC\Auxiliary\Build\vcvarsall.bat" 一般在Vs studio 的此安装路径下 修改python源代码 修改文件为 python3.6.2\Li…

shell脚本练习题

名为lianxi.txt的文件内容如下&#xff1a; Steve Blenheim:238-923-7366:95 Latham Lane, Easton, PA 83755:11/12/56:20300 Betty Boop:245-836-8357:635 Cutesy Lane, Hollywood, CA 91464:6/23/23:14500 Igor Chevsky:385-375-8395:3567 Populus Place, Caldwell, NJ 2387…

[230516] TPO71 | 2022年托福阅读真题第4/36篇 | Electrical Energy from the Ocean | 11:50

目录 7101 Electrical Energy from the Ocean Paragraph 1 问题1 Paragraph 2 问题2 Paragraph 3 问题3 Paragraph 4 问题4 做错 Paragraph 5 问题5 做错 Paragraph 6 问题6 Paragraph 7 问题7 Paragaph 8 问题8 做错 Paragraph 2 问题9 问题10 7101…

GateWay源码解析

前言 一、GateWay的自动配置 springboot 在引入一个新的组件时&#xff0c;一般都会有对应的XxxAutoConfiguration类来对该组件进行配置&#xff0c;GateWay也不例外&#xff0c;在引入了以下配置后&#xff0c;就会生成对应的GatewayAutoConfiguration自动配置类 <!-- gat…

15.Python Package目录及打包并发布到PyPI

欢迎访问个人网络日志&#x1f339;&#x1f339;知行空间&#x1f339;&#x1f339; 文章目录 0.基本介绍1.__init__.py文件1.1 Regular Package1.2 namespace package 2.Python Package工程2.1 安装及打包并发布到pypi2.2 将Python文件编译成.so 3.包的搜索路径参考资料 0.基…

go test coverage 单测覆盖率

单元测试的最终统计标准就是单测覆盖率&#xff0c;统计单测总体覆盖了多少行代码。一般来说&#xff0c;我们只需要关注增量代码的覆盖率&#xff0c;而非全量代码。增量代码就是本次迭代改动的代码&#xff0c;比如本次迭代改动了100行代码&#xff0c;我们保证单测能覆盖到这…

【Vue工程】007-Scss

【Vue工程】007-Scss 文章目录 【Vue工程】007-Scss一、概述1、CSS 问题三大缺点CSS 预处理器 2、简介3、中文网4、Slogan 二、基本使用1、安装2、配置全局 scss 样式文件3、在 vite.config.ts 配置4、组件中使用5、访问 http://localhost:5173/home 一、概述 1、CSS 问题 参考…

【OJ比赛日历】快周末了,不来一场比赛吗? #05.13-05.19 #14场

CompHub 实时聚合多平台的数据类(Kaggle、天池…)和OJ类(Leetcode、牛客…&#xff09;比赛。本账号同时会推送最新的比赛消息&#xff0c;欢迎关注&#xff01; 更多比赛信息见 CompHub主页 或 点击文末阅读原文 以下信息仅供参考&#xff0c;以比赛官网为准 目录 2023-05-…

AC AP简单组网

AC AP简单组网 1、LSW1交换机配置2、AC1控制器配置3、初步效果查看3.1、查看PC1获取地址情况3.2、查看AP获取地址情况 4、AC1控制器配置组网5、组网成功验收5.1、查看AP的物理地址&#xff08;dis arp)5.2、ensp模拟的拓扑结果5.3、STA链接到AP网络5.3、查看STA地址及连通性 vl…

ChatGPT:讯飞星火认知大模型-科大讯飞

讯飞星火认知大模型 科大讯飞推出的新一代认知智能大模型&#xff0c;拥有跨领域的知识和语言理解能力&#xff0c;能够基于自然对话方式理解与执行任务。从海量数据和大规模知识中持续进化&#xff0c;实现从提出、规划到解决问题的全流程闭环。 进入科大讯飞官网点击注册 …

【枚举+数学】CF1781D Many Perfect Squares

Many Perfect Squares - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题意&#xff1a; 思路&#xff1a; n&#xff0c;1~50&#xff1a;对n暴力 x&#xff0c;0~1e18&#xff1a;O(1)计算 完全平方数&#xff1a;p^2 Code&#xff1a; #include <bits/stdc.h>#de…

liunx将普通用户提升为管理员

场景 用户要求将账号设置为管理员 操作如下 先登录服务器用管理员账号 打开配置文件/etc/sudoers 此时你会发现文件是空的&#xff0c;为什么呢&#xff1f;原因如下 因为当时使用的是管理员账号 需要切换成root才可以修改此文件 命令sudo su - 操作见图片 操作完之后 用户…

深入浅出解析 JVM 中的 Safepoint

1. 初识 Safepoint-GC 中的 Safepoint 最早接触 JVM 中的安全点概念是在读《深入理解 Java 虚拟机》那本书垃圾回收器章节的内容时。相信大部分人也一样&#xff0c;都是通过这样的方式第一次对安全点有了初步认识。不妨&#xff0c;先复习一下《深入理解 Java 虚拟机》书中安…