Redis实战篇3:优惠券秒杀

说明

        该实战篇基于某马的Redis课程中的《某马点评项目》。非常适合有相关经验、缺少企业级解决方案,或者想要复习的人观看,全篇都会一步一步的推导其为什么要这么做,分析其优缺点,达到能够应用的地步。

        本实战篇中心思想就是把项目中的实战抽象成一个个的知识点进行讲解,让初学者达到举一反三的地步而不是只会照着视频敲代码而不去独立思考为什么要这么做。

        关于项目代码请移步到 某马程序员公众号,回复Redis获取。

一、全局唯一ID生成器 

对于一些敏感表的数据,我们的ID尽可能的需要复杂,没有固定的逻辑与规律,并且不重复 。全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息,我们的ID采用Long,8个字节,64个bit。

我们再生产ID的时候只需要干三件事:生成时间戳,生成序列号,然后组合。

 时间戳是一个31位的数组,他的单位是秒,一般来讲就是有一个基础时间值,然后用当前时间-基础时间得到。

  1. 生成基础时间
    1. //生成一个基础秒数时间
      public static void main(String[] args) {LocalDateTime baseTime = LocalDateTime.of(2024, 05, 20, 0, 0, 0);//toEpochSecond(ZoneOffset.UTC) 获取秒数(时区)long second = baseTime.toEpochSecond(ZoneOffset.UTC);System.out.println(second);//1716163200
      }
  2. 生成时间戳
    1. private static final long BEGIN_TIMESTAMP = 1716163200L;
      /*** 全局唯一ID生成器* @param keyPrefix* @return*/
      public long nextId(String keyPrefix){// 1. 生产时间戳LocalDateTime localDateTime = LocalDateTime.now();// 得到当前的秒数long nowSecond = localDateTime.toEpochSecond(ZoneOffset.UTC);// 用当前秒数 — 基础时间秒数long timestamp = nowSecond - BEGIN_TIMESTAMP;return null
      }
  3. 生成序列号,序列化采用自增的方式前面是键,.increment("icr:" + keyPrefix + ":" + localDateStr)只是键名,其每一天都会从 1 开始往上自增。效果如图所示(第二次运行)
    1. public long nextId(String keyPrefix) {// 1. 生产时间戳LocalDateTime localDateTime = LocalDateTime.now();// 得到当前的秒数long nowSecond = localDateTime.toEpochSecond(ZoneOffset.UTC);// 用当前秒数 — 基础时间秒数long timestamp = nowSecond - BEGIN_TIMESTAMP;// 2. 生产序列号// 获取当前日期 精确到天DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy:MM:dd");String localDateStr = LocalDate.now().format(fmt);System.out.println("localDateStr"+localDateStr);Long increment = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + localDateStr);System.out.println("increment"+increment);// 3. 拼接并且返回 一个全局ID为 时间戳+序列号return timestamp << 32 | increment;
      }
  4. 测试
    1. @Test
      void test22(){RedisIdWorker worker = new RedisIdWorker(stringRedisTemplate);long jls = worker.nextId("jls");System.out.println(jls);
      }
    2. 第一次运行

    3. 第二次运行

二、优惠券秒杀

优惠券往往是一人一卷,而秒杀通常伴随着开始时间和结束时间,只有在时间范围之内才可以进行抢购,而且库存要充足。
先来看一下基本逻辑
 

2.1 库存超卖问题 

根据上述理论,如果有并发执行抢购,大家都判断到了库存是否充足一步,此时就会出现问题,例如还剩最后一个库存,此时有两个线程同时检测到了库存充足,那么就都会进行扣减库存的步骤,从而使得库存变为-1,这是不行的。

 

这就提到了多线程编程中的多线程安全问题,对应这一问题的办法就是加锁:悲观锁与乐观锁 

 

2.2 乐观锁方案

用库存替代版本

我们之间用CAS法解决库存超卖问题

