Redis---------实现商品秒杀业务,包括唯一ID,超卖问题,分布式锁

news2025/1/13 13:37:45

 订单ID必须是唯一

59e1602b09834e0aadaa9602bbc8a20f.png

 唯一ID构成:

e0304f55e1f348838355b3126378a22d.png

代码生成唯一ID:


import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

//基于redis自增长的生成策略
@Component
public class RedisUUID {
    //起始时间时间秒数
    private static final long BEGIN_TIMESTAMP=1640995200L;
    
    //使用Redis自增策略
    private StringRedisTemplate stringRedisTemplate;

    public RedisUUID(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    //参数是业务的类型
    public long nextid(String keyType){
        //1,生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowsecend = now.toEpochSecond(ZoneOffset.UTC);
        //当前时间的秒数减去起始时间的秒数得到时间戳
        long nowtime_stamp = nowsecend - BEGIN_TIMESTAMP;

        //2,生成序列号
        String nowdate = now.format(DateTimeFormatter.ofPattern("yyyy:mm:dd"));//使得每天都会生成新的一轮ID
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyType + ":" + nowdate);

        //3,拼接返回
        return nowtime_stamp << 32 | count;
    }
}

827128d84da44b4793c030401783ed74.png

 

商品下单操作

业务逻辑:

7700ca73332d48708595361f1ae99dc3.jpg

 思路:主要是要了解以及掌握整个业务的流程:①先看商品是不是在秒杀的时间范围内②然后还要去看库存中是否还有该商品③如果有的话就扣减库存④然后就会生成订单,订单ID为唯一ID⑤把订单写入数据库中,再返回数据给前端

代码实现:

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

    @Autowired
    private ISeckillVoucherService iSeckillVoucherService;

    @Autowired
    private RedisUUID redisUUID;

    @Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {

        //1,查询商品的信息
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);

        //2,看是否在秒杀时间范围内
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("尚未开始!");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("已经结束啦!");
        }

        //3,再看库存是否还有
        if (voucher.getStock()<1) {
            return Result.fail("库存不足!");
        }

        //4,如果有就减扣库存
        boolean sucess = iSeckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
        if (!sucess) {
            return Result.fail("库存不足!");
        }

        //5,然后就创建订单信息
        VoucherOrder voucherOrder = new VoucherOrder();
            //5.1,订单id----id生成器
            long order = redisUUID.nextid("order");
            voucherOrder.setVoucherId(order);
            //5.2,用户id
            Long id = UserHolder.getUser().getId();
            voucherOrder.setUserId(id);
            //5.3,商品id
            voucherOrder.setVoucherId(voucherId);

        //6,保存进数据库
        save(voucherOrder);

        //7,返回数据
        return Result.ok(order);
    }
}

 

库存超卖问题

先看看什么是库存超卖问题:

正常情况:

ec6c0f2b42804805a77a24f6e75ce430.jpg

 但是涉及到高并发的时候一定会出问题:

b46184b741a443fdbfa8f540833db7cf.jpg

所以我们要想办法去解决这个问题,锁!!!

ee298fbbb6684e16bc4411dcc7c90c40.jpg

 悲观锁认为一定发生并发问题,所以每一次操作都会加锁,是线程串行进行,不会出现并发问题,但是这样的话就导致性能降低,所以我们使用乐观锁,乐观锁是先让你操作,等你要修改数据库的时候再判断与你查到的数据是否是一样,如果是一样的才可以修改,否则不可以减库存。

乐观锁的两种实现判断法:

第一种:版本号法,就是通过查询两次版本号来判断是否被修改过库存

ac6f979be91d4cba9c36fff6f96e61a8.jpg

第二种:CAS法,是在版本号法上做的改进方法,既然要判断两次版本是否相同,为啥不判断库存量是否相同呢,所以CSA法就是去判断前后两次查询到的库存量是否一样,如果一样就可以改

c859dfee73f44fd9844699936966ab0c.jpg

用乐观锁CAS法来解决超卖问题:

