Redis分布式锁的优化

分布式锁

分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

分布式锁的实现

分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:

MySQLRedisZookeeper
互斥利用mysql本身的互斥锁机制利用setnx这样的互斥命令利用节点的唯一性和有序性实现互斥
高可用
高性能一般一般
安全性断开连接,自动释放锁利用锁超时时间,到期释放临时节点,断开连接自动释放

基于 Redis 的分布式锁

用 Redis 实现分布式锁,主要应用到的是 SETNX key value命令(如果不存在,则设置)
主要要实现两个功能:

  1. 获取锁(设置一个 key)
  2. 释放锁 (删除 key)

基本思想是执行了 SETNX命令的线程获得锁,在完成操作后,需要删除 key,释放锁。
加锁:

@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);
    return Boolean.TRUE.equals(success);
}

释放锁:

@Override
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 释放锁
    stringRedisTemplate.delete(KEY_PREFIX + name);
}

可是这里会存在一个隐患——假设该线程发生阻塞(或者其他问题),一直不释放锁(删除 key)这可怎么办?

为了解决这个问题,我们需要为 key 设计一个超时时间,让它超时失效;但是这个超时时间的长短却不好确定:

  1. 设置过短,会导致其他线程提前获得锁,引发线程安全问题
  2. 设置过长,线程需要额外等待

锁的误删
引用
超时时间是一个非常不好把握的东西,因为业务线程的阻塞时间是不可预估的,在极端情况下,它总能阻塞到 lock 超时失效,正如上图中的线程1,锁超时释放了,导致线程2也进来了,这时候 lock 是 线程2的锁了(key 相同,value不同,value一般是线程唯一标识);假设这时候,线程1突然不阻塞了,它要释放锁,如果按照刚刚的代码逻辑的话,它会释放掉线程2的锁;线程2的锁被释放掉之后,又会导致其他线程进来(线程3),如此往复。。。
为了解决这个问题,需要在释放锁时多加一个判断,每个线程只释放自己的锁,不能释放别人的锁!
释放锁

@Override
public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

原子性问题

刚刚我们谈论的释放锁的逻辑:

  1. 判断当前锁是当前线程的锁
  2. 当前线程释放锁

可以看到释放锁是分两步完成的,如果你是对并发比较有感觉的话,应该一下子就知道这里会存在问题了。
分步执行,并发问题!
引用
假设 线程1 已经判断当前锁是它的锁了,正准备释放锁,可偏偏这时候它阻塞了(可能是 FULL GC 引起的),锁超时失效,线程2来加锁,这时候锁是线程2的了;可是如果线程1这时候醒过来,因为它已经执行了步骤1了的,所以这时候它会直接直接步骤2,释放锁(可是此时的锁不是线程1的了)

其实这就是一个原子性的问题,刚刚释放锁的两步应该是原子的,不可分的!

要使得其满足原子性,则需要在 Redis 中使用 Lua 脚本了。

引入 Lua 脚本保持原子性

lua 脚本:

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0

Java 中调用执行:

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;
    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);
        return Boolean.TRUE.equals(success);
    }@Override
    public void unlock() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
}

到了目前为止,我们设计的 Redis 分布式锁已经是生产可用的,相对完善的分布式锁了。

总结

这一次我们从秒杀场景的业务需求出发,一步步地利用 Redis 设计出一种生产可用的分布式锁:

实现思路:

  1. 利用set nx ex获取锁,并设置过期时间,保存线程标示
  2. 释放锁时先判断线程标示是否与自己一致,一致则删除锁 (Lua 脚本保证原子性)

有哪些特性?

  1. 利用set nx满足互斥性
  2. 利用set ex保证故障时锁依然能释放,避免死锁,提高安全性
  3. 利用Redis集群保证高可用和高并发特性

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

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

相关文章

LangChain-03 astream_events 流输出

内容简介 尝试用 FAISS 或 DocArrayInMemorySearch 将数据向量化后检索astream_events 的效果为 |H|arrison| worked| at| Kens|ho|.|| 安装依赖 # 之前的依赖即可 pip install --upgrade --quiet langchain-core langchain-community langchain-openai # Win或Linux用户可…

摸鱼toyaml.com更新

摸鱼https://toyaml.com/windowsupdate.html

一次MySQL事务的旅程:Buffer Pool, Binlog, Redo Log揭秘

MySQL中的各种Buffer和Log以及表空间 MySQL中一次事务涉及了各种Buffer,Log和表空间&#xff0c;主要涉及&#xff1a;Buffer Pool, Binlog, Undo Log, Redo Log以及表空间。 我们来探讨下。 Buffer Pool Buffer Pool主要存放在内存中&#xff0c;它是一个缓存区域&#xf…

36---USB HUB电路设计

视频链接 USB HUB电路设计01_哔哩哔哩_bilibili USB HUB 电路设计 1、USB HUB基本介绍 USB Hub&#xff0c;指的是一种可以将一个USB接口扩展为多个&#xff0c;并可以使这些接口同时使用的装置。 Hub也是大家常说的集线器&#xff0c;它使用星型拓扑结构连接多个USB接口设…

【御控物联】JavaScript JSON结构转换(17):数组To对象——键值互换属性重组

文章目录 一、JSON结构转换是什么&#xff1f;二、核心构件之转换映射三、案例之《JSON数组 To JSON对象》四、代码实现五、在线转换工具六、技术资料 一、JSON结构转换是什么&#xff1f; JSON结构转换指的是将一个JSON对象或JSON数组按照一定规则进行重组、筛选、映射或转换…

vue 条件渲染、列表循环渲染、事件绑定 初探第三天

