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 订单ID
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    // 6.2 用户ID
    Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    // 6.3 代金券ID
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    // 7. 返回订单ID
    return 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 订单ID
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    // 6.2 用户ID
    Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    // 6.3 代金券ID
    voucherOrder.setVoucherId(voucherId);
    save(voucherOrder);
    // 7. 返回订单ID
    return 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后会去线程池上面找。

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

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

相关文章

谷歌Material Design设计标准指南

Material Design是谷歌的Android设计规范。虽然这种优秀的设计语言应用于Android&#xff0c;但它的本质被许多设计师借鉴&#xff0c;并用于自己的设计。它是一个广泛的UX、UI设计师必须学习优秀的设计规范。 现在&#xff0c;Material Design设计规范已正式内置为即时设计&a…

MySQL -- SQL笔试题相关

1.银行代缴花费bank_bill 字段名描述serno流水号date交易日期accno账号name姓名amount金额brno缴费网点 serno: 一个 BIGINT UNSIGNED 类型的列&#xff0c;作为主键&#xff0c;且不为空。该列是自动增量的&#xff0c;每次插入新行时&#xff0c;都会自动递增生成一个唯一的…

【AIGC】大型语言模型在人工智能规划领域模型生成中的探索

大型语言模型在人工智能规划领域模型生成中的新应用 一、引言二、LLM在规划领域模型生成中的潜力三、实证分析&#xff1a;LLM在规划领域模型生成中的表现四、代码实例&#xff1a;LLM在规划领域模型生成中的应用五、结论与展望 一、引言 随着人工智能技术的迅猛发展&#xff0…

String类详解

前言&#xff1a;String类是表示字符串的类&#xff0c;String类的内部也提供了非常多的方法来供程序员使用。 String类还有一大特性&#xff0c;就是不可变性。只要使用string创建了字符串&#xff0c;就不可以修改。为string类提供了一层安全性。&#xff08;对于" &qu…

Android 11.0 系统设置语言和输入法菜单Launage语言列表增加支持多种英语语言功能

1.前言 在11.0的系统ROM产品定制化开发中,在系统中的语言和输入法菜单中,在添加语言的默认列表中对于同一类型的语言就可以会出现一中语言,比如多种英语类型 就显示的不全,所以要求显示所有的英语类型,这样就需要了解语言列表的加载流程然后加载所有的英语类型,接下来具…

Qt 5桌面APP开发实战

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 第一节&#xff1a;Qt 5桌面APP开发实战入门 Qt 5的跨平台特性 Qt 5的界面设计工具 Qt 5的…

硬盘重新分区后数据丢失,如何高效恢复?

在数字化时代&#xff0c;硬盘作为我们存储重要数据的“仓库”&#xff0c;承载着工作文件、家庭照片、视频资料等众多不可替代的信息。然而&#xff0c;有时因为误操作或系统需要&#xff0c;我们可能会对硬盘进行重新分区&#xff0c;结果却发现宝贵的数据不见了。面对这种情…

vue3学习(五)

前言 接上一篇笔记&#xff0c;继续Router路由相关入门知识学习&#xff0c;笔记与code示例&#xff0c;分享学习&#xff0c;大佬请忽略。 一、Router路由入门知识点 入门知识点就这些&#xff0c;其他心法可以去官网继续深造。 二、code示例 按照前面分享的“webstorm新建v…

虚拟现实环境下的远程教育和智能评估系统(五)

查阅相关VR眼动注意力联合教育学相关论文 1.Exploring Eye Gaze Visualization Techniques for Identifying Distracted Students in Educational VR&#xff08;IEEE VR 2020&#xff09; 摘要&#xff1a;我们提出了一种架构&#xff0c;使VR教学代理能够响应眼动追踪监控…

【C#】类和对象的区别

1.区别概述 结构体和类的最大区别是在存储空间上&#xff0c;前者是值类型&#xff0c;后者是引用类型&#xff0c;它们在赋值上有很大的区别&#xff0c;在类中指向同一块空间的两个类的值会随一个类的改变而改变另一个&#xff0c;请看如下代码所示&#xff1a; namespace …

