【黑马点评Redis——003优惠券秒杀】

1.优惠券秒杀

1.1 全局ID生成器

1.1.1 什么是全局ID生成器

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具

需要满足以下特性:

  • 唯一性
  • 高可用
  • 高性能
  • 递增性
  • 安全性

1.1.2 为什么需要全局ID生成器?

自增ID存在的问题:

  • ID的规律性太明显
  • 受单表数据量的限制

1.1.2 如何构建一个全局ID生成器

全局唯一ID生成策略

  • UUID
  • Redis自增(可以携带一些信息)
  • snowflake算法
  • 数据库自增
    Redis自增ID策略
  • 每天一个key,方便统计订单量
  • ID构造是时间搓+计数器
    在这里插入图片描述
    ID的组成部分:
  • 符号位:1bit,永远为0
  • 时间戳:31bit,以秒为单位,可以使用69年
  • 序列号:32bit,秒内的计数器,支持每秒最多可以产生2^32个不同ID

1.2 优惠券秒杀的下单功能流程图

在这里插入图片描述

1.3 库存超卖问题

  • 悲观锁:添加同步锁,让线程串行执行
    • 优点:简单粗暴
    • 缺点:性能一般
  • 乐观锁:不加锁,在更新时判断是否有其它线程在修改
    • 优点:性能好
    • 缺点:存在成功率低的问题
      在这里插入图片描述

1.4 乐观锁解决超卖

乐观锁的关键是判断之前的数据是否有被修改过,常见的方式有两种:

  • 版本号法(在这里库存可以当做版本号)
  • CAS法

在这里插入图片描述

    @Transactional@Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始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("库存不足");}// 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);// 7.返回订单idreturn Result.ok(orderId);}

1.5 实现一人一单

在这段代码中我们需要先判断该用户是否已经购买过优惠券,我们需要对用户Id进行加锁,通过userId.toString().intern()来获取同一个对象。同时通过代理来防止事务失效。

   @Overridepublic Result seckillVoucher(Long voucherId) {// 1.查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判断秒杀是否开始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()){// 获取代理对象(事务),通过代理对象防止Transaction失效IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}@Transactionalpublic Result createVoucherOrder(Long voucherId){// 5.一人一单Long userId = UserHolder.getUser().getId();// 5.1 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2 判断是否存在if(count>0){// 用户已经购买过了return Result.fail("用户已经购买过一次");}// 6.扣减库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock",0).update();if (!success){return Result.fail("库存不足");}// 7.创建订单VoucherOrder voucherOrder = new VoucherOrder();// 7.1 订单idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2 用户idvoucherOrder.setUserId(userId);// 7.3 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 8. 返回订单idreturn Result.ok(orderId);}

2. 知识储备

2.1 事务失效的常见原因

2.1.1 访问权限问题

众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。

但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题。
spring要求被代理方法(开启事务的方法)必须是public的。

也就是说,如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。

2.1.2 方法用final修饰

有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,这样会导致事务失效。
如果你看过spring事务的源码,可能会知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。

 注意:如果某个方法是static的,同样无法通过动态代理,变成事务方法。

2.1.3 方法内部调用

在某个Service类的某个方法中,调用另外一个事务方法。

@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Transactionalpublic void add(UserModel userModel) {userMapper.insertUser(userModel);updateStatus(userModel);}@Transactionalpublic void updateStatus(UserModel userModel) {doSameThing();}
}

我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

如何解决这个问题

2.1.3.1 新加一个Service方法

只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:

@Servcie
public class ServiceA {@Autowiredprvate ServiceB serviceB;public void save(User user) {queryData1();queryData2();serviceB.doSave(user);}}@Servciepublic class ServiceB {@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}
2.1.3.2 在该Service类中注入自己

如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:

@Servcie
public class ServiceA {@Autowiredprvate ServiceA serviceA;public void save(User user) {queryData1();queryData2();serviceA.doSave(user);}@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}
2.1.3.3 通过AopContent类

可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。

@Servcie
public class ServiceA {public void save(User user) {queryData1();queryData2();((ServiceA)AopContext.currentProxy()).doSave(user);}@Transactional(rollbackFor=Exception.class)public void doSave(User user) {addData1();updateData2();}}

2.1.4 未被spring管理

在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。

2.1.5 多线程调用

spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

2.1.6 表不支持事务

在mysql5之前,默认的数据库引擎是myisam。它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。myisam好用,但有个很致命的问题是:不支持事务。

2.1.7 未开启事务

2.2 toString().intern()的作用

intern() 方法用于在运行时将字符串添加到内部的字符串池中,并返回字符串池中的引用。

它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

返回值
当调用 intern() 方法时,如果字符串池中已经存在相同内容的字符串,则返回字符串池中的引用;否则,将该字符串添加到字符串池中,并返回对字符串池中的新引用。

