【Redis】分布式锁的实现

目录

一、本地锁存在的问题

二、redis实现分布式锁原理

三、使用示例

四、锁误删问题

 解决思路

获取锁和释放锁代码优化

五、锁释放的原子性问题

解决思路(Lua脚本)

使用流程

总结


        大家好,我是千语。上期给大家讲了使用悲观锁来解决“一人一单”的并发场景。但上期使用的是一个本地锁,本地锁在集群模式下会失效。具体可以看一下我上一篇博客。


【并发问题】一人一单(悲观锁解决)-CSDN博客


一、本地锁存在的问题

在集群模式下,该项目会启动多个实例,且每个实例都会有各种的jvm。我们上面使用到的锁其实都是本地锁,所以就可能会出现这样的情况:

张三在进行并发地判断自己是否满足一人一单时,第一个请求被分配到了实例A,获取锁并判断到数据库中还没有改商品的订单,可以抢购,但当还没有完全提交事务到数据库时,即使还没有释放锁。

张三发送第二个请求被分配到了实例B,那么用户尝试获取锁时,是可以获取到的。然后判断到数据库没有订单,可以抢单的操作,这样又造成了一个用户抢到了多个订单的操作。

解析:因为每个实例都会有自己的JVM,而JVM里面都会有自己的锁监视器,并且每个实例的锁都是存储在它自己的jvm里面的,所以请求分配到不同的实例,锁监视器监视到的锁都是打开的状态。也就是说我们上面应用锁的方式只是在单机的情况下适用,集群模式下就不适用了。



二、redis实现分布式锁原理

        原理就是使用redis的setnx命令,这个命令是给redis里面set值,但是只有这个键不存在的时候才set,所以我们要获取锁时,setnx一个固定的键,获取锁成功;当其他线程也想要获取锁时,也使用setnx命令,这时候是set不到的,所以这个线程就获取锁失败。当业务执行完释放锁时,就把这个键删除就可以了。

图例:



三、使用示例

