【Redis】Redis实现分布式锁

news2024/11/24 2:50:37

【Redis】Redis实现分布式锁

文章目录

  • 【Redis】Redis实现分布式锁
    • 1. 分布式锁概念
    • 2. 为什么要实现分布式锁
      • 2.1 并发安全问题
    • 3. 分布式锁的实现方案
      • 3.1 Redis实现分布式锁
        • 3.1.1 定义分布锁接口和类
        • 3.1.2 编写lua脚本
        • 3.1.3 使用线程锁
        • 3.1.4 总结

在实现分布式锁之前,首先我们要明白什么是分布式锁?为什么要实现分布式锁?

1. 分布式锁概念

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-II4dqszM-1673770522700)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230115153704987.png)]


2. 为什么要实现分布式锁

设定一个场景:美团外卖的特价优惠卷一个用户只能购买一张,不能够重复购买。

在系统单机部署的情况下,通过加锁就能够解决就可以解决一人一单的安全问题,但是当系统的用户量越来越多,并发越来越大,单机部署的项目就越来越感到“捉襟见肘”,所以我们需要在多台服务器上集群部署系统,以满足用户的需求。但是当我们集群部署系统之后,问题也随之而来了,之前的锁无法满足集群部署下系统的并发问题,容易出现一人多单的问题。基于此,我们就需要实现 分布式锁 去解决系统集群部署下的并发问题。


2.1 并发安全问题

首先明白一点,我们单机部署下系统是通过 synchronized (userId.toString().intern()) 对用户id进行加锁。因为在单个jvm中可以确保每次从常量池中得到的用户id都是同一个。当一个线程拿到锁之后,其他线程就无法重复拿到锁了。这样就实现了一人一单。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ChX8HiJM-1673770522701)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230115152335502.png)]

当系统集群部署之后,每个单独部署的系统都有了各自的jvm,如果还是像之前通过 synchronized (userId.toString().intern()) 对用户id加锁,那么只能做到在各台单独部署的系统上实现并发安全,但是在整个集群上是并发不安全的。jvm1的第一个线程拿到了锁,jvm2的第一个线程也能拿到锁,因为它们不是拿的不是同一个线程池上的用户id。这样就会出现一人多单的问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iwfo45Hy-1673770522701)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230115152615007.png)]

所以我们不能使用之前的方案了,我们需要一个外部共享的工具来替代原来的锁监视器。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mq1GZhA4-1673770522702)(C:\Users\zhuhuanjie\AppData\Roaming\Typora\typora-user-images\image-20230115153157627.png)]

由此,就引出来分布式锁


3. 分布式锁的实现方案

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

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

综合考虑,我们使用Redis实现分布式锁。


3.1 Redis实现分布式锁

3.1.1 定义分布锁接口和类

首先我们定义分布式锁的接口:

public interface ILock {

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

    /**
     * 释放锁
     */
    void unlock();
}

然后定义一个类来实现这个接口:

public class SimpleRedisLock implements ILock {

    private StringRedisTemplate stringRedisTemplate;
    private String name;

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

    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.setResultType(Long.class);
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    }

    //获取锁
    @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 BooleanUtil.isTrue(success);
    }
    
    //版本一 释放锁
    //@Override
    //public void unlock() {
    //    //获取缓存线程标识
    //    String cacheThreadId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    //    //获取当前线程标识
    //    String threadId = ID_PREFIX + Thread.currentThread().getId();
    //    if (threadId.equals(cacheThreadId)) {
    //        //判断线程标示是否一致,防止误删
    //        stringRedisTemplate.delete(KEY_PREFIX + name);
    //    }
    //}

    //版本二 释放锁
    public void unlock() {
        //调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId()
        );
    }
}

解释

  • 虽然线程id在每个jvm内都是唯一的,但是不保证在其它jvm中不会出现相同的线程id。所以我们在线程id前面拼接一个uuid,避免线程标识相同而误删锁。
  • 释放锁的过程中,为防止误删锁需要保证校验线程标识是否相同操作和释放锁操作的原子性,这里我们使用lua脚本。