//4,如果有就减扣库存
        boolean sucess = iSeckillVoucherService.update()
                    .setSql("stock = stock - 1")
                    .eq("voucher_id", voucherId)
                    .eq("stock",voucher.getStock())
                    .update();
        if (!sucess) {
            return Result.fail("库存不足!");
        }

但是这样任然还不能解决超卖问题,因为如果两个线程同时来查到100,线程1做完修改还剩99,线程2查到不是100就会不执行修改,这样也会有问题,所以又要进行改进策略

 //4,如果有就减扣库存
        boolean sucess = iSeckillVoucherService.update()
                    .setSql("stock = stock - 1")
                    .eq("voucher_id", voucherId)
                    .gt("stock",0)
                    .update();
        if (!sucess) {
            return Result.fail("库存不足!");
        }

一人一单问题

 使用悲观锁处理单体服务下的多线程问题:

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

    @Autowired
    private ISeckillVoucherService iSeckillVoucherService;

    @Autowired
    private RedisUUID redisUUID;

    @Override

    public Result seckillVoucher(Long voucherId) {

        //1,查询商品的信息
        SeckillVoucher voucher = iSeckillVoucherService.getById(voucherId);

        //2,看是否在秒杀时间范围内
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("尚未开始!");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("已经结束啦!");
        }

        //3,再看库存是否还有
        if (voucher.getStock()<1) {
            return Result.fail("库存不足!");
        }

        //实现单体服务下的一人一单的多线程安全问题
        
        Long id = UserHolder.getUser().getId();
        //先获取锁,再提交事务,保证线程安全
        synchronized (id.toString().intern()){
            //获得Spring的代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }
    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {
        //一人一单问题
        Long id = UserHolder.getUser().getId();
        Integer count = query().eq("user_id", id).eq("voucher_id", voucherId).count();

        if(count > 0){
            return Result.fail("你已经购买过!");
        }

        //4,如果有就减扣库存
        boolean sucess = iSeckillVoucherService.update()
                    .setSql("stock = stock - 1")
                    .eq("voucher_id", voucherId)
                    .gt("stock",0)
                    .update();
        if (!sucess) {
            return Result.fail("库存不足!");
        }

        //5,然后就创建订单信息
        VoucherOrder voucherOrder = new VoucherOrder();
        //5.1,订单id----id生成器
        long order = redisUUID.nextid("order");
        voucherOrder.setVoucherId(order);

        //5.2,用户id
        voucherOrder.setUserId(id);

        //5.3,商品id
        voucherOrder.setVoucherId(voucherId);

        //6,保存进数据库
        save(voucherOrder);

        //7,返回数据
        return Result.ok(order);
    }
}

 添加依赖:

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

 在启动类上添加:

@EnableAspectJAutoProxy(exposeProxy = true)

分布式集群模式下的多线程问题:

当我们是处理分布式集群模式下,两个JVM不是共用一把锁,导致每个JVM都有自己的锁导致我们之前的锁锁不住,每个JVM都有一个线程会获得锁。

 

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

 

 

 基于Redis实现分布式锁:

创建锁对象:
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;


public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final  String KEY_PREFXY="lock:";

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


    @Override
    public boolean trylock(long timeoutSec) {
        //获取线程ID作为标识
        long ThreadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFXY + name, ThreadId + "", timeoutSec, TimeUnit.MINUTES);
        //避免空指针
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFXY + name);
    }
}
代码实现Redis分布式锁的应用:

①先创建锁的对象,然后先是去获取锁②没有获取到锁就直接返回错误③获取到锁就可以进行对数据库的操作④操作完之后进行释放锁

Long id = UserHolder.getUser().getId();
        //创建锁对象
        SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order" + id, stringRedisTemplate);

        //获取锁
        boolean trylock = simpleRedisLock.trylock(1200);
        //判断是否获得锁成功
        if (!trylock) {
            //获取锁失败
            return Result.fail("不允许重复下单!");
        }

        //获得Spring的代理对象(事务)
        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            simpleRedisLock.unlock();
        }

 但是就上面的处理还不够严谨,因为如果一个线程发生阻塞的话,其他线程可能会获得锁并且释放锁,导致锁误删问题,