@Component
public class RedisLock {@Autowiredprivate RedisTemplate<String, String> redisTemplate;/*** 尝试获取分布式锁* @param lockKey 锁的键* @param expireTime 过期时间* @param timeUnit 时间单位* @return 获取锁成功与否*/public String tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {// 使用setIfAbsent方法尝试获取锁(对应Redis的SETNX命令)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, timeUnit);//设置锁超时时间,避免死锁return locked != null && locked;  //set成功表明获取锁成功}/*** 释放分布式锁* @param lockKey 锁的键* @return 是否释放成功*/public boolean releaseLock(String lockKey) {return redisTemplate.delete(lockKey)}
}

业务中实际加锁操作:


public String lockTest(){String lockKey = "product_stock_lock";try {// 尝试获取锁,超时时间10秒,锁持有时间30秒lockValue = redisLockHelper.tryLock(lockKey, 30, TimeUnit.SECONDS);if (lockValue != null) {// 获取锁成功,执行业务逻辑System.out.println("获取锁成功,处理库存扣减...");// 模拟业务处理Thread.sleep(5000); return "库存扣减成功";} else {// 获取锁失败return "系统繁忙,请稍后重试";}} catch (InterruptedException e) {Thread.currentThread().interrupt();return "操作被中断";} finally {// 释放锁(只有持有锁的线程才能释放)if (lockValue != null) {boolean released = redisLockHelper.releaseLock(lockKey, lockValue);System.out.println("锁释放结果: " + released);}}}



四、锁误删问题

        在上述的使用示例当中,实际上会存在锁误删的问题。具体如下:

  1. 线程1获取锁成功,执行业务代码后阻塞,未执行到手动释放锁的操作,锁超时后自动释放了
  2. 由于锁超时被释放,线程2获取锁成功,执行业务
  3. 线程1阻塞过后,继续执行任务,执行了释放锁操作。但此时锁其实是线程2的,由于没有做判断,线程1执行了释放锁的操作。
  4. 由于锁已经被线程1释放,线程3可以获取锁,执行业务。
  5. 结果:线程2和线程3都同时在执行了只能单个线程执行的业务。

图例:


 解决思路

获取锁时,判断一下标识是否一致;

setnx时,value的值可以设置成当前线程的name或者id

因为线程idjvm里面是自增的,所以在集群模式下,多个jvm可能会存在id相同的线程,所以也是会冲突的,所以id不可行,往下看。

所以可以使用uuid+线程id作为锁的标识

当要释放锁时,先获取锁的值,如果是自己当前的线程id,再进行释放锁


获取锁和释放锁代码优化

@Component
public class RedisLockHelper {@Autowiredprivate RedisTemplate<String, String> redisTemplate;//生成当前锁持有者的唯一标识的uuid前缀private static final String ID_PREFIX= UUID.randomUUID().toString(true) + "-";/*** 尝试获取分布式锁* @param lockKey 锁的键* @param expireTime 过期时间* @param timeUnit 时间单位* @return 锁的唯一标识,获取失败时为null*/public String tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {// 使用UUID前缀+当前线程id作为锁持有者的唯一标识String lockValue = ID_PREFIX + Thread.currentThread().getid();// 使用setIfAbsent方法尝试获取锁(对应Redis的SETNX命令)Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, expireTime, timeUnit);return locked != null && locked ? lockValue : null;}/*** 释放分布式锁* @param lockKey 锁的键* @return 是否释放成功*/public boolean releaseLock(String lockKey) {//获取当前线程的标识String currentThreadLock = ID_PREFIX + Thread.currentThread().getid();// 获取分布式锁内的锁标识String lockValue = redisTemplate.opsForValue().get(lockKey)  //释放锁时,先判断该锁是不是当前线程持有的      if(currentThreadLock.equals(lockValue)) {//如果当前线程是锁的持有者,就释放锁return redisTemplate.delete(lockKey);}else{return false;}}
}

 业务层使用锁的代码不需要修改



五、锁释放的原子性问题

上一个问题是执行业务时线程阻塞,阻塞结束后误删了锁。

所以我们在释放锁前先判断一下标识,看是否是当前线程的锁再释放就可以解决

但是,当我们判断完标识是一致后,线程1在进行释放锁之前被阻塞了(由于这两者不是原子性)

等到锁过期,其他线程成功获取锁执行业务,那么线程1又误删了锁:

图例


解决思路(Lua脚本)

使用Lua脚本,在脚本里面写一系列操作,然后使用redis客户端调用该脚本,这些操作就会一次性执行,满足原子性。


使用流程

(1)创建并填写Lua脚本文件:

注意:Lua脚本是使用lua语言来写的。具体可以去看一下语法内容,下面只给出一种解决思路和大概的解决流程。后续可以使用redission来简化这些操作


(2)读取lua脚本,形成一个RedisScript,便于后续调用api


(3)执行Lua脚本,释放锁


(4)锁使用:

业务中使用锁的方法都不需要边



总结

  1. 分布式锁利用set nx ex的原理。(set nx的互斥性,ex保证超时释放锁,避免死锁)
  2. 释放锁时要看看锁是不是该线程的持有者,避免误删
  3. 使用Lua脚本满足一组操作的原子性

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

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

相关文章

Unity3D对象池设计与实现详解

前言 在Unity3D中&#xff0c;对象池&#xff08;Object Pooling&#xff09;是一种优化技术&#xff0c;用于减少频繁实例化和销毁对象带来的性能开销。以下是对象池的详细设计和实现步骤&#xff1a; 对惹&#xff0c;这里有一个游戏开发交流小组&#xff0c;希望大家可以点…

[Spring]-组件的生命周期

组件生命周期 认识组件的声明周期 实验1 通过Bean指定组件的生命周期 package com.guigu.spring.ioc.bean;Data public class User {private String username;private String password;private Car car;Autowiredpublic void setCar(Car car) {System.out.println("自动…

【golang】网络数据包捕获库 gopacket

详解 github.com/google/gopacket/pcap 包 github.com/google/gopacket/pcap 是 Go 语言中一个强大的网络数据包捕获库&#xff0c;它是 gopacket 项目的一部分&#xff0c;提供了对 libpcap&#xff08;Linux/Unix&#xff09;和 WinPcap&#xff08;Windows&#xff09;的 G…

RBTree的模拟实现

1&#xff1a;红黑树的概念 红⿊树是⼀棵⼆叉搜索树&#xff0c;他的每个结点增加⼀个存储位来表⽰结点的颜⾊&#xff0c;可以是红⾊或者⿊⾊。通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束&#xff0c;红⿊树确保没有⼀条路径会⽐其他路径⻓出2倍&#xff0c;因…

React 第三十九节 React Router 中的 unstable_usePrompt Hook的详细用法及案例

React Router 中的 unstable_usePrompt 是一个用于在用户尝试离开当前页面时触发确认提示的自定义钩子&#xff0c;常用于防止用户误操作导致数据丢失&#xff08;例如未保存的表单&#xff09;。 一、unstable_usePrompt用途 防止意外离开页面&#xff1a;当用户在当前页面有…

OSI 7层模型

OSI 7层模型&#xff1a; 1、物理层&#xff08;光纤等把电脑连接起来的物理手段&#xff09; 2、数据链路层&#xff08;以太网&#xff0c;确认0和1电信号的分组方式&#xff0c;负责MAC地址&#xff0c;MAC地址用于在网络中唯一标示一个网卡&#xff0c;相当于网卡的身份证…

视频编解码学习十一之视频原始数据

一、视频未编码前的原始数据是怎样的&#xff1f; 视频在未编码前的原始数据被称为 原始视频数据&#xff08;Raw Video Data&#xff09;&#xff0c;主要是按照帧&#xff08;Frame&#xff09;来组织的图像序列。每一帧本质上就是一张图片&#xff0c;通常采用某种颜色格式…

Redis学习打卡-Day1-SpringDataRedis、有状态无状态

Redis的Java客户端 Jedis 以 Redis 命令作为方法名称&#xff0c;学习成本低&#xff0c;简单实用。Jedis 是线程不安全的&#xff0c;并且频繁的创建和销毁连接会有性能损耗&#xff0c;因此推荐使用 Jedis 连接池代替Jedis的直连方式。 lettuce Lettuce是基于Netty实现的&am…

告别静态配置!Spring Boo动态线程池实战指南:Nacos+Prometheus全链路监控

一、引言 1.1 动态线程池的必要性 传统线程池的参数&#xff08;如核心线程数、队列容量&#xff09;通常通过配置文件静态定义&#xff0c;无法根据业务负载动态调整。例如&#xff0c;在电商大促场景中&#xff0c;流量可能瞬间激增&#xff0c;静态线程池容易因配置不合理导…

Flask如何读取配置信息

目录 一、使用 app.config 读取配置 二、设置配置的几种方式 1. 直接设置 2. 从 Python 文件加载 3. 从环境变量加载 4. 从字典加载 5. 从 .env 文件加载&#xff08;推荐开发环境用&#xff09; 三、读取配置值 四、最佳实践建议 在 Flask 中读取配置信息有几种常见方…

【React中useCallback钩子详解】

useCallback 是 React 中的一个性能优化 Hook,用于缓存函数引用,避免在组件重新渲染时重复创建相同的函数,从而减少不必要的子组件渲染或副作用执行。以下是其核心要点: 1. 核心作用 函数记忆化:返回一个记忆化的回调函数,仅在依赖项变化时重新创建函数,否则复用之前的函…

【!!!!终极 Java 中间件实战课:从 0 到 1 构建亿级流量电商系统全链路解决方案!!!!保姆级教程---超细】

终极 Java 中间件实战课:电商系统架构实战教程 电商系统架构实战教程1. 系统架构设计1.1 系统模块划分1.2 技术选型2. 环境搭建2.1 开发环境准备2.2 基础设施部署3. 用户服务开发3.1 创建Maven项目3.2 创建用户服务模块3.3 配置文件3.4 实体类与数据库设计3.5 DAO层实现3.6 Se…

C#异步Task,await,async和Unity同步协程

标题 TaskawaitasyncUnity协程 Task Task是声明异步任务的必要关键字&#xff0c;也可以使用Task<>泛型来定义Task的返回值。 await await是用于等待一个Task结束&#xff0c;否则让出该线程控制权&#xff0c;让步给其他线程&#xff0c;直到该Task结束才往下运行。 …

【USRP】在linux下安装python API调用

UHD 源码安装 安装库 sudo apt-get install autoconf automake build-essential ccache cmake cpufrequtils doxygen ethtool \ g git inetutils-tools libboost-all-dev libncurses5 libncurses5-dev libusb-1.0-0 libusb-1.0-0-dev \ libusb-dev python3-dev python3-mako …

什么是 NoSQL 数据库?它与关系型数据库 (RDBMS) 的主要区别是什么?

我们来详细分析一下 NoSQL 数据库与关系型数据库 (RDBMS) 的主要区别。 什么是 NoSQL 数据库&#xff1f; NoSQL (通常指 “Not Only SQL” 而不仅仅是 “No SQL”) 是一类数据库管理系统的总称。它们的设计目标是解决传统关系型数据库 (RDBMS) 在某些场景下的局限性&#xf…

蓝桥杯题库经典题型

1、数列排序&#xff08;数组 排序&#xff09; 问题描述 给定一个长度为n的数列&#xff0c;将这个数列按从小到大的顺序排列。1<n<200 输入格式 第一行为一个整数n。 第二行包含n个整数&#xff0c;为待排序的数&#xff0c;每个整数的绝对值小于10000。 输出格式 输出…

wordpress自学笔记 第三节 独立站产品和类目的三种展示方式

wordpress自学笔记 摘自 超详细WordPress搭建独立站商城教程-第三节 独立站产品和类目的三种展示方式&#xff0c;2025 WordPress搭建独立站教程#WordPress建站教程https://www.bilibili.com/video/BV1rwcteuETZ?spm_id_from333.788.videopod.sections&vd_sourcea0af3b…

智能手表蓝牙 GATT 通讯协议文档

以下是一份适用于智能手表的 蓝牙 GATT 通讯协议文档&#xff0c;适用于 BLE 5.0 及以上标准&#xff0c;兼容 iOS / Android 平台&#xff1a; 智能手表蓝牙 GATT 通讯协议文档 文档版本&#xff1a;V1.0 编写日期&#xff1a;2025年xx月xx日 产品型号&#xff1a;Aurora Wat…

Linux PCI 驱动开发指南

注&#xff1a;本文为 “Linux PCI Drivers” 相关文章合辑。 英文引文&#xff0c;机翻未校。 中文引文&#xff0c;略作重排。 如有内容异常&#xff0c;请看原文。 How To Write Linux PCI Drivers 翻译: 司延腾 Yanteng Si siyantengloongson.cn 1. 如何写 Linux PCI 驱动 …

Python 接入DeepSeek

不知不觉DeepSeek已经火了半年左右&#xff0c;冲浪都赶不上时代了。 今天开始学习。 本文旨在使用Python调用DeepSeek的接口&#xff08; 这里写目录标题 一、环境准备1.1 DeepSeek1.2 Python 二、接入DeepSeek2.1 参数2.2 requests2.3 openai2.4 返回示例 一、环境准备 1.1…