Redis实战—Redis分布式锁

 本博客为个人学习笔记,学习网站与详细见:黑马程序员Redis入门到实战 P56 - P63

目录

分布式锁介绍

基于Redis的分布式锁

Redis锁代码实现

修改业务代码 

分布式锁误删问题

分布式锁原子性问题 

Lua脚本

编写脚本 

代码优化

总结 


分布式锁介绍

        在上一篇文章 Redis实战—优惠卷秒杀 中,我们通过使用锁、事务和代理对象实现了“一人一单”的优惠券秒杀功能。但我们使用的锁是基于JVM内部的锁,这导致锁的范围只能限制单个JVM的线程操作,因此在集群情况下,依然会出现超卖问题。所以我们需要设置一个锁,使其能够同时限制集群中的多个JVM线程操作,而这个锁就是分布式锁,由此引出本文。

集群情况下JVM锁的使用情况如下图。

 集群情况下分布式锁的使用情况如下图。

 分布式锁的实现


基于Redis的分布式锁


        我们利用Redis的SET lock thread1 NX操作来模拟获取锁,即如果当前不存在lock键,则添加lock键成功,如果当前存在lock键,则添加lock键失败。我们将添加lock键的操作视为获取锁的操作,将lock键是否存在视为当前锁是否已被其他线程获取。执行语句后,通过Redis返回OK或者nil,我们可以判断是否获取锁成功。为防止宕机时无法对锁进行销毁,我们在进行SET操作时还需通过EX为键设置一个合理的时间。


Redis锁代码实现

// 接口类
public interface ILock {

    /*
    * 尝试获取锁
    * timeoutSec 锁持有的超时时间,过期后自动释放
    * 返回值 true代表获取锁成功;false代表获取锁失败
    * */
    boolean tryLock(long timeoutSec);

    //释放锁
    void unlock();

}

// 接口实现类
public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        long threadId = Thread.currentThread().getId();
        // 获取锁,并添加时间
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId + " ", timeoutSec, TimeUnit.SECONDS);

        //避免拆箱导致空指针,使用Boolean.TRUE.equals方法返回结果
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

修改业务代码 

    public Result seckillVoucher(Long voucherId) {
       
        //判断是否满足抢购条件
        ...

        Long userId = UserHolder.getUser().getId();
        // 创建锁对象,根据用户ID加锁
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 获取锁
        boolean isLock = lock.tryLock(1200);

        // 若获取锁失败
        if (!isLock)
            return Result.fail("不允许重复下单");

        // 若获取锁成功
        try {
            // 获取当前代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }
    }

分布式锁误删问题

        如上图所示,持有锁的线程1在锁的内部出现了业务阻塞,导致它的锁被超时释放。这时线程2尝试获得锁成功,然而在线程2持有锁执行过程中,线程1的业务反应过来,继续执行,而线程1业务执行完成后,进行了删除锁逻辑,此时就会把本应属于线程2的锁进行删除,这就是误删其它线程锁的情况。 


        解决方案:当线程创建锁时,同时为该锁添加当前线程标识,该标识由UUID随机数为前缀与线程id组合而成(为避免出现集群下两个线程的id相同的情况,因此添加UUID前缀)。当一个线程删除锁时,需要判断当前线程标识与锁标识是否一致,若一致,说明该锁由当前线程创建,可进行删除;若不一致,说明该锁由其它线程创建,不可进行删除。

        对simpleRedisLock类代码优化如下。

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

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

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁,并设置标识、添加时间
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);

        //避免拆箱导致空指针,使用Boolean.TRUE.equals方法返回结果
        return Boolean.TRUE.equals(success);
    }

    @Override
    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在"判断结束"到"释放锁"的期间,受到了阻塞(遇到JVM垃圾回收机制时会暂停程序,导致阻塞),这时线程2获取锁。当线程1恢复后,继续进行释放锁的操作,将会误删线程2的锁。我们前面设置了锁标识,并且要求在释放锁之前需要做一个判断,但在判断可以释放锁后,如果遇到了阻塞,将可能导致上图所示的误删操作。

        解决方法:我们需要实现"判断"和"释放锁"这两条命令的原子性问题。