数据结构:排序(1)【冒泡排序】【插入排序】【堆排序】【希尔排序】

一.冒泡排序 冒泡排序实际上就是这样&#xff1a; 1.冒泡排序的实现 两个数进行比较&#xff0c;大的往后移动。对于排序这个专题来说&#xff0c;这是比较简单的一种排序了&#xff1a; void Swap(int* a, int* b) {int tmp *a;*a *b;*b tmp; } void BubbleSort1(int* …

Amazon云计算AWS(二)

目录 三、简单存储服务S3&#xff08;一&#xff09;S3的基本概念和操作&#xff08;二&#xff09;S3的数据一致性模型&#xff08;三&#xff09;S3的安全措施 四、非关系型数据库服务SimpleDB和DynamoDB&#xff08;一&#xff09;非关系型数据库与传统关系数据库的比较&…

Elasticsearch 认证模拟题 -2

一、题目 有一个索引 task3&#xff0c;其中有 fielda&#xff0c;fieldb&#xff0c;fieldc&#xff0c;fielde 现要求对 task3 重建索引&#xff0c;重建后的索引新增一个字段 fieldg 其值是fielda&#xff0c;fieldb&#xff0c;fieldc&#xff0c;fielde 的值拼接而成。 …

基于JSP的高校二手交易平台

开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 工具&#xff1a;浏览器&#xff08;如360浏览器、谷歌浏览器、QQ浏览器等&#xff09;、MySQL数据库 系统展示 系统功能界面 用户注册与登录界面 个人中心界面 商品信息界面 摘要 本文研究了高…

Go 优雅的爬虫框架 - Colly

Colly 是一款用 Go 语言编写的优雅网络爬虫框架,速度快、灵活且易于使用 关键特性包括: 线程安全。用户友好的 API。支持 XHR(Ajax)和 WebSocket。缓存和持久化。支持速度限制和分布式爬取。强大的可扩展性。colly采集器配置 AllowedDomains: 设置收集器使用的域白名单,设…

TrueNAS开启SSH登录ROOT

简介: 从 SCALE Bluefin 22.12.0 开始,为了加强安全性并遵守联邦信息处理标准 (FIPS),root帐户登录已被弃用。所有 TrueNAS 用户都应创建具有所有必需权限的本地管理员帐户,并开始使用它来访问 TrueNAS。当根用户密码被禁用时,只有管理用户帐户才能登录 TrueNAS Web 界面。…

深入剖析 Kubernetes 原生 Sidecar 容器

1 Sidecar 容器的概念 sidecar 容器的概念在 Kubernetes 早期就已经存在。一个明显的例子就是 2015 年的这篇 Kubernetes 博客文章&#xff0c;其中提到了 sidecar 模式。多年来&#xff0c;sidecar 模式在应用程序中变得越来越普遍&#xff0c;使用场景也变得更加多样化。 其…

大语言模型拆解——Tokenizer

1. 认识Tokenizer 1.1 为什么要有tokenizer&#xff1f; 计算机是无法理解人类语言的&#xff0c;它只会进行0和1的二进制计算。但是呢&#xff0c;大语言模型就是通过二进制计算&#xff0c;让你感觉计算机理解了人类语言。 举个例子&#xff1a;单1&#xff0c;双2&#x…

【银河麒麟V10服务器OS-系统根分区扩容】指导教程手册

【银河麒麟V10服务器OS-系统根分区扩容】指导教程手册 环境信息&#xff1a;VMware虚拟软件16.0 首先查看KylinOS服务器版本&#xff1a;nkvers 备注&#xff1a; (Tercel) 版本是 V10 SP1 版本&#xff0c; (Sword) 版本是 V10 SP2 版本&#xff0c; (Lance) 版本是 V10 …

Apache SeaTunnel On SparkEngine 集成CDP

随着数据处理需求的日益增长&#xff0c;选择一个高效、灵活的数据处理工具变得尤为关键。SeaTunnel&#xff0c;作为一个开源的数据集成工具&#xff0c;不仅支持多种数据处理引擎&#xff0c;还提供了丰富的连接器和灵活的数据同步方案。 本文将详细介绍 SeaTunnel 的优势和…