条件渲染 <script>const app Vue.createApp({data(){return {show:true,conditionOne: false,conditionTwo: true,}},template:<div v-if"show"> hello word </div><div v-if"conditionOne"> if </div><div v-else…

HWOD:将字符串中的数字用*括起来

一、知识点 当需要类似括号( )这样成对出现的字符时&#xff0c;可以通过设置flag来标示 比如flag等于0表示前面所有的括号都是成对的 flag等于1表示最靠近的括号是未成对的&#xff1b;满足条件时&#xff0c;补齐括号&#xff0c;使其成对&#xff0c;flag置0 二、题目 …

如何展示科技产品的原理和应用

一、合理安排展示区域 不同的科技产品具有不同的展示需求&#xff0c;设计师需要根据展品的特点和大小&#xff0c;合理安排展示区域。对于较大的科技产品&#xff0c;可以设置特定的展台或展示区域&#xff0c;并配备合适的灯光和装饰&#xff0c;以凸显产品的重要性和独特性。…

matlab实现决策树可视化——信息增益、C4.5、基尼指数

代码&#xff1a;https://download.csdn.net/download/boyas/89074326

第十五章 Nginx

一、Nginx 1.1 Nginx 相关概念 1.1.1 正向代理 正向代理类似一个跳板机&#xff0c;代理访问外部资源。 比如我们国内访问谷歌&#xff0c;直接访问访问不到&#xff0c;我们可以通过一个正向代理服务器&#xff0c;请求发到代理服&#xff0c;代理服务器能够访问谷歌&am…

iOS开发进阶(十三):脚手架创建iOS项目

文章目录 一、前言二、xcode-select 命令三、拓展阅读 一、前言 项目初期&#xff0c;需要搭建项目基本框架&#xff0c;为此离不开辅助工具&#xff0c;即脚手架。当然&#xff0c;IDE也可以实现新建空白项目&#xff0c;但是其新建后的项目结构可能不符合预期设计&#xff0…

【论文阅读】Transformer 论文逐段精读

Transformer 论文逐段精读【论文精读】 文章目录 Transformer 论文逐段精读【论文精读】&#x1f4dd;摘要&#x1f4dc;结论&#x1f4cc;引言⏱️相关工作⭐模型Overview3.1 Encoder and Decoder Stacks3.2 Attention3.2.1 Scaled Dot-Product Attention3.3.2 Multi-head att…

如何系统地自学Python

1、如何系统地自学Python 小白的话可以快速过一下某马&#xff0c;某谷。 主要关注Python有什么集合&#xff0c;里面的集合怎么使用 然后再找一个Python爬虫实战视频&#xff0c;先跟着视频敲一遍代码&#xff0c;然后再尝试自己做一遍 然后再找一个Python服务开发视频&am…

FPGA + 图像处理 (二) RGB转YUV色域、转灰度图及仿真

前言 具体关于色域的知识就不细说了&#xff0c;简单来讲YUV中Y通道可以理解为就是图像的灰度图&#xff0c;因此&#xff0c;将RGB转化为YUV是求彩色图的灰度直方图、进行二值化操作等的基础。 HDMI时序生成模块 这里先介绍一下仿真时用于生成HDMI时序&#xff0c;用这个时…

Flutter 开发学习笔记(4):widget布局容器学习

文章目录 前言相关链接Widget 有状态和无状态Flutter 代码风格去掉烦人的括号后缀提示代码缩进 Flutter 布局最简单的布局widgets和Material widgets Dark语法习惯Flutter 布局默认布局Center居中Padding 填充Align对齐默认居中顶部底部右上角 通用 WidgetContainer处于性能原因…

波奇学Linux:tcp滑动窗口

连接建立成功和上层有没有accept没有关系 listen的第一个参数为1&#xff0c;建立的连接数量为2 操作系统底层用队列来管理建立的连接&#xff0c;上层用accept来获取链接 blacklog1 表示底层已经建立好的连接队列的最大长度 超过最大长度的连接&#xff0c;服务端不会丢弃…

STM32CubeMX配置步骤详解零 —— 引言

引子 初识 笔者接触STM32系列MCU有些年头了。初次接触是2015年&#xff0c;那时是在第二空间&#xff08;北京&#xff09;科技有限公司上班&#xff0c;是以STM32F407&#xff08;后缀好像是RGT6或ZGT6&#xff0c;记得不是很清楚了&#xff09;为主芯片做VR头戴式设备&…

SpringBoot整合Activiti7——实战之出差流程(分支)

文章目录 代码实现部署流程启动流程查询任务填写出差审批单经理审批xml文件 出差流程&#xff1a;开始 - 填写出差表单 - 判断&#xff08;出差天数大于等于5&#xff09;- 副经理审批 - 否则总经理审批 - 完成 代码实现 部署流程 Testpublic void testDeployProcess() throws …

物联网实战--入门篇之(九)安卓QT--开发框架

目录 一、QT简介 二、开发环境 三、编码风格 四、设计框架 五、总结 一、QT简介 QT是一款以C为基础的开发工具&#xff0c;已经包含了很多常用的库&#xff0c;除了基本的GUI以外&#xff0c;还有网络、数据库、多媒体、进程通信、串口、蓝牙等常用库&#xff0c;开发起来…

mysql 正则表达式查询

学习了mysql 连接查询和子查询和myql join连接&#xff0c;接下来学习下正则表达式查询。正则表达式的规则都是相似的。 8&#xff0c;使用正则表达式查询 正则表达式通常被用来检索或替换那些符合某个模式的文本内容,根据指定的匹配模式匹配文本中符合要求的特殊字符串。例如从…