解决锁误删问题:
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final  String KEY_PREFXY="lock:";
    
    //得到一个唯一锁的标识
    private static final  String ID_PREFXY= UUID.randomUUID(true)+"-";

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


    @Override
    public boolean trylock(long timeoutSec) {
        //获取线程标识
        String ThreadId = ID_PREFXY+Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFXY + name, ThreadId, timeoutSec, TimeUnit.MINUTES);
        //避免空指针
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {

        //获取线程标识
        String ThreadId = ID_PREFXY+Thread.currentThread().getId();
        //判断要来修改的进程跟锁的标识是否一致
        String s = stringRedisTemplate.opsForValue().get(KEY_PREFXY + name);

        if(ThreadId.equals(s)){
            //释放锁
            stringRedisTemplate.delete(KEY_PREFXY + name);
        }
    }
}

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

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

相关文章

2024年IEEE第16届先进信息通信技术国际会议(ICAIT 2024)即将召开!

2024年IEEE第16届先进信息通信技术国际会议&#xff08;ICAIT 2024&#xff09;将于2024年 8月16-19日在湖北恩施举行。先进信息通信技术作为当今世界科技发展的前沿领域&#xff0c;不仅是推动经济社会发展的重要引擎&#xff0c;也是提升国家综合竞争力的重要支撑。因此&…

EDA(一)Verilog

EDA&#xff08;一&#xff09;Verilog Verilog是一种用于电子系统设计自动化&#xff08;EDA&#xff09;的硬件描述语言&#xff08;HDL&#xff09;&#xff0c;主要用于设计和模拟电子系统&#xff0c;特别是在集成电路&#xff08;IC&#xff09;和印刷电路板&#xff08;…

AI-数学-高中53-离散型随机变量的均值与方差

原作者视频&#xff1a;【随机变量】【一数辞典】3离散型随机变量的均值与方差_哔哩哔哩_bilibili 标准差 方差开根

算法数据结构--单调栈

