使用Redis实现分布式锁,基于原本单体系统进行业务改造

news2025/3/13 2:44:43

一、单体系统下,使用锁机制实现秒杀功能,并限制一人一单功能

1.流程图:

2.代码实现:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIDWorker redisIDWorker;
    /**
     * 下单秒杀券
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        //1.查询优惠券信息
        SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
        //2.判断当前时间是否在优惠券的开始时间和结束时间内
        if(seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())){
            return Result.fail("优惠券秒杀尚未开始");
        }
        if(seckillVoucher.getEndTime().isBefore(LocalDateTime.now())){
            return Result.fail("优惠券秒杀已经结束");
        }
        //3.判断库存是否充足
        if (seckillVoucher.getStock() < 1){
            return Result.fail("库存不足");
        }
        //加上锁
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()){//事务提交后才释放锁,避免事务未提交产生的线程安全问题
            //拿到代理对象-这样事务才会生效
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }

    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //4.一人一单
        //4.1 查询数据库中是否有当前用户购买此秒杀券的记录
        Long userId = UserHolder.getUser().getId();
        long count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if(count>0){
            return Result.fail("此用户已经购买过一次了,不能重复购买");
        }
        //5.扣减库存
        boolean isDiscount = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)//乐观锁:使用stock代替版本号- 减库存之前判断库存是否充足,防止超卖
                .update();
        if(!isDiscount){
            return Result.fail("库存不足");
        }
        //6.创建订单并保存
        VoucherOrder voucherOrder = new VoucherOrder();
        long orderId = redisIDWorker.nextId("order");
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setUserId(userId);
        voucherOrder.setId(orderId);
        save(voucherOrder);
        //7.返回订单ID
        return Result.ok(orderId);
    }
}

3.存在的并发安全问题

        这里我们是使用用户ID作为锁对象的,但是在分布式系统下,有多台JVM,其内存空间是各自独立的,此时虽然用户ID的值是一样的,但是其userId.toString().intern()方法返回的对象只能保证在同一个JVM上是相同的,不同的JVM使用serId.toString().intern()方法返回的对象是不同的。

        因此,对于不同JVM的线程,当前使用的方式并不能解决线程安全问题,也即,这种方法无法适配分布式系统。

4.解决方案-引入分布式锁

由于引发的问题是因为 :

        在不同的JVM中获得到的锁对象是不同的,因此只要让它们获得的锁对象是同一个,那就可以解决这个问题。

4.1分布式锁

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

4.2分布式锁的实现

二、使用Redis实现分布式锁

1.原理:

实现分布式锁时需要实现的两个基本方法:

(1)获取锁:

        互斥:确保只能有一个线程获取锁

        非阻塞:尝试一次,成功返回true,失败返回false

(2)释放锁:

        手动释放

        超时释放:设置过期时间

2.流程图:

3.代码改造:

1.ILock接口:(后续实现的锁都要实现这个接口)

public interface ILock {

    /**
     * 尝试获取锁
     * @param timeOutSec
     * @return true代表获取锁成功,false代表获取锁失败
     */
    boolean tryLock(Long timeOutSec);

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

2.SimpleRedisLock:

public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    private String name;

    private static final String KEY_PREFIX = "lock:";

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

    @Override
    public boolean tryLock(Long timeOutSec) {
        //使用String 类型来实现锁,如果key存在,则无法设置新的值
        //value 为当前线程ID,为后面释放锁做准备
        String threadId = Thread.currentThread().getId() + "";
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeOutSec, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isLock);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX+name);
    }
}

3.Service代码改造:

将 seckillVoucher 中的:

         //加上锁
        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()){//事务提交后才释放锁,避免事务未提交产生的线程安全问题
            //拿到代理对象-这样事务才会生效
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }

修改为: 

        //加上锁
        Long userId = UserHolder.getUser().getId();
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        boolean isLock = lock.tryLock(10L);
        if(!isLock){
            return Result.fail("请勿重复下单");
        }
        //拿到代理对象-这样事务才会生效
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            lock.unlock();
        }

        这样就实现了redis分布式锁在秒杀业务中的应用。

4.当前存在的问题-误删问题:

如上图,线程1在成功获取到锁,执行业务的时候发生阻塞,可能会由于超时释放锁。

此时线程2成功获取到锁,也在执行自己的业务,在线程2执行业务的时候,线程1又开始运行,在线程1执行完成之后会去释放这个锁。

此时如果线程3去获取锁,也能获取成功。

.....

这就是当前代码存在的线程不安全问题。

5.解决方案-改进Redis的分布式锁

5.1 解决误删除-step1:删前判断

需求:修改之前的分布式锁实现,满足:

1.在获取锁时存入线程标示(可以用UUID表示)

2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致 如果一致则释放锁 如果不一致则不释放锁

5.1.1 流程图

5.1.2 改造代码:

修改SimpleRedisLock 如下:

