如何用 Redis 实现一个分布式锁

2023-11-06

场景模拟

一般电子商务网站都会遇到如团购、秒杀、特价之类的活动,而这样的活动有一个共同的特点就是访问量激增、上千甚至上万人抢购一个商品。然而,作为活动商品,库存肯定是很有限的,如何控制库存不让出现超买,以防止造成不必要的损失是众多电子商务网站程序员头疼的问题,这同时也是最基本的问题。

在秒杀系统设计中,超卖是一个经典、常见的问题,任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难点。

针对大量的并发请求,我们可以通过 Redis 来抗,也就是说对于库存直接请求 Redis 缓存,不直接请求数据库,如在 Redis 中有 50 个库存,如下:

image-20220309110319351

但不管是缓存还是数据库,在不做任何处理的情况下,都会出现超买的问题,常见的处理方式就是在代码中通过JVM 加锁的方式,如下:

server1

@RestController
public class SkillController {

    @Autowired
    private RedisTemplate redisTemplate;

    // 秒杀接口
    @RequestMapping("/deduct_stock")
    public String deductStock() {

        // 加锁
        synchronized (this) {
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
            if (stock > 0) {
                // 库存 -1
                int realStock = stock - 1;
                // 扣减库存
                redisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        }

        return "8080";
    }
}

当然,在单机情况下确实没有任何问题,但现在绝大多数系统都是分布式系统,就算是 ERP 系统也会部署 2 台机器防止单点故障,所以一般情况下一个请求如下:

image-20220309110832380

server2

server2 和 server1 代码基本相同,只是开启了 2 个 JVM 实例。

@RestController
public class SkillController {

    @Autowired
    private RedisTemplate redisTemplate;

    // 秒杀接口
    @RequestMapping("/deduct_stock")
    public String deductStock() {

        // 加锁
        synchronized (this) {
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
            if (stock > 0) {
                // 库存 -1
                int realStock = stock - 1;
                // 扣减库存
                redisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        }

        return "8090";
    }
}

Nginx

一般来说,前端通过 nginx 请求转发并通过 upstream 实现负载均衡,其关键配置如下:

image-20220309111849631

Jmeter

我们这里通过 Jmeter 来进行并发压测,不会用的参考 Jmeter 使用,然后这里提供下载链接:Jmeter 下载 (提取码:2hyo)。

并发请求:1 s 内 200 个请求(模拟高并发),循环 5 次,一共 1000 个总请求。

image-20220309113854795

请求地址:秒杀的减库存接口。

image-20220309113953340

JVM 锁

了解了上面的配置,然后启动 2 个实例,端口分别为 8080,8090,如下:

image-20220309112256004

如过不知道如何启动 2 个实例的看下面:

image-20220309112521469

注意:要修改启动端口。

使用 JVM 锁也就是同步代码块的方式存在问题,如上面测试的结果如下:

image-20220309135924299

不仅 2 个服务同时存在相同的库存,甚至同一个服务也存在相同的值,很明显在高并发分布式场景下,JVM 层面的锁是不可行的。

Redis SETNX

SETNX

格式:setnx key value

将 key 的值设为 value ,当且仅当 key 不存在。

若给定的 key 已经存在,则 SETNX 不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

1、实现一个最简单的分布式锁

@RestController
public class SkillController {

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/deduct_stock")
    public String deductStock() {

        // 商品 ID,具体应用中应该是请求传入的
        String lockKey = "lock:product_01";
        // SETNX 加锁
        Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "product");
        
        // 如果为 false 说明这把锁存在,直接返回
        if (!result) {
            // 模拟返回业务
            return "系统繁忙";
        }
        int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
        if (stock > 0) {
            // 库存 -1
            int realStock = stock - 1;
            // 扣减库存 模拟其他更多业务操作
            redisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }

        // 加锁后需要释放锁
        redisTemplate.delete(lockKey);
        