public class RunoobTest {public static void main(String args[]) {String str1 = "Runoob";String str2 = new String("Runoob");String str3 = str2.intern();System.out.println(str1 == str2);  // falseSystem.out.println(str1 == str3);  // true}
}

优点
使用 intern() 方法可以在需要比较字符串内容时节省内存,因为它可以确保相同内容的字符串共享同一个对象。然而,过度使用 intern() 方法可能导致字符串池的增长,消耗大量内存。因此,应谨慎使用 intern() 方法,只在必要时使用。

3. 问题及反思

3.1 一人一单的并发安全问题

如果是集群模式下,会有多个tomcat,tomcat中的锁不共享。需要采用分布式锁才可以生效。
在这里插入图片描述

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

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

相关文章

nfs网络存储配置

准备&#xff1a;yum install rpcbind yum install nfs-server 一台服务器&#xff1a;192.168.220.131 一台客户端&#xff1a;192.168.220.220 服务器&#xff1a; 先启动rpcbind服务&#xff1a;systemctl restart rpcbind 在启动…

同旺科技 USB TO SPI / I2C适配器读写24LC256--字节写

所需设备&#xff1a; 1、USB 转 SPI I2C 适配器&#xff1b;内附链接 2、24LC256芯片 适应于同旺科技 USB TO SPI / I2C适配器升级版、专业版&#xff1b; 00地址写入一个字节数据AA&#xff0c;并读回验证&#xff1b; 单字节写时序&#xff1a; 读字节时序&#xff1a; …

文件上传服务器、文件展示等异步问题

问题&#xff1a; 文件上传模块&#xff1a;当文件已经上传完成&#xff0c;文件进度已经走完了&#xff0c;但是服务器响应还没有返回结果&#xff0c;出现了&#xff0c;获取不到上传后的文件路径&#xff0c;需要等待服务器返回结果后&#xff0c;才能获取文件路径并点击跳…

Qt 跨平台开发的一丢丢总结

Qt 跨平台开发 文章目录 Qt 跨平台开发摘要第一 \ & /第二 神奇{不能换行显示第三 预处理宏 关键字&#xff1a; Qt、 win、 linux、 lib、 MSVC 摘要 最近一直在琢磨Qt跨平台开发的问题&#xff0c;缘由有以下几个&#xff0c; 首先第一个&#xff0c;我们目前开发…

1.5 掌握Scala内建控制结构

本次课通过一系列编程任务和案例&#xff0c;深入讲解了Scala编程语言中的控制结构。 条件表达式 Scala的条件表达式使用if-else结构&#xff0c;允许根据条件执行不同的代码分支。与Java相比&#xff0c;Scala的条件表达式更加简洁&#xff0c;并且可以直接返回相应的值。 …

配置网络设备的密码设置以及忘记密码的恢复方式以及实现全网互通

1.实验拓扑图&#xff1a; 2.实验需求&#xff1a; 1.推荐步骤 1.1配置IP&#xff1a; 不过多说了&#xff0c;较为基础&#xff08;略&#xff09; 2.推荐步骤 2.所有网络设备配置console接口密码 首先进入全局模式&#xff0c;输入以下代码(进入接口console接口0给其配置密…

玩原神玩的!30本提升你视野、眼界和格局的好书不如你挑的一本适合自己的书!——早读(逆天打工人爬取热门微信文章解读)

许久不见&#xff0c;雨天坐公车&#xff0c;别是一番滋味在心头 引言Python 代码第一篇 洞见 人民日报推荐&#xff1a;30本提升你视野、眼界和格局的好书第二篇 人民日报 来了&#xff01;新闻早班车要闻社会政策 结尾 不要着急 最好的总会在最不经意的时候出现 意外的六分钟…

Nacos服务注册中心

1.引入依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>2.application.properties中配置 # 应用名称 spring.application.namenacos-aserver…

Vue3炫酷商品卡牌 组件设计

文章目录 演示代码 感谢来自BinaryMoon-CSS 艺术之暗系魔幻卡牌的博文。&#x1f495; 演示 代码 接口类型 export interface CourseBaseVO {/*** 主键*/id: string | number;/*** 机构ID*/companyId: string | number;/*** 课程名称*/name: string;/*** 大分类*/mt: string…

在matplotlib中控制colorbar的长度

在matplotlib中控制colorbar的长度 使用matplotlib绘制带颜色的箭头图&#xff0c;有时想直接把颜色条拿来当比例尺条&#xff0c;就需要控制颜色条的长度。 1. pyplot.colorbar()参数说明 pyplot.colorbar(mappable, ax, cax, **kwargs) mappable是一个ScalarMappble类型的…

01、创建型-单例模式--只有一个实例

文章目录 前言一、基本介绍1.1 什么是单例模式1.2 为什么要用单例模式1.3 应用场景1.4 单例优缺点 二、单例模式的实现方式2.1 饿汉式单例2.1.1 静态变量方式2.1.2 静态代码块 2.2 懒汉式单例2.2.1 懒汉式单例2.2.2 懒汉式优化①-线程安全2.2.2 懒汉式优化②-双重检查锁2.2.3 懒…

[RTOS 学习记录] 工程管理工具make及makefile

[RTOS 学习记录] 工程管理工具make及makefile 这篇文章是我阅读《嵌入式实时操作系统μCOS-II原理及应用》后的读书笔记&#xff0c;记录目的是为了个人后续回顾复习使用。 前置内容&#xff1a; 开发工具 Borland C/C 3.1 精简版 文章目录 1 make 工具2 makefile 的内容结构3…