public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    private String name;

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";//用于区分不同jvm的线程
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(Long timeOutSec) {
        //使用String 类型来实现锁,如果key存在,则无法设置新的值
        //value 为当前线程ID,为后面释放锁做准备
        String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeOutSec, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isLock);
    }

    @Override
    public void unlock() {
        //获取线程标识
        String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了
        //获取redis中的值
        String key = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        if(threadId.equals(key)){//一致的话,删除锁
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }

    }
}

        可以发现,在释放锁(unlock)代码中,判断一致和删除锁是非原子性的,那么也会引发线程安全问题:

可以看到,线程1在判断一致之后阻塞,在线程1阻塞期间由于超时,锁被释放。

线程2此时获取锁成功,在执行业务的时候,线程1阻塞结束,将锁删除,那还是会存在误删的情况。

这里引入lua脚本来解决原子性问题

5.2 解决误删除-step2:使用Lua脚本实现原子性

        Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法可以参考网站:https://www.runoob.com/lua/lua-tutorial.html

(1)unlock.lua:

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

(2)修改 SimpleRedisLock如下:

public class SimpleRedisLock implements ILock{

    private StringRedisTemplate stringRedisTemplate;

    private String name;

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";//用于区分不同jvm的线程

    private static final DefaultRedisScript<Long>UNLOCK_SCRIPT;

    static {
        //类加载的时候初始化,不用重复初始化
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));//设置lua脚本位置
        UNLOCK_SCRIPT.setResultType(Long.class);//设置返回类型
    }
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(Long timeOutSec) {
        //使用String 类型来实现锁,如果key存在,则无法设置新的值
        //value 为当前线程ID,为后面释放锁做准备
        String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeOutSec, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isLock);
    }

    /**
     * 使用Lua脚本保证原子性
     */
    @Override
    public void unlock() {
        String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                threadId);
    }

//    @Override
//    public void unlock() {
//        //获取线程标识
//        String threadId = ID_PREFIX + Thread.currentThread().getId() + "";//这样每个线程都有唯一key了
//        //获取redis中的值
//        String key = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
//        if(threadId.equals(key)){//一致的话,删除锁
//            stringRedisTemplate.delete(KEY_PREFIX+name);
//        }
//
//    }
}

        此时这个使用Redis实现的分布式锁就可以满足大部分业务需求了,不过还是存在一些问题:

1.不可重入

2.不可重试

3.超时释放存在的安全隐患

4.集群时,主从节点之间存在的不一致性

        这些问题我们可以直接采用成熟的,已实现的框架来帮助我们解决问题,如Redisson,后续也会出一篇关于Redisson的文章。

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

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

相关文章

Nginx内存池源代码剖析----ngx_create_pool函数

ngx_create_pool 是 Nginx 内存池 的初始化函数&#xff0c;负责创建并初始化一个内存池对象。它的作用是 为后续的内存分配操作提供统一的管理入口&#xff0c;通过预分配一块较大的内存区域&#xff0c;并基于此区域实现高效的内存分配、对齐管理和资源回收。 源代码定义&…

DeepSeek 开放平台无法充值 改用其他平台API调用DeepSeek-chat模型方法

近几天DeepSeek开放平台无法充值目前已经关闭状态&#xff0c;大家都是忙着接入DeepSeek模型 &#xff0c;很多人想使用DeepSeek怎么办&#xff1f; 当然还有改用其他平台API调用方法&#xff0c;本文以本站的提供chatgpt系统为例&#xff0c;如何修改DeepSeek-chat模型API接口…

QT基础一、学会建一个项目

注&#xff1a;因为CSDN有很多付费才能吃到的史&#xff0c;本人对此深恶痛绝&#xff0c;所以我打算出一期免费的QT基础入门专栏&#xff0c;这是QT基础知识的第一期&#xff0c;学会建一个项目&#xff0c;本专栏是适用于c / c基础不错的朋友的一个免费专栏&#xff0c;接下来…

科技引领未来,中建海龙C-MiC 2.0技术树立模块化建筑新标杆

在建筑行业追求高效与品质的征程中&#xff0c;中建海龙科技有限公司&#xff08;简称“中建海龙”&#xff09;以其卓越的创新能力和强大的技术实力&#xff0c;不断书写着装配式建筑领域的新篇章。1 月 10 日&#xff0c;由深圳安居集团规划&#xff0c;中建海龙与中海建筑共…

解锁养生秘籍,拥抱健康生活

在这个快节奏的时代&#xff0c;人们行色匆匆&#xff0c;常常在忙碌中忽略了健康。其实&#xff0c;养生并非遥不可及&#xff0c;它就藏在生活的细微之处&#xff0c;等待我们去发现和实践。 规律作息是健康的基础。日出而作&#xff0c;日落而息&#xff0c;顺应自然规律&am…

STM32 如何使用DMA和获取ADC

目录 背景 ‌摇杆的原理 程序 端口配置 ADC 配置 DMA配置 背景 DMA是一种计算机技术&#xff0c;允许某些硬件子系统直接访问系统内存&#xff0c;而不需要中央处理器&#xff08;CPU&#xff09;的介入&#xff0c;从而减轻CPU的负担。我们可以通过DMA来从外设&#xf…