        return "8080";
    }
}

2、存在的问题

① 业务代码异常—死锁

在实际的场景中,一次秒杀过程涉及到很多的业务操作,如果在释放锁之前的某个业务操作抛异常,使得锁没有被释放,那么此时就会存在死锁问题。此时这个 key 永远存在于 redis 中,其它线程执行 SETNX 永远失败。

也就是说我们要保证释放锁得到执行,所以要把上面的业务代码放在 try catch 或者 try finally 中:

image-20220309151042583

② Redis 宕机

但其实上面的代码并不一定能完全解决问题,如果 Redis 宕机或者被重启,同样会导致 finally 中的代码执行失败,结果就同上了。

所以一般我们需要给这个 key 设置过期时间,即:

Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "product");

redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);

但上面的写法存在原子性问题,所以我们不能分开来写,得合成一条命令:

Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "product", 10, TimeUnit.SECONDS);

③ 高并发可能存在的问题

一般来说,并发量不大的情况下,上面的写法已经满足要求,但对于几千上万的并发,可能会导致接口的响应变慢,比如:

某个请求 A 执行完整个操作需要 15s ,而我们上面设置的超时时间为 10s ,所以此时请求 A 并没有执行完,但由于设置了过期时间把 key 给删掉了,然后这时再来一个请求 B,是可以加锁成功的,且在 8s 内执行完成,而 B 在执行过程中 A 同样也在执行,如果此时 A 可能先于 B 去执行 finally 中的代码把锁给删除了,但 A 删除的锁并不是它的,而是 B 加的锁,同理,当请求 C 加锁后又被请求 B 给释放了,也就是说,这把分布式锁直接无效了(尽管可能性很小),同样会出现超买问题。

这个问题的根本在于:自己加的锁被被别人释放了

因此我们可以确定 value 值唯一性,如 UUID,如下:

image-20220309155219774

但这种方式同样存在原子问题,也就是上图中 ② 处的代码,结果也会导致自己加的锁被被别人释放

Redisson

针对上面的问题,我们可以通过 Redisson 来解决,其使用非常简单,和 JDK 中的 Lock 使用类似,如下:

@RestController
public class RedissonController {

    @Autowired
    private Redisson redisson;

    @Autowired
    private RedisTemplate redisTemplate;

    @RequestMapping("/deduct_stock1")
    public String deductStock() {

        // 商品 ID,具体应用中应该是请求传入的
        String lockKey = "lock:product_01";

        // 获取锁
        RLock lock = redisson.getLock(lockKey);
        // 加锁
        lock.lock();

        try {
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
            if (stock > 0) {
                // 库存 -1
                int realStock = stock - 1;
                // 扣减库存
                redisTemplate.opsForValue().set("stock", realStock + "");
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            // 释放锁
            lock.unlock();
        }

        return "8080";
    }
}

再去测试就是正常的了:

image-20220310091303423

Redisson 其原理如下:

image-20220309165722140

Redisson 锁的加锁机制如上图所示,线程去获取锁,获取成功则执行保存数据到 redis 数据库。如果获取失败,则一直通过 while 循环尝试获取锁(可自定义等待时间,超时后返回失败),获取成功后,保存数据到 redis 数据库。

Redisson 提供的分布式锁是支持锁自动续期的(锁续命),也就是说,如果线程仍旧没有执行完,那么 Redisson 会自动给 redis 中的目标 key 延长超时时间,这在 Redisson 中称之为 Watch Dog(看门狗)机制。

那么 redisson 是怎么实现原子性的

当然是 lua。不管是加锁操作,还是看门狗机制都是通过 lua来保证其原子性。

其加锁调用链路如下:

RedissonLock.lock()--->lockInterruptibly()--->tryAcquire()--->tryLockInnerAsync()

关键代码就在 tryLockInnerAsync() 中:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
    		  // 如果锁不存在,则通过hset设置它的值,并设置过期时间
              "if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              // 如果锁已存在,其是当前线程,则通过hincrby给数值递增1,即锁的重入
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              // 如果锁已存在,不是当前线程,则返回过期时间 ttl
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

那么对于锁自动续期如下:

RedissonLock.lock()--->lockInterruptibly()--->tryAcquire()--->scheduleExpirationRenewal()

scheduleExpirationRenewal() 方法会开启一个子线程去执行自动延期的操作,当然也是执行 lua代码,如下,截取关键部分:

// getName()就是当前锁的名字 
RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        // 判断这个锁 getName() 是否在redis中存在,如果存在就进行 pexpire 延期 默认30s
        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
            "return 1; " +
        "end; " +
        "return 0;",
          Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

他会判断这个锁 getName() 是否在 redis 中存在,如果存在就进行 pexpire 延期,默认lockWatchdogTimeout=30s,且是每间隔lockWatchdogTimeout/3=10s时间,去执行延时操作。

源码:https://gitee.com/javatv/redis.git

参考:redisson 中的看门狗机制

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何用 Redis 实现一个分布式锁 的相关文章