@Override
@Transactional
public Result seckillVoucher(Long voucherId) {// 1. 查询优惠卷SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2. 判断秒杀是否开始 LocalDateTime.now:2024-05-16T15:18:44.718 年月日时分秒if(voucher.getBeginTime().isAfter(LocalDateTime.now())){// 秒杀尚未开始return Result.fail("秒杀尚未开始");}// 3. 判断结束时间是否结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){// 秒杀结束return Result.fail("秒杀已经结束");}// 4. 判断库存是否充足if(voucher.getStock()<1){return Result.fail("库存不足");}/*** 乐观锁 判断现在查到的库存值与之前获取的库存值是否相同*/boolean success = seckillVoucherService.update().setSql("stock = stock-1").eq("voucher_id", voucherId).gt("stock", 0) //乐观锁.update();if (!success) {return Result.fail("库存扣减失败,库存不足");}VoucherOrder voucherOrder = new VoucherOrder();// 6.1 订单IDlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2 用户IDLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3 代金券IDvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7. 返回订单IDreturn Result.ok(orderId);
}

 

2.3 一人一单问题 

        Long userID = UserHolder.getUser().getId();int count = query().eq("user_id", userID).eq("voucher_id", voucherId).count();if (count > 0) {// 如果>0 证明用户已经购买过了return Result.fail("你已经下过此订单了!");}

只需要在扣减库存之前判断一下这个人下没下过订单即可。

但是这个如果继续用乐观锁是一定会有问题的。 
为什么?

因为与订单一样,一个人使用工具等插件在很短的时间内疯狂的请求,则还会出现多线程并发问题,在执行查询该用户是否下单时可能会有多个查询共同查询到无订单从而下单成功。而这是查询问题,不是添加问题,而且还是一个人的查询问题,所以这里使用悲观锁。

2.4 基于悲观锁解决一人一单问题

第一个问题,锁要加在哪里?

如果把锁加在类上,那这个类执行时就会发生,张三下订单,获取锁,李四就不能下订单,得等锁释放,这很明显不是我们需要的,我们希望张三下订单,获取锁,之后张三就不能下订单,但是李四可以下订单并且获得一把锁,这就需要我们将锁加在用户ID上,这样保证一个用户一把锁,并且用户之间没有串行。 

 悲观锁函数:

/*** 加悲观锁* @param voucherId* @return*/
@Transactional
public  Result createVuchorOther(Long voucherId) {Long userID = UserHolder.getUser().getId();int count = query().eq("user_id", userID).eq("voucher_id", voucherId).count();if (count > 0) {// 如果>0 证明用户已经购买过了return Result.fail("你已经下过此订单了!");}// 5. 扣减库存 乐观锁形式/*** 乐观锁 判断现在查到的库存值与之前获取的库存值是否相同*/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 订单IDlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2 用户IDLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3 代金券IDvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7. 返回订单IDreturn Result.ok(orderId);
}

 调用:

@Override
@Transactional
public Result seckillVoucher(Long voucherId) {// 1. 查询优惠卷SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2. 判断秒杀是否开始 LocalDateTime.now:2024-05-16T15:18:44.718 年月日时分秒if(voucher.getBeginTime().isAfter(LocalDateTime.now())){// 秒杀尚未开始return Result.fail("秒杀尚未开始");}// 3. 判断结束时间是否结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){// 秒杀结束return Result.fail("秒杀已经结束");}// 4. 判断库存是否充足if(voucher.getStock()<1){return Result.fail("库存不足");}//悲观锁Long userID = UserHolder.getUser().getId();synchronized(userID.toString().intern()) {// 判断用户是否下过订单 考虑多线程 只能用悲观锁// .intern()去字符串常量池里面找//获取事务IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVuchorOther(voucherId);}
}

 来讲一下这个: IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
为什么要这么写?

首先如果仅仅这么写:

那么就代表这个由this调用这个函数,我们知道如果事务想生效是需要他的代理对象,spring会自动的拿到这个类的代理对象来使得事务生效,而这里用this调用则拿到的是这个目标对象,所以事务有可能会失效。

解决方案就是拿到事务代理对象,AopContext.currentProxy()可以获得代理对象,强转为当前类的代理对象即可,再用代理对象调用函数即可完成事务。(在代理对象类中创建这个函数)

他还需要一个依赖:

以及启动项上的设置

再来说一下为什么要用intern(),看tostring源码,他也是新new 一个string 这样的话,即使是同样的ID,通过toString后,也是不同的对象,那就做不到同样用户ID同一把锁了,同一个对象同一个请求后有不同的锁,通过intern后会去线程池上面找。

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

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

相关文章

Mariadb操作命令指南

MariaDB简介 ​ 以下内容仅是站长或网友个人学习笔记、总结和研究收藏。不保证正确性&#xff0c;因使用而带来的风险与本站无关&#xff01; 数据库应用程序与主应用程序分开存在&#xff0c;并存储数据集合。 每个数据库都使用一个或多个API来创建&#xff0c;访问&#xf…

分立元件实现稳压

电路原理图 优点&#xff1a;电压精度高&#xff0c;可以调整输出电压 缺点&#xff1a;压差大时效率较低&#xff0c;发热严重。 参考连接 TL431-高效5V精密稳压器-电路知识干货 (qq.com)https://mp.weixin.qq.com/s?__bizMzkxNzIxNTc5OQ&mid2247484878&idx1&…

操作系统教材第6版——个人笔记1

第一章 计算机操作系统概述 操作系统是计算机系统中最重要的系统软件&#xff0c;它统一管理计算机系统的硬件资源与信息资源&#xff0c;控制与调度上层软件的执行并为其提供易于使用的接口。从资源管理、程序控制、操作控制、人机交互、程序接口、系统结构6个角度深入观察操…

Github 如何配置 PNPM 的 CI 环境

最近出于兴趣在写一个前端框架 echox&#xff0c;然后在 Github 上给它配置了最简单的 CI 环境&#xff0c;这里简单记录一下。 特殊目录 首先需要在项目根目录里面创建 Github 仓库中的一个特殊目录&#xff1a;.github/workflows&#xff0c;用于存放 Github Actions 的工作…

269 基于matlab的四连杆机构动力学参数计算

基于matlab的四连杆机构动力学参数计算。将抽油机简化为4连杆机构&#xff0c;仿真出悬点的位移、速度、加速度、扭矩因数、游梁转角等参数&#xff0c;并绘出图形。程序已调通&#xff0c;可直接运行。 269机构动力学参数计算 位移、速度、加速度 - 小红书 (xiaohongshu.com)

段码屏|液晶显示模块|超低功耗LCD驱动芯片

1 简介 PC164S32 是一款支持 128 点 (32 4)显示 的多功能 LCD 控制器芯片&#xff0c;内部存储器RAM数据直接映射到 LCD 显示。可软件配置特性使其适用于包括 LCD 模块和显示子系统在内的多种 LCD 应用。主控制器与 PC164S32接口仅需3 或 4 条线。内置的省电模式极大的降低了功…

我给线程池管理框架hippo4j找bug

1 虚拟机参数不生效 hippo4j的docker启动脚本位于 docker/docker-startup.sh 。从下图可以看到 JAVA_OPT放在了jar包名 hippo4j-server.jar之后&#xff0c;而只有项目参数才放在jar包名之后。 实际上这里JAVA_OPT中包含虚拟机参数&#xff0c;而虚拟机参数要放在jar包名之前…

使用 CNN 训练自己的数据集

CNN&#xff08;练习数据集&#xff09; 1.导包&#xff1a;2.导入数据集&#xff1a;3. 使用image_dataset_from_directory()将数据加载tf.data.Dataset中&#xff1a;4. 查看数据集中的一部分图像&#xff0c;以及它们对应的标签&#xff1a;5.迭代数据集 train_ds&#xff0…

【漏洞复现】DT-高清车牌识别摄像机 任意文件读取漏洞

0x01 产品简介 DT-高清 车牌识别摄像机是一款先进的安防设备&#xff0c;采用高清图像传感器和先进的识别算法&#xff0c;能够精准、快速地识别车牌信息。其高清晰该摄像机结合了智能识别技术&#xff0c;支持实时监宴图像质量确保在各种光照和天气条件下都能准确捕捉车牌信息…

【面试八股总结】MySQL事务:事务特性、事务并行、事务的隔离级别

参考资料&#xff1a;小林coding 一、事务的特性ACID 原子性&#xff08;Atomicity&#xff09; 一个事务是一个不可分割的工作单位&#xff0c;事务中的所有操作&#xff0c;要么全部完成&#xff0c;要么全部不完成&#xff0c;不会结束在中间某个环节。原子性是通过 undo …

C#根据数据量自动排版标签的样例

这是一个C#根据数据量自动排版标签的样例 using System; using System.Collections.Generic; using System.Data.SqlClient; using System.Drawing; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using HslCommuni…

代码随想录算法训练营第四十五天 | 1049. 最后一块石头的重量 II、494. 目标和、474.一和零

1049. 最后一块石头的重量 II 视频讲解&#xff1a; 动态规划之背包问题&#xff0c;这个背包最多能装多少&#xff1f;LeetCode&#xff1a;1049.最后一块石头的重量II_哔哩哔哩_bilibili 代码随想录 解题思路 直接将这一些石头&#xff0c;分为两堆&#xff0c;让他们尽可能…

C语言 | Leetcode C语言题解之第120题三角形最小路径和

题目&#xff1a; 题解&#xff1a; int minimumTotal(int** triangle, int triangleSize, int* triangleColSize) {int f[triangleSize];memset(f, 0, sizeof(f));f[0] triangle[0][0];for (int i 1; i < triangleSize; i) {f[i] f[i - 1] triangle[i][i];for (int j …

【excel】设置二级联动菜单

文章目录 【需求】在一级菜单选定后&#xff0c;二级菜单联动显示一级菜单下的可选项【步骤】step1 制作辅助列1.列转行2.在辅助列中匹配班级成员 之前做完了 【excel】设置可变下拉菜单&#xff08;一级联动下拉菜单&#xff09;&#xff0c;开始做二级联动菜单。 【需求】在…

python实现——综合类型数据挖掘任务(无监督的分类任务)

综合类型数据挖掘任务 航空公司客户价值分析。航空公司客户价值分析。航空公司客户价值分析。航空公司已积累了大量的会员档案信息和其乘坐航班记录&#xff08;air_data.csv&#xff09;&#xff0c;以2014年3月31日为结束时间抽取两年内有乘机记录的所有客户的详细数据。利用…

万界星空科技MES系统功能介绍

制造执行系统或MES 是一个全面的动态软件系统&#xff0c;用于监视、跟踪、记录和控制从原材料到成品的制造过程。MES在企业资源规划(ERP) 和过程控制系统之间提供了一个功能层&#xff0c;为决策者提供了提高车间效率和优化生产所需的数据。 万界星空科技MES 系统基础功能&am…

【全开源】Java短剧系统微信小程序+H5+微信公众号+APP 源码

打造属于你的精彩短视频平台 一、引言&#xff1a;为何选择短剧系统小程序&#xff1f; 在当今数字化时代&#xff0c;短视频已经成为人们日常生活中不可或缺的一部分。而短剧系统小程序源码&#xff0c;作为构建短视频平台的强大工具&#xff0c;为广大开发者提供了快速搭建…

03-树1 树的同构(浙大数据结构PTA习题)

03-树1 树的同构 分数 25 作者 陈越 单位 浙江大学 给定两棵树 T1​ 和 T2​。如果 T1​ 可以通过若干次左右孩子互换就变成 T2​&#xff0c;则我们称两棵树是“同构”的。例如图1给出的两棵树就是同构的&#xff0c;因为我们把其中一棵树的结点A、B、G…

CSPM.pdf

PDF转图片 归档&#xff1a;

跨境电商多店铺:怎么管理?风险如何规避?

跨境电商的市场辽阔&#xff0c;有非常多的商业机会。你可能已经在Amazon、eBay、Etsy等在线平台向潜在客户销售产品了。为了赚更多的钱&#xff0c;你可能还在经营多个店铺和品牌。 但是&#xff0c;像Amazon、eBay、Etsy等知名平台会有自己的规则&#xff0c;他们开发了很多…