3.1.2 编写lua脚本

在lua脚本中我们要实现两个功能,一个是判断线程标识是否一致,一个是释放锁。

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

3.1.3 使用线程锁

@Override
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠券id
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始
    LocalDateTime beginTime = voucher.getBeginTime();
    LocalDateTime endTime = voucher.getEndTime();
    if (LocalDateTime.now().isBefore(beginTime)) {
        //2.1.秒杀未开始
        return Result.fail("秒杀未开始");
    }
    if (LocalDateTime.now().isAfter(endTime)) {
        //2.2.秒杀已结束
        return Result.fail("秒杀已结束");
    }
    //2.3.秒杀正在进行
    int stock = voucher.getStock();
    //3.判断库存是否充足
    if (stock <= 0) {
        //3.1库存不足
        return Result.fail("库存不足");
    }
    //3.2.库存充足

    Long userId = UserHolder.getUser().getId();
    //确保每个userId的锁是同一把
    SimpleRedisLock lock = new SimpleRedisLock(stringRedisTemplate, "order:" + userId);
    if (!lock.tryLock(1200)) {
        //获取锁失败
        return Result.fail("不允许重复下单");
    }
    try {
        //获取代理对象(事务)
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createVoucherOrder(voucherId);
    } finally {
        //释放锁
        lock.unlock();
    }
}

@Transactional
public Result createVoucherOrder(Long voucherId) {
    //4.一人一单
    Long userId = UserHolder.getUser().getId();

    //4.1.查询是否已经有订单
    int count = lambdaQuery().eq(VoucherOrder::getUserId, userId).eq(VoucherOrder::getVoucherId, voucherId).count();
    if (count > 0) {
        //已有订单
        return Result.fail("不能重复购买");
    }

    //5.扣减库存
    //5.1.写法一
    LambdaUpdateWrapper<SeckillVoucher> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.setSql("stock=stock-1").eq(SeckillVoucher::getVoucherId, voucherId).gt(SeckillVoucher::getStock, 0);
    boolean success = seckillVoucherService.update(updateWrapper);
    //5.2.写法二
    //boolean success = seckillVoucherService.update()
    //        .setSql("stock = stock - 1")
    //        .eq("voucher_id", voucherId).gt("stock", 0).update();

    if (!success) {
        //扣减失败
        return Result.fail("库存不足!");
    }

    //6.创建订单
    VoucherOrder voucherOrder = new VoucherOrder();
    //6.1.订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    //6.2.用户id
    voucherOrder.setUserId(UserHolder.getUser().getId());
    //6.3.代金券id
    voucherOrder.setVoucherId(voucherId);
    //6.4.写入数据库
    save(voucherOrder);

    //7.返回订单id
    return Result.ok(voucherOrder.getId());
}

解释

在方法中调用另一个加了事务注解的方法,会导致事务注解失效。我们需要让代理对象来调用加了事务注解的方法,这样就不会失效了。


3.1.4 总结

至此,Redis实现的分布式锁已经完成了,但是还有优化的余地。我们也可以使用成熟的分布式锁框架 Redisson

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

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

相关文章

aardio - 升级bindConfig函数,支持多属性和多子组件

一、需求分析 aardio的 winform.bindConfig() 函数&#xff0c;绑定后&#xff0c;一个组件&#xff0c;只能保存一个属性。 有时候需要同时保存多个属性&#xff0c;比如一个comobox组件&#xff0c;需要保存项目列表&#xff0c;同时保存当前选中的项目索引。当前这个bindC…

代码随想录算法训练营第十八天二叉树 java : .106 从中序与后序遍历序列构造二叉树113. 路径总和ii 112 路径总和 513.找树左下角的值