  • 用Java将图像添加到数据库

    我正在尝试将图像添加到 mysql 数据库中的 BLOB 字段 图像大小将小于 100kb 但是我遇到了问题 想知道将这些数据添加到数据库的更好方法是什么 com mysql jdbc MysqlDataTruncation 数据截断 第
  • 在 Java 中实现排列算法的技巧

    作为学校项目的一部分 我需要编写一个函数 该函数将接受整数 N 并返回数组 0 1 N 1 的每个排列的二维数组 该声明看起来像 public static int permutations int N 该算法描述于http www usn
  • 通过 RMI 的服务器,无需注册

    我有一个可以通过 RMI 连接的服务对象 目前我正在这样做 Server Registry r LocateRegistry createRegistry 1234 r bind server UnicastRemoteObject exp
  • 32 位数字中 1 的数量

    我正在寻找一种在 32 位数字中包含 1 数量的方法 之间不使用循环 任何人都可以帮助我并向我提供代码或算法吗 这样做 提前致谢 See Integer bitCount int http java sun com javase 6 doc
  • 如何在 Java 中用 \n 替换 \\n

    我有一个string test first n middle n last 现在我想更换所有 n by n 我试过了test replaceAll n n and test replaceAll n n 但它们不起作用 有人有解决办法吗 T
  • Android 3.1 USB 主机 - BroadcastReceiver 未收到 USB_DEVICE_ATTACHED

    我经历过USB 主机的描述和示例位于developer android com http developer android com guide topics usb host html检测连接和分离的 USB 设备 如果我在清单文件中使用
  • Redis是如何实现高吞吐量和高性能的?

    我知道这是一个非常普遍的问题 但是 我想了解允许 Redis 或 MemCached Cassandra 等缓存 以惊人的性能极限工作的主要架构决策是什么 如何维持连接 连接是 TCP 还是 HTTP 我知道它完全是用C写的 内存是如何管理
  • Java ZIP - 如何解压缩文件夹?

    是否有任何示例代码 如何将 ZIP 中的文件夹部分解压到我想要的目录中 我已将文件夹 FOLDER 中的所有文件读取到字节数组中 如何从其文件结构创建 我不确定你所说的部分是什么意思 您的意思是在没有 API 帮助的情况下自己完成吗 如果您
  • 在单独的模块中使用 Spring AOP 方面

    我在一个 Maven 项目模块中有一个方面 com x NiceAspect 在一个单独的 Maven 模块中有一个类 com x NiceClass 这些模块具有相同的 POM 父级 共同创建一个项目 我想要实现的目标是拥有一个通用的方面
  • 使用 https 的 Java Jersey RESTful Web 服务

    我是 Java EE 的新手 正在开发一个 RESTful API 其中每个 API 调用用户都会发送编码的凭据 我的问题是如何通过默认的 http 实现 https 协议并确保我的连接安全 我正在使用 Jersey Restful Web
  • 如果在构造函数中使用 super 调用重写方法会发生什么

    有两个班级Super1 and Sub1 超1级 public class Super1 Super1 this printThree public void printThree System out println Print Thre
  • 如何在打开导航抽屉时使背景 Activity 变小?

    我想做我的背景Activity打开时稍微小一点Navigation Drawer 模拟存在的效果Airbnb应用 我想最好的解释是截图 但重点不是让 View 变小 而是让它成为与 Drawer 打开 关闭动画同步的动画 因此 如果您开始打
  • Android:如何停止监听电话监听器? [复制]