Lua脚本

        Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,能够确保多条命令执行时的原子性。Lua是一种编程语言,其基本语法可以参考网站:Lua 教程 | 菜鸟教程。这里重点介绍Redis提供的调用函数,我们可以使用lua去操作redis,以保证多条redis命令的原子性,这样就可以实现拿锁、判断、删锁多条命令的原子性动作了,作为一名Java程序员这一块并不需要大家过于精通,只需要知道它有什么作用即可。


编写脚本 

        我们需要在resources文件中新建.lua文件(如果没有该新建项,需要下载EmmyLua插件),并在其中添加下图中的脚本内容。


代码优化

优化后的代码如下。

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    //初始化UNLOCK_SCRIPT
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        //初始化返回值
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        // 获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        // 获取锁,并设置锁标识、添加时间
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);

        //避免拆箱导致空指针,使用Boolean.TRUE.equals方法返回结果
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                //要求传入KEYS集合,使用Collections单元素集合工具
                Collections.singletonList(KEY_PREFIX + name),
                //线程标识
                ID_PREFIX + Thread.currentThread().getId());
    }

/*  @Override
    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);
    }*/
}

总结 

基于Redis的分布式锁实现思路
· 利用set nxex获取锁,并设置过期时间,保存线程标识
· 释放锁时先判断线程标识是否与锁标识一致,若一致则删除锁

特性
· 利用set nx满足互斥性
· 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
· 利用redis集群保证高可用和高并发特性(本文未涉及)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【技巧】Leetcode 201. 数字范围按位与【中等】

数字范围按位与 给你两个整数 left 和 right &#xff0c;表示区间 [left, right] &#xff0c;返回此区间内所有数字 按位与 的结果&#xff08;包含 left 、right 端点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;left 5, right 7 输出&#xff1a;4 解题思路 …

vscode禅模式怎么退出

1、如何进入禅模式&#xff1a;查看--外观--禅模式 2、退出禅模式 按二次ESC&#xff0c;就可以退出。

公共 IP 地址和私有 IP 地址的区别总结

什么是IP地址&#xff1f; IP 地址&#xff0c;即互联网协议地址&#xff08;Internet Protocol Address&#xff09;&#xff0c;是网络设备在网络中进行通信的标识。IP 地址可以看作是设备在网络中的“地址”&#xff0c;有助于数据包在网络中找到正确的接收端。IP 地址主要…

计算机系统基础实训七-MallocLab实验

实验目的与要求 1、让学生理解动态内存分配的工作原理&#xff1b; 2、让学生应用指针、系统级编程的相关知识&#xff1b; 3、让学生应用各种动态内存分配器的实现方法&#xff1b; 实验原理与内容 &#xff08;1&#xff09;动态内存分配器基本原理 动态内存分配器维护…

外包IT运维解决方案

随着企业信息化进程的不断深入&#xff0c;IT系统的复杂性和重要性日益增加。高效的IT运维服务对于保证业务连续性、提升企业竞争力至关重要。外包IT运维解决方案通过专业的服务和技术支持&#xff0c;帮助企业降低运维成本、提高运维效率和服务质量。 本文结合《外包IT运维解…

咖啡事故,上海Manner咖啡店,1天两起店员和顾客发生冲突

上海咖啡店Manner&#xff0c;一天的时间竟然发生两起店员和员工发生肢体冲突&#xff1a; 事情详情&#xff1a; Manner威海路716店事件: 店员泼顾客咖啡粉&#xff0c;随后被辞退品牌方回应媒体&#xff0c;表示将严肃处理Manner梅花路门店事件:顾客因等待时间长抱怨&…

Aquila-Med LLM:开创性的全流程开源医疗语言模型

​论文链接&#xff1a;https://arxiv.org/pdf/2406.12182 开源链接&#xff1a;https://huggingface.co/BAAI/AquilaMed-RL http://open.flopsera.com/flopsera-open/details/AquilaMed_SFT http://open.flopsera.com/flopsera-open/details/AquilaMed_DPO 近年来&#xf…

Magento1与Magento2的区别

本人接触magento有些年头了。。。 2012年开始用magento 1.7。2016年开始用magento2.0。 截止到目前。M1最新版本是1.9.3.3。 M2最新版本是2.2.2。 想当年第一次接触magento的时候&#xff0c;是跟同事一起&#xff0c;网上下载的Alan Storm的深入理解magento系统&#xff0c;…

链表中环的入口节点

