Redis实战—Redis分布式锁

news2024/11/24 7:07:01

 本博客为个人学习笔记,学习网站与详细见:黑马程序员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集群保证高可用和高并发特性(本文未涉及)

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

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

相关文章

transdreamer 论文阅读笔记

这篇文章是对dreamer系列的改进&#xff0c;是一篇world model 的论文改进点在于&#xff0c;dreamer用的是循环神经网络&#xff0c;本文想把它改成transformer&#xff0c;并且希望能利用transformer实现并行训练。改成transformer的话有个地方要改掉&#xff0c;dreamer用ht…

Large Language Model based Multi-Agents: A Survey of Progress and Challenges

目录 摘要简介背景单一智能体系统单智能体 vs .多智能体系统 剖析多智能体系统&#xff1a;接口、剖析、通信和能力智能体 - 环境接口智能体画像智能体通信能力获取 摘要 大型语言模型( Large Language Models&#xff0c;LLMs )在各种任务中都取得了令人瞩目的成功。由于LLMs…

Linux搭建我的世界乌托邦探险之旅3.2整合包服务端,Minecraft开服教程

Linux服务器使用MCSM10 搭建 我的世界 乌托邦探险之旅3.2 整合包 服务端 的教程&#xff0c;Minecraft整合包开服教程。 大型养老探险整合包&#xff1a;乌托邦探险之旅3.2&#xff0c;探索上千种结构&#xff0c;造访丰富的自然群系&#xff0c;欣赏生动的生物动画&#xff0…

Android SurfaceFlinger——屏幕热插拔回调(九)

上一篇文章分析了回调注册监听的调用流程&#xff0c;对于数据的回调正好是注册监听的逆向调用。首先前面提到过在 HWC2On1Adapter 中就会直接转型为每一个回调到上层&#xff0c;这里我们就看一下屏幕热插拔回调&#xff08;hotplugHook&#xff09;的调用流程。 一、硬件回调…

数据库系统概论——数据库恢复技术

文章目录 数据库恢复技术事务的基本概念什么是事务如何定义事务&#xff1a;事务的特性 数据库恢复概述故障的种类恢复的实现技术恢复策略事务故障的恢复系统故障的恢复介质故障的恢复 数据库恢复技术 事务的基本概念 什么是事务 事务使用户定义的一个数据库操作序列&#x…

步步精:连接器领域的卓越品牌

自1987年成立以来&#xff0c;步步精坐落于美丽的旅游城市——温州市乐清虹桥镇&#xff0c;被誉为“国家电子主体生产基地”、“国家精密模具制造基地”。公司拥有7大厂区、9大事业部&#xff0c;800名专职员工&#xff0c;致力于提供高品质的连接器解决方案。注册商标“BBJCO…

数据库精选题(三)(SQL语言精选题)(按语句类型分类)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;数据库 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 前言 创建语句 创建表 创建视图 创建索引…

【机器学习】基于Softmax松弛技术的离散数据采样

1.引言 1.1.离散数据采样的意义 离散数据采样在深度学习中起着至关重要的作用&#xff0c;它直接影响到模型的性能、泛化能力、训练效率、鲁棒性和解释性。 首先&#xff0c;采样方法能够有效地平衡数据集中不同类别的样本数量&#xff0c;使得模型在训练时能够更均衡地学习…

开启声音的奇幻之旅:AI声音变换器的魔法秘籍与创意应用

AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频https://aitools.jurilu.com/这个充满科技魔力的时代&#xff0c;AI Voice Changer 就像一把神奇的钥匙&#xff0c;能为我们打开声音的魔法之门。今天&#xff0c;就让我带你…

在线教育系统源码入门:教育培训小程序开发全流程

本篇文章&#xff0c;笔者将详细介绍在线教育系统源码的入门知识&#xff0c;并带领大家了解教育培训小程序的开发全流程。 一、在线教育系统的基本概念 一个完整的在线教育系统应具备以下几个模块&#xff1a; 用户管理 课程管理 教学互动 支付模块 数据统计 二、开发工…

【开发】内网穿透ztncui搭建私有节点