    这个问题在这里已经有答案了 可能的重复 Android 为什么 PhoneCallListener 在活动完成后仍然存在 https stackoverflow com questions 11666853 android why phon
  • Spring Oauth2. DaoAuthenticationProvider 中未设置密码编码器

    我对 Spring Oauth 和 Spring Security 很陌生 我正在尝试在我的项目中使用 client credentials 流程 现在 我设法使用自己的 CustomDetailsS ervice 来从系统中已存在的数据库
  • 为什么我们在同一台服务器上使用多个应用程序服务器实例

    我想这是有充分理由的 但我不明白为什么有时我们会在同一物理服务器上放置例如 5 个具有相同 Web 应用程序的实例 这与多处理器架构的优化有关吗 JVM 或其他允许的最大内存限制 嗯 过了很长一段时间我又看到这个问题了 一台机器上的多个 J
  • 如何将模型从 ML Pipeline 保存到 S3 或 HDFS?

    我正在尝试保存 ML Pipeline 生成的数千个模型 正如答案中所示here https stackoverflow com questions 32121046 run 3000 random forest models by gro
  • JAXB 枚举字段未序列化

    我有以下课程 package dictionary import java io Serializable import java util Objects import javax xml bind annotation XmlEleme
  • POJO 支持使用omnifaces 自动完成primefaces

    我正在尝试在我的项目中使用 primefaces 自动完成组件 以避免将特定转换器写入我尝试使用的每个列表对象全能面孔 http showcase omnifaces org converters ListConverter如建议的here
  • Java:将秒转换为分钟、小时和天[重复]

    这个问题在这里已经有答案了 任务是 输出应如下所示 最好回显输入 您输入了 500 000 秒 即 5 天 18 小时 53 分钟 20 秒 5天18 53 20小时 我该怎么做呢 最容易理解和做到的方法是什么 讲师还说 没有硬编码 我不太
  • JAAS keytab 配置的相对路径

    我有一个系统 其中 NET 客户端使用 Kerberos 针对 Java 服务器进行身份验证 一切正常 但我正在尝试改进服务器配置 目前一个keytab根目录中需要文件C 因为我的jaas配置文件看起来像这样 Server com sun

随机推荐

  • 有趣的数据结构算法5——利用循环链表解决Josephus问题

    有趣的数据结构算法5 利用循环链表解决Josephus问题 题目复述 解题思路 实现代码 GITHUB下载连接 本次教程主要讲述如何利用循环链表解决Josephus问题 题目复述 据说著名犹太历史学家 Josephus有过以下的故事 在罗马
  • Selenium碰到的异常记录

    Java版本的Selenium异常记录 1 没有找到类的异常 NoClassDefFoundError 异常如下 Exception in thread main java lang NoClassDefFoundError com goo
  • 记录Android开发中SELINUX权限问题

    记录Android开发中SELINUX权限和用户权限问题 在安卓开发中 当linux内核中配置了SELINUX权限管理 访问硬件相关的设备文件 led tty等 时 如果没有对文件和访问文件的程序设置selinux的权限 就有可能报如下错误
  • 编写java程序151条建议读书笔记(20)

    建议139 大胆采用开源工具 MVC框架有Structs 也有Spring MVC WebWorker IoC容器有Spring 也有Coolgle Guice ORM有Hibernate MyBatis 日志有经典的log4j 崭新的lo
  • Java—RPC:远程过程调用(1)

    Java RPC 远程过程调用 1 在我们学习RPC的过程中 首先我们先认识一下项目结构在发展中干的变化 一 项目结构变化 1 单体结构 单体结构又叫单一项目 在我们所认识的传统项目基本上都是单一项目 j可是在互联网逐步发展的过程中 逐渐的
  • 卡通渲染技巧(三)——崩坏3卡通渲染实践

    系列链接 卡通渲染技巧 一 漫反射部分 卡通渲染技巧 二 高光部分 描边 卡通渲染技巧 三 崩坏3卡通渲染实践 耳听为虚眼见为实 不实际看一下你永远不知道技术分享里吹了多少牛 其实是没有实际应用到游戏里 前排赞美 SnapDragon Pr
  • 【手写一个RPC框架】simpleRPC-04

