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

 订单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);
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java毕业设计 基于SSM SpringBoot vue宠物领养平台

Java毕业设计 基于SSM SpringBoot vue宠物领养平台 SSM 宠物领养平台 功能介绍 首页 图片轮播 新闻信息 新闻类型 新闻详情 宠物百科 宠物百科类型 宠物百科详情 宠物 宠物类型 宠物详情 立即领养 留言 论坛 发布帖子 登录 个人中心 宠物收藏 宠物领养订单 后台管理 登录注…

EtherCAT开发_3_SSC生成协议栈移植到STM32F405

一、协议栈的生成 协议栈的生成可参考《https://blog.csdn.net/g360250466/article/details/129847081》 几个重点的字段&#xff1a; 1、Hardware中 EL9800_HW, 设置为1&#xff0c;在该基础上进行修改 CONTROLLER_16BIT&#xff0c;设置为0 CONTROLLER_32BIT&#xff0c;设置…

PG控制文件的管理与重建

一.控制文件位置与大小 逻辑位置&#xff1a;pgpobal 表空间中 物理位置&#xff1a;$PGDATA/global/pg_control --pg_global表空间的物理位置就在$PGDATA/global文件夹下 物理大小&#xff1a;8K 二.存放的内容 1.数据库初始化的时候生成的永久化参数&#xff0c;无法更改…

Golang | Leetcode Golang题解之第66题加一

题目&#xff1a; 题解&#xff1a; func plusOne(digits []int) []int {n : len(digits)for i : n - 1; i > 0; i-- {if digits[i] ! 9 {digits[i]for j : i 1; j < n; j {digits[j] 0}return digits}}// digits 中所有的元素均为 9digits make([]int, n1)digits[0]…

自定义拦截器jwt登录校验接口模拟账号登录

五一闲在宿舍&#xff0c;本来想写一个自己的简易博客网站&#xff0c;发现vue基础太差&#xff0c;做不出来页面效果于是便放弃&#xff0c;但也没有完全放弃。于是我分析了一下简易博客的后端实现流程&#xff0c;除了最基本的crud以外&#xff0c;在自己目前的对接口的分析中…

ubuntu搭建jupyter_notebook服务器

环境&#xff1a;ubuntu 22.04 目录 环境&#xff1a;ubuntu 22.04 一、创建一个anaconda用户 创建用户condaUser 为用户condaUser设置密码 开放opt文件夹的权限 登录condaUser用户 二、安装anaconda 下载anaconda 安装anaconda 三、添加环境变量 四、anaconda换源 …

stm32之hal库串口中断和ringbuffer的结合

前言 结合hal库封装的中断处理函数使用rt-thread内部的rt-ringbuffer数据结构源码改造hal库串口部分的源码&#xff0c;将内部静态方法变为弱引用的函数&#xff0c;方便重写标志位采用信号量或变量的两种方式&#xff0c;内部数据分配方式采用动态和静态两种方式 hal库部分串…

GDPU JavaWeb 猜字母游戏

他在对你重定向打卡的大饼与立即跳转到你面前的谎言之间反复横跳。 sendRedirect与forward sendRedirect与forward区别 sendRedirect用于将请求重定向到另一个资源&#xff0c;可以是同一个应用程序内的其他 Servlet&#xff0c;也可以是其他 Web 应用程序的资源&#xff0c;…

R语言数据探索与分析-运用时间序列预测模型对成都市API进行预测分析

一、研究背景 “绿水青山就是金山银山&#xff0c;要让绿水青山变成金山银山”让人们深刻的意识到环境的重要性。与此同时&#xff0c;由于现代生活水平的不断提高&#xff0c;所带来的环境污染也不断增多&#xff0c;空气以及环境的污染带来了越来越多的疾病&#xff0c;深刻…

基于node.js+css+html+mysql博客系统

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、Php、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

Docker 加持的安卓手机:随身携带的知识库(一)

这篇文章聊聊&#xff0c;如何借助 Docker &#xff0c;尝试将一台五年前的手机&#xff0c;构建成一个随身携带的、本地化的知识库。 写在前面 本篇文章&#xff0c;我使用了一台去年从二手平台购入的五年前的手机&#xff0c;K20 Pro。 为了让它能够稳定持续的运行&#xf…

Elasticsearch:对 Java 对象的 ES|QL 查询

作者&#xff1a;Laura Trotta ES|QL 是 Elasticsearch 引入的一种新的查询语言&#xff0c;它将简化的语法与管道操作符结合起来&#xff0c;使用户能够直观地推断和操作数据。官方 Java 客户端的新版本 8.13.0 引入了对 ES|QL 查询的支持&#xff0c;提供了一个新的 API&…

【简单介绍下Lisp的学习历程】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

【文献阅读】 The ITS Irregular Terrain Model(Longely-Rice模型)海上电波传播模型

前言 因为最近在做海上通信的一个项目&#xff0c;所以需要对海上的信道进行建模&#xff0c;所以才阅读到了这一篇文献&#xff0c;下面的内容大部分是我的个人理解&#xff0c;如有错误&#xff0c;请见谅。欢迎在评论区和我一起讨论。 Longely-Rice模型介绍 频率介于 20 …

深度学习:基于TensorFlow、Keras,使用长短期记忆神经网络模型(LSTM)对Microsoft股票进行预测分析

前言 系列专栏&#xff1a;机器学习&#xff1a;高级应用与实践【项目实战100】【2024】✨︎ 在本专栏中不仅包含一些适合初学者的最新机器学习项目&#xff0c;每个项目都处理一组不同的问题&#xff0c;包括监督和无监督学习、分类、回归和聚类&#xff0c;而且涉及创建深度学…

Python 植物大战僵尸

文章目录 效果图项目结构实现思路源代码 效果图 项目结构 实现思路 下面是代码的实现思路&#xff1a; 导入必要的库和模块&#xff1a;首先&#xff0c;我们导入了Python的os、time库以及pygame库&#xff0c;还有植物大战僵尸游戏中用到的各个植物和僵尸的类。 初始化游戏和…

基于Python的LSTM网络实现单特征预测回归任务(TensorFlow)

目录 一、数据集 二、任务目标 三、代码实现 1、从本地路径中读取数据文件 2、数据归一化 3、创建配置类&#xff0c;将LSTM的各个超参数声明为变量&#xff0c;便于后续使用 4、创建时间序列数据 5、划分数据集 6、定义LSTM网络 &#xff08;1&#xff09;创建顺序模…

【深度学习】第一门课 神经网络和深度学习 Week 4 深层神经网络

&#x1f680;Write In Front&#x1f680; &#x1f4dd;个人主页&#xff1a;令夏二十三 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f4e3;系列专栏&#xff1a;深度学习 &#x1f4ac;总结&#xff1a;希望你看完之后&#xff0c;能对…

G1 - 生成对抗网络(GAN)

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 目录 理论知识生成器判别器基本原理 环境步骤环境设置数据准备模型设计模型训练模型效果展示 总结与心得体会 理论知识 生成对抗网络&#xff08;Generative …

Jenkins流水线部署springboot项目

文章目录 Jenkins流水线任务介绍Jenkins流水线任务构建Jenkins流水线任务Groovy脚本Jenkinsfile实现 Jenkins流水线任务实现参数化构建拉取Git代码构建代码制作自定义镜像并发布 Jenkins流水线任务介绍 之前采用Jenkins的自由风格构建的项目&#xff0c;每个步骤流程都要通过不…