文章目录 写在前面一键部署ztnuci记录后续 写在前面 前面搭建moon节点转发的确会降低延迟&#xff0c;但是总有出现moon节点解析不成功的例子&#xff0c;于是疯狂寻找答案是为什么&#xff1f;终于在知乎上找到这样一个答案。 一键部署ztnuci 参考这篇很完善的教程和贴心的…

小i机器人:总负债5.31亿,员工数量在减少,银行借款在增加,净利润已下降-362.68%

小i机器人:总负债5.31亿,员工数量在减少,银行借款在增加,总收入在增长,净利润已下降-362.68% 来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 目录 一、小i机器人公司介绍 二、小i机器人过去20年的发展历程和取得的成就 三、小i机器人的产品和技术架构 四、小i机器人…

动手学深度学习(Pytorch版)代码实践 -卷积神经网络-24深度卷积神经网络AlexNet

24深度卷积神经网络AlexNet import torch from torch import nn import liliPytorch as lp import liliPytorch as lp import matplotlib.pyplot as pltdropout1 0.5 #Alexnet架构 net nn.Sequential(nn.Conv2d(1, 96, kernel_size11, stride4, padding1),nn.ReLU(),nn.MaxPo…

29-Linux--守护进程

一.基础概念 1.守护进程&#xff1a;精灵进程&#xff0c;在后台为用户提高服务&#xff0c;是一个生存周期长&#xff0c;通常独立于控制终端并且周期性的执行任务火处理事件发生 2.ps axj&#xff1a;查看守护进程 3.进程组&#xff1a;多个进程的集合&#xff0c;由于管理…

# 消息中间件 RocketMQ 高级功能和源码分析(十一)

消息中间件 RocketMQ 高级功能和源码分析&#xff08;十一&#xff09; 一、消息中间件 RocketMQ 源码分析&#xff1a; 拉取消息长轮询机制 1、消息拉取长轮询机制分析 RocketMQ 未真正实现消息推模式&#xff0c;而是消费者主动向消息服务器拉取消息&#xff0c;RocketMQ …

LeetCode80. 删除有序数组中的重复项 II题解

LeetCode80. 删除有序数组中的重复项 II题解 题目链接&#xff1a; https://leetcode.cn/problems/remove-duplicates-from-sorted-array-ii/ 题目描述&#xff1a; 给你一个有序数组 nums &#xff0c;请你 原地 删除重复出现的元素&#xff0c;使得出现次数超过两次的元素…

基于uniapp的h5接入企业微信客服在线聊天

首先说下企业微信接入场景,支持的接入场景有以下几种,基本上涵盖了微信生态大部分场景: 接入步骤 1.创建企业微信号 按照官方操作步骤注册,需要注意的是未认证仅支持接入100人,已认证支持接入2000人. 2.创建客服账号 每个客服账号支持配置人工或是机器人或是人工机器人回复…

艾尔登法环黄金树幽影/ELDEN RING Shadow of the Erdtree(全DLC)

百度网盘 https://pan.baidu.com/s/1ulKiNQdVtV5dto-Vm7k4IA 提取码&#xff1a;yqrs 游戏介绍 《艾尔登法环》是一款以正统黑暗奇幻世界为舞台的动作RPG游戏。走进辽阔的场景与地下迷宫探索未知&#xff0c;挑战困难重重的险境&#xff0c;享受克服困境时的成就感吧。…

【单元测试】Spring Boot 的测试库

Spring Boot 的测试库 1.了解回归测试框架 JUnit2.了解 assertThat3.了解 Mockito4.了解 JSONPath5.测试的回滚 单元测试&#xff08;unit test&#xff09;是为了检验程序的正确性。一个单元可能是单个 程序、类、对象、方法 等&#xff0c;它是应用程序的最小可测试部件。 单…

如何修复“AI的原罪”

如何修复“AI的原罪” 上个月&#xff0c;《纽约时报》声称&#xff0c;科技巨头OpenAI和谷歌不顾服务条款和版权法的禁止&#xff0c;将大量YouTube视频转录成文本&#xff0c;并将其用作人工智能模型的额外训练数据&#xff0c;从而进入了版权灰色地带。《纽约时报》还援引Me…