细胞计数专题 | LUNA-FX7™新自动对焦算法提高极低细胞浓度下的细胞计数准确性

现代细胞计数仪采用自动化方法&#xff0c;在特定浓度范围内进行细胞计数。其上限受限于在高浓度条件下准确区分细胞边界的能力&#xff0c;而相机视野等因素则决定了下限。在图像中仅包含少量可识别细胞或特征的情况下&#xff0c;自动对焦可能会失效&#xff0c;从而影响细胞…

蓝桥杯备考:贪心算法之纪念品分组

P1094 [NOIP 2007 普及组] 纪念品分组 - 洛谷 这道题我们的贪心策略就是每次找出最大的和最小的&#xff0c;如果他们加起来不超过我们给的值&#xff0c;就分成一组&#xff0c;如果超过了&#xff0c;就把大的单独成一组&#xff0c;小的待定 #include <iostream> #i…

Win11配置wsl、ubuntu、docker

系统要求 安装WSL。 开通虚拟化&#xff1a; 准备工作 dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestartdism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestartwsl --set-default-versi…

尚硅谷爬虫note008

一、handler处理器 定制更高级的请求头 # _*_ coding : utf-8 _*_ # Time : 2025/2/17 08:55 # Author : 20250206-里奥 # File : demo01_urllib_handler处理器的基本使用 # Project : PythonPro17-21# 导入 import urllib.request from cgitb import handler# 需求&#xff…

matlab汽车动力学半车垂向振动模型

1、内容简介 matlab141-半车垂向振动模型 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略

【新品解读】AI 应用场景全覆盖!解码超高端 VU+ FPGA 开发平台 AXVU13F

「AXVU13F」Virtex UltraScale XCVU13P Jetson Orin NX 继发布 AMD Virtex UltraScale FPGA PCIE3.0 开发平台 AXVU13P 后&#xff0c;ALINX 进一步研究尖端应用市场&#xff0c;面向 AI 场景进行优化设计&#xff0c;推出 AXVU13F。 AXVU13F 和 AXVU13P 采用相同的 AMD Vir…

【Linux探索学习】第二十七弹——信号(上):Linux 信号基础详解

Linux学习笔记&#xff1a; https://blog.csdn.net/2301_80220607/category_12805278.html?spm1001.2014.3001.5482 前言&#xff1a; 前面我们已经将进程通信部分讲完了&#xff0c;现在我们来讲一个进程部分也非常重要的知识点——信号&#xff0c;信号也是进程间通信的一…

redis解决高并发看门狗策略

当一个业务执行时间超过自己设定的锁释放时间&#xff0c;那么会导致有其他线程进入&#xff0c;从而抢到同一个票,所有需要使用看门狗策略&#xff0c;其实就是开一个守护线程&#xff0c;让守护线程去监控key&#xff0c;如果到时间了还未结束&#xff0c;就会将这个key重新s…

MySQL-事务隔离级别

事务有四大特性&#xff08;ACID&#xff09;&#xff1a;原子性&#xff0c;一致性&#xff0c;隔离性和持久性。隔离性一般在事务并发的时候需要保证事务的隔离性&#xff0c;事务并发会出现很多问题&#xff0c;包括脏写&#xff0c;脏读&#xff0c;不可重复读&#xff0c;…

从入门到精通:Postman 实用指南

Postman 是一款超棒的 API 开发工具&#xff0c;能用来测试、调试和管理 API&#xff0c;大大提升开发效率。下面就给大家详细讲讲它的安装、使用方法&#xff0c;再分享些实用技巧。 一、安装 Postman 你能在 Postman 官网&#xff08;https://www.postman.com &#xff09;下…

sql sqlserver的特殊函数COALESCE和PIVOT的用法分析

一、COALESCE是一个返回参数中第一个非NULL值的函数&#xff0c; 列如&#xff1a;COALESCE&#xff08;a,b,c,d,e&#xff09;;可以按照顺序取abcde&#xff0c;中的第一个非空数据&#xff0c;abcde可以是表达式 用case when 加ISNULL也可以实现&#xff0c;但是写法复杂了…

智能猫眼实现流程图

物理端开发流程图 客户端端开发流程图 用户功能开发流程图 管理员开发流程图

foobar2000设置DSP使用教程及软件推荐

foobar2000安卓中文版&#xff1a;一款高品质手机音频播放器 foobar2000安卓中文版是一款备受好评的高品质手机音频播放器。 几乎支持所有的音频格式&#xff0c;包括 MP3、MP4、AAC、CD 音频等。不论是经典老歌还是最新的流行音乐&#xff0c;foobar2000都能完美播放。除此之…

【R语言】回归分析与判别分析

一、线性回归分析 1、lm()函数 lm()函数是用于拟合线性模型&#xff08;Linear Models&#xff09;的主要函数。线性模型是一种统计方法&#xff0c;用于描述一个或多个自变量&#xff08;预测变量、解释变量&#xff09;与因变量&#xff08;响应变量&#xff09;之间的关系…