    目录 前言 实现 项目创建 配置依赖 common service server client 文件结构 运行 本项目所有代码可见 https github com weiyu zeng SimpleRPC 前言 之前谈到 网络传输使用BI
  • 音频——WAV 格式详解

    文章目录 WAV 文件格式解析 概述 块解析 RIFF chunk fmt chunk data chunk 示例分析 代码解析 WAV 文件格式解析 概述 wav 文件支持多种不同的比特率 采样率 多声道音频 WAV 文件格式是 Micr
  • 考研经验

    1 初试 考研初试准备的开始时间主要有两批 第一批是从3月份开始准备 第二批是从7月份开始准备 我属于前面那一批 接下来按照考研科目的顺序来讲一下我在考研初试准备的一些经验 政治 100分 题型 选择题 单选 多选 分析题 科目 马原 史纲
  • 修改Nuget默认包存放位置

    nuget默认的全局包下载地址一般为 C Users UserName nuget packages 项目多了之后 nuget下载的包就回慢慢的变多 导致c盘被大量占用 这时候我们想要将nuget的默认的包存放位置放在其其他的目录下面 可以
  • 边缘计算与智能服务

    随着信息化的不断发展 人们对互联网提出了更高的生活需求 5G 人工智能 物联网等新兴技术应运而生 万物互联已经成为一种新的发展趋势 网络技术不再只停留于原来的数字层面 在物质生活中可以提供更加智能化的服务帮助 而与物之间的密切交流带来的不仅
  • 三菱PLC N:N 通讯

    简介 三菱NN通讯是采用485通讯方式 只能用于COM1通讯口 其通讯是程序中设定好固定的模式以及站点号 参照软元件通讯表就可以由主站直接访问软元件寄存器来获取从站数据 要是从站之间进行数据交互 则必须从站先将数据发送到主站 再由主站发送至
  • Typora改变字体颜色

    方法一 下载AutoHotkey并创建快捷键的方法 推荐 第一步 在官网 https www autohotkey com 下载 AutoHotkey并傻瓜式安装 安装在任意盘符下均可 第二步 在安装目录下创建AutoHotKey ahk文
  • Proxmox虚拟环境(PVE)简介

    Proxmox虚拟环境 简称PVE 是用于操作来宾操作系统的基于Debian Linux和KVM的虚拟化平台 Proxmox免费提供 可以通过制造商 维也纳的Proxmox Server Solutions GmbH 购买商业支持 Prox
  • Eclipse查看java源代码

    第一步 点击Window下的Preferences 第二步 选择Java下的Installed JRES 鼠标点击右边的jre1 8 0 点击Edit 第三步 打开以rt jar结尾的jar包 双击Source attachment 如果是
  • python21天打卡Day12--for循环,列表推导式-构建列表

    for循环 a range从左开始 不包括右 如下输出1 100 for i in range 1 101 a append i print a 列表推导式 b i for i in range 1 101 print b D 学习 Pyt
  • 【神经网络搜索】ENAS:Efficient Neural Architecture Search

    GiantPandaCV导语 本文介绍的是Efficient Neural Architecture Search方法 主要是为了解决之前NAS中无法完成权重重用的问题 首次提出了参数共享Parameter Sharing的方法来训练网络
  • 浅谈web架构之架构设计

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 前言 题目有点大 所以不可能说得非常具体 笔者也不能驾驭全部 前面介绍过网站发展过程中架构的演化过程 本文主要针对网站架构各个方面的建设进行简单介绍 架构模式 先来说说模式
  • python语言程序设计_梁勇—第五章练习题重点题目答案

    1 统计正数和负数的个数后计算这些数的平均值 编写程序来读入不指定个数的整数 然后决定已经读取的整数中有多少个正数和负数并计算这些输入值 def calculate avg sum 0 positive 0 negative 0 while
  • 如何用 Redis 实现一个分布式锁

    场景模拟 一般电子商务网站都会遇到如团购 秒杀 特价之类的活动 而这样的活动有一个共同的特点就是访问量激增 上千甚至上万人抢购一个商品 然而 作为活动商品 库存肯定是很有限的 如何控制库存不让出现超买 以防止造成不必要的损失是众多电子商务网