文章目录前言LeetCode 513.找树左下角的值题目讲解思路那么如何找最左边的呢&#xff1f;Leetcode 112 路径总和题目讲解LeetCode 113. 路径总和ii题目讲解Leetcode 106 从中序与后序遍历序列构造二叉树题目讲解前言 人的不幸在于他们不想走自己的那条路&#xff0c;总想走别人…

大数据必学Java基础(一百二十四):Maven的常见插件

文章目录 Maven的常见插件 一、编辑器插件 二、资源拷贝插件 三、tomcat插件 Maven的常见插件

Dubbo 服务暴露

Dubbo 服务暴露 1. 服务暴露时序图 2. 源码分析 DubboBootstrap.exportServices 从配置管理器中获取到所有的ServiceConfig实例&#xff0c;遍历&#xff0c;然后一个一个的暴露。 ServiceConfig.export 如果DubboBootstrap为空&#xff0c;也就没有初始化&#xff0c;就初…

猴子都能看懂的噪声(noise)专题

背景 除了生成各种奇形怪状与自然景观&#xff0c;噪声也有其他美妙的用途&#xff01; 工作原因&#xff0c;经常接触与噪声相关的画面效果&#xff08;火焰啊&#xff0c;画面扰动啊之类的&#xff09;&#xff0c;做的时候一知半解&#xff0c;傻傻分不清楚各种形态的nois…

Java文件:XWPFDocument导出Word文档

文章目录一、前言二、基本的概念三、Maven依赖(JAR)四、Word模板1.正文段落2.正文表格3.页眉4.页脚五、XWPFDocument的使用5.4导出Word文档1.word模板2.PdfTest测试类3.ISystemFileService接口4.SystemFileServiceImpl实现类5.结果六、遇到问题5.1输出为word的时候换行符无效一…

NR5G基础概念扫盲

文章目录前言BWP未完待续前言 随着人工智能、万物互联时代的到来&#xff0c;人类社会进入到一个新的阶段。新兴的科技产业对信息社会基础设施提出了更高的要求&#xff0c;对低时延、大带宽、高流量的需求&#xff0c;催生了5G技术&#xff0c;并推动其蓬勃发展。通信&#x…

【深入浅出XML】包装纯粹信息的标记语言

XMLXML的定义和概述&#x1f3b6;XML的定义&#x1f3b6;XML的最好描述&#x1f3b6;HTML和XML的重要区别&#x1f3b6;XML的文档结构&#x1f3b6;其他一些标记XML和优势&#x1f3b6;XML的优势XML解析&#x1f3b6;DOM解析❔解析测试&#x1f91e;解析步骤&#x1f91e;案例测…

在Windows部署Java的Jar包

背景 使用 Java 编写了一些有用的工具&#xff0c;因为不方便部署到服务器上&#xff0c;所以需要把 Java 生成的 jar 包在本地 Windows 上部署。 查阅了几种部署方式&#xff0c;认为通过 winsw 进行部署最方便。 安装 winsw 进入 winsw 的下载页面&#xff0c;下载 sampl…

【ROS2 入门】ROS 2 参数服务器(parameters)概述

大家好&#xff0c;我是虎哥&#xff0c;从今天开始&#xff0c;我将花一段时间&#xff0c;开始将自己从ROS1切换到ROS2&#xff0c;在上一篇中&#xff0c;我们一起了解ROS 2中Topic&#xff0c; 这一篇&#xff0c;我们主要会围绕ROS中另外一个重要的概念“Parameters ”&am…

图的拓扑排序(AOV网络)