链表中环的入口节点 描述 链表中环的入口节点 给一个长度为n链表&#xff0c;若其中包含环&#xff0c;请找出该链表的环的入口结点&#xff0c;否则&#xff0c;返回null。 数据范围&#xff1a; n≤10000&#xff0c; 1<结点值<10000 要求&#xff1a;空间复杂度 O(1)…

windows下mysql修改 my.ini的datadir后 `Access denied`

1. 背景 window安装mysql数据库时,不能指定数据文件存放位置(默认安装路径 "C:/ProgramData")。 只能通过修改mysql.ini来更改数据文件存放目录。 2. 问题: 修改mysql.ini后,mysql 出现 "Access denied for user ‘root‘@‘localhost‘ (using passwor…

webpack安装sass

package.json文件 {"devDependencies": {"sass-loader": "^7.2.0","sass": "^1.22.10","fibers": "^4.0.1"} }这个不用webpack.config.js module.exports {module: {rules: [{test: /\.s[ac]ss$/i,u…

算法设计与分析:分治法求最近点对问题

目录 一、实验目的 二、实验内容 三、算法思想 四、实验步骤 1、蛮力法 2、分治法 2.1 先用快速排序SortX(A,1,n)将所有点按x坐标升序排序 2.2 点数n<3时直接计算&#xff0c;时间复杂度为O(1) 2.3 点数n>3时 五、实验结果和分析 一、实验目的 1. 掌握分治法思…

Ilya出走记:SSI的超级安全革命

图片&#xff5c;OpenAI官网 ©自象限原创 作者丨罗辑、程心 和OpenAI分道扬镳以后&#xff0c;Ilya“神秘而伟大”的事业终于揭开了面纱。 6月20日&#xff0c;前OpenAI核心创始人 Ilya Stuskever&#xff0c;在官宣离职一个月后&#xff0c;Ilya在社交媒体平台公开了…

SambaLingo——教会大模型新语言

在当今数字化时代&#xff0c;语言不仅是沟通的桥梁&#xff0c;也是信息和知识传递的核心。尽管大模型&#xff08;LLMs&#xff09;在处理英语等主流语言方面取得了显著进展&#xff0c;但它们在理解和生成其他语言内容方面的能力却参差不齐。这种不平衡限制了技术在全球范围…

Charles抓取安卓应用https包演示

一、准备软件 夜神安卓模拟器 (yeshen.com) Charles (charlesproxy.com) 二、配置抓包 2.1 Charles安装PC根证书 记住这里的ip端口 三、安卓模拟器配置 3.1 配置安卓客户端网络代理 填写上文的ip端口&#xff0c;保存 3.2 安装根证书 3.2.1 导出根证书 linux主机执行 op…

Springboot项目ES报异常query_shard_exception

详细异常信息如下&#xff1a; {"error": {"root_cause": [{"type": "query_shard_exception","reason": "failed to create query: {\n \"bool\" : {\n \"filter\" : [\n {\n \…

AST小工具|编写一个通用的js混淆代码美化工具

关注它&#xff0c;不迷路。 本文章中所有内容仅供学习交流&#xff0c;不可用于任何商业用途和非法用途&#xff0c;否则后果自负&#xff0c;如有侵权&#xff0c;请联系作者立即删除&#xff01; 一.问题 如题&#xff0c;如何编写一个通用的js混淆代码美化工具&…

R语言——R语言基础

1、用repeat、for、while计算从1-10的所有整数的平方和 2、编写一个函数&#xff0c;给出两个正整数&#xff0c;计算他们的最小公倍数 3、编写一个函数&#xff0c;让用户输入姓名、年龄&#xff0c;得出他明年的年龄。用paste打印出来。例如&#xff1a;"Hi xiaoming …

算法:渐进记号的含义及时间复杂度计算

渐进记号及时间复杂度计算 渐近符号渐近记号 Ω \Omega Ω渐进记号 Θ \Theta Θ渐进记号小 ο \omicron ο渐进记号小 ω \omega ω渐进记号大 O \Omicron O常见的时间复杂度关系 时间复杂度计算&#xff1a;递归方程代入法迭代法套用公式法 渐近符号 渐近记号 Ω \Omega Ω …

图扑助力铝型材挤压:数字孪生引领智慧管理

通过图扑数字孪生技术&#xff0c;为铝型材挤压车间提供实时监控和优化管理方案。高精度三维建模和数据可视化提升了生产效率和管理透明度&#xff0c;推动智能制造和资源优化配置。