文章目录 介绍单调递增栈单调递减栈图示应用场景 步骤模板Deque用法例题[739. 每日温度](https://leetcode.cn/problems/daily-temperatures/)[496. 下一个更大元素 I](https://leetcode.cn/problems/next-greater-element-i/) 总结 介绍 单调栈是一种特殊的栈数据结构&#x…

Excel中怎样把单元格里的数据拆分成多行?

时常会遇到这种情况&#xff0c;需要将一个单元格里的数据分拆到多行&#xff0c;可以使用公式&#xff0c;这里演示使用基础操作的办法。 按照excel使用经验&#xff0c;可以复制数据&#xff0c;粘贴到MS Word里&#xff0c;这个是excel的同族软件&#xff0c;兼容性好。 在…

基于SSM SpringBoot vue教务排课系统

基于SSM SpringBoot vue教务排课系统 系统功能 登录 个人中心 学生信息管理 教师信息管理 课室信息管理 班级信息管理 系别信息管理 专业信息管理 课程信息管理 选课信息管理 课表信息管理 开发环境和技术 开发语言&#xff1a;Java 使用框架: SSM(Spring SpringMVC Myba…

基于 AI 的数据库助手-Chat2DB

序言 现在已经开始步入 AI 时代&#xff0c;AI 产品也已经络绎不绝。今天&#xff0c;给大家介绍一款数据库的 AI 产品 —— Chat2DB。 一、什么是 Chat2DB Chat2DB 由阿里提供的一个数据库管理、数据开发、数据分析的工具&#xff0c;它是一个 AI 原生的数据库管理工具&…

STM32数字示波器+详细注释+上位机程序+硬件

目录 1、设计指标&#xff1a; 2、功能&#xff1a; 3、上位机的程序 ​4、测试的照片 5、PCB 6、模拟电路板 7、程序 资料下载地址&#xff1a;STM32数字示波器详细注释上位机程序硬件 1、设计指标&#xff1a; 主控: STM32…

2022 亚马逊云科技中国峰会,对话开发者论坛

目录 前言 最近整理资料发现还有一些前 2 年的内容没发出来&#xff0c;故补发记录&#xff0c;每年都有新的感悟。 开发者论坛 1. 你认为什么是开发者社区&#xff0c;如何定义一个成功的开发者社区&#xff1f; 我认为可以把开发者社区看成一个 “产品” 来对待&#xff…

大功率双向直流电源的输出电压和电流

双向直流电源&#xff08;Bidirectional DC Power Supply&#xff09;是一种具有双向输出电能的直流电源。与传统的直流电源相比&#xff0c;双向直流电源在输出电能的同时&#xff0c;还具备一定的电流输入能力&#xff0c;从而使其应用范围更加广泛。大功率双向直流电源作为电…

n-Track Studio Suite for Mac激活版:打造您的专属音频工作室

n-Track Studio Suite for Mac是一款功能强大的数字音频工作站软件&#xff0c;让您在家中就能享受到专业录音棚的待遇。无论是录制人声、乐器还是MIDI序列&#xff0c;都能轻松应对。 n-Track Studio Suite for Mac激活版下载 这款软件拥有实时音高校准、时间拉伸和自动补足功…

Java项目:基于SSM框架实现的学院党员管理系统高校党员管理系统(ssm+B/S架构+源码+数据库+毕业论文+开题)

一、项目简介 本项目是一套基于SSM框架实现的学院党员管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功能齐…

机器学习:深入解析SVM的核心概念【一、间隔与支持向量】

直接阅读原始论文可能有点难和复杂&#xff0c;所以导师直接推荐我阅读周志华的《西瓜书》&#xff01;&#xff01;然后仔细阅读其中的第六章&#xff1a;支持向量机 间隔与支持向量 **问题一&#xff1a;什么叫法向量&#xff1f;为什么是叫法向量**什么是法向量&#xff1f;…

OpenCV如何模板匹配(59)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV如何实现背投(58) 下一篇 &#xff1a;OpenCV在图像中寻找轮廓(60) 目标 在本教程中&#xff0c;您将学习如何&#xff1a; 使用 OpenCV 函数 matchTemplate()搜索图像贴片和输入…

C语言 | Leetcode C语言题解之第60题排列序列

题目&#xff1a; 题解&#xff1a; char* getPermutation(int n, int k) {int factorial[n];factorial[0] 1;for (int i 1; i < n; i) {factorial[i] factorial[i - 1] * i;}--k;char* ans malloc(n 1);ans[n] \0;int valid[n 1];for (int i 0; i < n; i) {val…

OpenWRT部署Zerotier虚拟局域网实现内网穿透

前言 细心的小伙伴肯定已经发现了&#xff1a;电脑上部署了Zerotier&#xff0c;如果路由器也部署了OpenWRT&#xff0c;那是否能远程访问呢&#xff1f; 答案是肯定的。 OpenWRT部署Zerotier有啥好处&#xff1f; 那好处必须多&#xff0c;其中的一个便是在外远程控制家里…

【UnityRPG游戏制作】NPC交互逻辑、动玩法

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;就业…

Oracle 数据库全面升级为 23ai

从 11g 到 12c 再到 19c&#xff0c;今天&#xff0c;我们迎来了 23ai &#xff01; “ Oracle AI Vector Search allows documents, images, and relational data that are stored in mission-critical databases to be easily searched based on their conceptual content Ge…

算法打卡day40

今日任务&#xff1a; 1&#xff09;139.单词拆分 2&#xff09;多重背包理论基础&#xff08;卡码网56携带矿石资源&#xff09; 3&#xff09;背包问题总结 4&#xff09;复习day15 139单词拆分 题目链接&#xff1a;139. 单词拆分 - 力扣&#xff08;LeetCode&#xff09; …

【22-处理不平衡数据集:Scikit-learn中的技术和策略】

文章目录 前言了解不平衡数据集重采样技术过采样欠采样生成合成样本调整类别权重使用适合于不平衡数据集的评估指标结论前言 在机器学习任务中,不平衡数据集是一个非常常见的问题。它指的是数据集中各类别样本数量差异较大,这种情况在现实世界的数据收集中非常普遍,特别是在…