文章目录拓扑排序概念实现邻接表(队列)邻接矩阵(栈)总结源代码邻接表邻接矩阵拓扑排序 概念 拓扑排序是对有向无环图的顶点的一种排序. AOV网络 : 在有向图中, 用顶点表示活动或者任务, 弧表示活动或者任务间的优先关系, 则此有向图称为用顶点表示活动的网络(Activity On Ve…

小程序介绍和注册安装

小程序介绍和注册安装微信小程序介绍小程序特点其它平台小程序注册微信小程序开发帐号获取appidappid简介微信开发者工具安装创建一个小程序项目核心步骤微信开发者工具构成微信小程序介绍 简短定义&#xff1a;微信小程序是运行在微信APP中的一个程序。 常见小程序 行程码拼…

UDS诊断系列介绍11-3E服务

本文框架1. 系列介绍1.1 3E服务概述2. 3E服务请求与应答2.1 3E服务请求2.2 3E服务正响应2.3 3E服务否定响应3. Autosar系列文章快速链接1. 系列介绍 UDS&#xff08;Unified Diagnostic Services&#xff09;协议&#xff0c;即统一的诊断服务&#xff0c;是面向整车所有ECU的…

# 【笔记】大话设计模式21-23

【笔记】大话设计模式21-23 文章目录【笔记】大话设计模式21-23单例模式21.1 Example21.2 定义21.3 Show me the code一般单例代码(**懒汉模式**)静态初始化&#xff08;**饿汉模式**&#xff09;21.4 总结22 桥接模式22.1 Example22.2 定义22.3 Show me the code22.4 总结23 命…

Code for VeLO 1: Training Versatile Learned Optimizers by Scaling Up

Code for VeLO 1: Training Versatile Learned Optimizers by Scaling Up 这篇文章将介绍一下怎么用VeLO进行训练。 这篇文章基于https://colab.research.google.com/drive/1-ms12IypE-EdDSNjhFMdRdBbMnH94zpH#scrollToRQBACAPQZyB-&#xff0c;将介绍使用learned optimizer in…

入门力扣自学笔记230 C++ (题目编号:2293)

2293. 极大极小游戏 题目&#xff1a; 给你一个下标从 0 开始的整数数组 nums &#xff0c;其长度是 2 的幂。 对 nums 执行下述算法&#xff1a; 设 n 等于 nums 的长度&#xff0c;如果 n 1 &#xff0c;终止 算法过程。否则&#xff0c;创建 一个新的整数数组 newNums …

【Python百日进阶-数据分析】Day226 - plotly的仪表盘go.Indicator()

文章目录一、语法二、参数三、返回值四、实例4.1 Bullet Charts子弹图4.1.1 基本子弹图4.1.2 添加步骤和阈值4.1.3 自定义子弹4.1.4 多子弹4.2 径向仪表图4.2.1 基本仪表4.2.2 添加步骤、阈值和增量4.2.3 自定义仪表图4.3 组合仪表图4.3.1 组合仪表图4.3.2 单角量规图4.3.3 子弹…

Android 深入系统完全讲解(19)

技术的学习关键点 是什么&#xff1f;思路。 而我这里分享一个学习的经典路线&#xff0c;先厘清总框架&#xff0c;找到思路&#xff0c;然后再逐步击破。 这里关于音视频的就是&#xff1a; 总体分为几部分&#xff1a; 1 绘制 2 编解码格式 3 Android 平台的 FFmpeg 开源移…

Compressed Sensing——从零开始压缩感知

Problem 考虑一个线性方程组求解问题&#xff1a; Axb(1)A x b \tag{1}Axb(1) 其中&#xff0c;A∈RmnA \in\mathbb R^{m\times n}A∈Rmn&#xff0c;x∈Rn1x \in\mathbb R^{n\times 1}x∈Rn1&#xff0c;b∈Rm1b \in\mathbb R^{m\times 1}b∈Rm1且m≪nm \ll nm≪n 这是一个…

【C++11】—— lambda表达式

目录 一、lambda表达式的简介 二、lambda表达式的基本语法 三、lambda表达式的使用方法 四、lambda表达式的底层原理 一、lambda表达式的简介 lambda表达式就类似于仿函数&#xff0c;相比仿函数要更加的简洁&#xff0c;我们看一下下面的代码&#xff1a; //商品类 struct…