【Redis学习05】优惠券秒杀及其优化
文章目录
- 1. 全局唯一ID
- 1.1 全局唯一ID介绍及生成策略
- 1.2 代码实现
- 1.3 总结
- 2. 优惠券秒杀下单
- 2.1 添加优惠券
- 2.2 优惠券秒杀功能
- 3. 超卖问题
- 3.1 问题分析
- 3.2 乐观锁与悲观锁
- 3.3 乐观锁的实现方式
- 3.4 代码实现
- 4. 一人一单
- 4.1 需求分析
- 4.2 代码实现
- 4.3 问题分析
1. 全局唯一ID
1.1 全局唯一ID介绍及生成策略
为了解决这个问题,我们就有必要自己设计一个全局的唯一ID。
全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具。
一般需要满足以下特性:
为了增加ID的安全性,我们可以不直接使用redis自增数值,而是拼接一些其他信息:
1.2 代码实现
首先,我们可以使用2022年1月1日零点的时间的秒数作为开始时间戳,用生成ID的本地时间减去开始时间戳作为最终时间戳。
通过下面代码我们可以获取2022年1月1日零点的时间的秒数,并将其定义为一个常量。
接下来我们生成序列号,将时间戳和序列号进行拼接然后返回。
拼接时间戳和序列号的做法是首先将时间戳使用位运算向左移动三十二位,然后将右边空出来的三十二位与序列号做或运算,得到最终的id。
@Component
public class RedisIdWorker {public static final long BEGIN_TIMESTAMP = 1640995200;//序列号位数public static final int COUNT_BITS = 32;@Autowiredprivate StringRedisTemplate stringRedisTemplate;public Long nextId(String keyPrefix) {//1. 生成时间戳LocalDateTime now = LocalDateTime.now();long nowSeconds = now.toEpochSecond(ZoneOffset.UTC);long timeStamp = nowSeconds - BEGIN_TIMESTAMP;//2. 生成序列号//2.1 获取当前日期,精确到天String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));//调用redis自增长策略Long count = stringRedisTemplate.opsForValue().increment("icr" + keyPrefix + ":" + date);//3. 拼接并返回//时间戳左移32位,左移留下的32位0与count做或运算return timeStamp << COUNT_BITS | count;}}
1.3 总结
2. 优惠券秒杀下单
每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券就需要秒杀抢购。
2.1 添加优惠券
我们在优惠券的接口中添加了新增普通优惠券和秒杀券的方法,我们可以通过请求去添加需要的优惠券。
@RestController
@RequestMapping("/voucher")
public class VoucherController {@Resourceprivate IVoucherService voucherService;/*** 新增普通券* @param voucher 优惠券信息* @return 优惠券id*/@PostMappingpublic Result addVoucher(@RequestBody Voucher voucher) {voucherService.save(voucher);return Result.ok(voucher.getId());}/*** 新增秒杀券* @param voucher 优惠券信息,包含秒杀信息* @return 优惠券id*/@PostMapping("seckill")public Result addSeckillVoucher(@RequestBody Voucher voucher) {voucherService.addSeckillVoucher(voucher);return Result.ok(voucher.getId());}
2.2 优惠券秒杀功能
实现我们的优惠券秒杀功能首先需要满足以下两点:
- 秒杀是否开始或已经结束
- 库存是否充足
接下来我们梳理一下秒杀流程图,可以说业务流程并不复杂,我们使用代码实现一下。
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {@Autowiredprivate ISeckillVoucherService seckillVoucherService;@Autowiredprivate RedisIdWorker RedisIdWorker;/*** 优惠券秒杀* @param voucherId* @return*/@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//1. 根据优惠券id查询优惠券信息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. 充足,扣减优惠券数量voucher.setStock(voucher.getStock()-1);boolean success = seckillVoucherService.updateById(voucher);if (!success) {return Result.fail("优惠券已被抢完");}//6. 创建订单//6.1 设置idVoucherOrder voucherOrder = new VoucherOrder();Long voucherOrderId = RedisIdWorker.nextId("voucherOrder");voucherOrder.setId(voucherOrderId);//6.2 设置user_id Long userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);//6.3 设置优惠券idvoucherOrder.setVoucherId(voucherId);this.save(voucherOrder);return Result.ok(voucherOrderId);}
}
3. 超卖问题
3.1 问题分析
我们学习使用redis在做项目就需要考虑并发问题,我们来看一下如下几种情况,观察我们的程序会出现哪些问题。
正常(理想)情况下:
并发情况下:出现库存为-1 的情况,也就是出现了超卖问题
3.2 乐观锁与悲观锁
超卖问题的典型就是多线程问题,针对这一问题的常见解决方案就是加锁。
加锁有乐观锁和悲观锁两种,其中,如果有高并发的需求,悲观锁因其串行执行保证线程安全而不能满足需要,因此,对于高并发解决问题一般是使用乐观锁。
当然,使用乐观锁也不一定能完全解决高并发带来的线程安全问题,我们接下去还会继续学习其他的解决方案。
3.3 乐观锁的实现方式
乐观锁的关键是判断之前查询得到的数据是否发生变化,常见的方法有如下两种
- 版本号法
- CAS(Compare And Set)法
CAS法是根据版本号法进行改进的:既然库存和版本号是同时进行修改的,那我们何苦新增一个版本号字段来增加复杂度呢?我们直接判断扣减库存前的库存跟之前查询的库存是否一致不就解决了吗。
是的,如果扣减之前查询的库存没有发生变化,则进行扣减操作,如果扣减之前查询的库存发生变化,则说明有其他线程对库存进行了修改,这时候我们就不进行库存扣减操作,返回操作失败的提示信息。
3.4 代码实现
@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {//1. 根据优惠券id查询优惠券信息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();//5. 充足,扣减优惠券数量//当更新时查询的库存为未更新前的库存时进行库存减一boolean success = seckillVoucherService.update().setSql("stock=stock-1").eq("voucher_id", voucherId).eq("stock", voucher.getStock()).update();if (!success) {return Result.fail("优惠券已被抢完");}//6. 创建订单//6.1 设置idVoucherOrder voucherOrder = new VoucherOrder();Long voucherOrderId = RedisIdWorker.nextId("voucherOrder");voucherOrder.setId(voucherOrderId);//6.2 设置user_idvoucherOrder.setUserId(userId);//6.3 设置优惠券idvoucherOrder.setVoucherId(voucherId);this.save(voucherOrder);return Result.ok(voucherOrderId);}
}
我们使用Jmeter进行高并发测试
我们使用Jmeter进行一秒钟两百个线程进行优惠券秒杀,按我们猜想应该是优惠券不够卖,至少有一半的线程是不能抢到优惠券的,结果确实如此嘛?
我们打开数据库看一下库存和订单
可以看出,我们启动了两百个线程但只卖出了20几张优惠券,这是为什么呢?
因为我们设置的条件是当我们扣减库存时查询的库存必须和我们扣减之前保持一致,而在高并发下,我们很多线程被这一个条件卡在了“门外”,进而导致抢购失败的情况。
如何优化呢?其实我们设置的条件可以不再是扣减库存时查询的库存必须和我们扣减之前保持一致,而是库存大于0,就是不一致只要库存大于0是不是也能满足需要。说干就干,我们修改一下代码
boolean success = seckillVoucherService.update().setSql("stock=stock-1").gt("stock", 0).eq("voucher_id", voucherId).update();
可以看到,这种办法确实有效。
4. 一人一单
4.1 需求分析
对于秒杀优惠券,我们要求是同一个用户只能购买一张优惠券,不能多买。
我们重新梳理一下流程
4.2 代码实现
@Transactionalpublic Result seckillVoucher(Long voucherId) {//1. 根据优惠券id查询优惠券信息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();//一人一单int count = this.query().eq("user_id", userId).eq("voucher_id", voucherId).count();if(count>0){return Result.fail("一个用户只能购买一个优惠券");}//当更新时查询的库存大于0时进行库存减一boolean success = seckillVoucherService.update().setSql("stock=stock-1").gt("voucher_id", 0).eq("stock", voucher.getStock()).update();if (!success) {return Result.fail("优惠券已被抢完");}//6. 创建订单//6.1 设置idVoucherOrder voucherOrder = new VoucherOrder();Long voucherOrderId = RedisIdWorker.nextId("voucherOrder");voucherOrder.setId(voucherOrderId);//6.2 设置user_idvoucherOrder.setUserId(userId);//6.3 设置优惠券idvoucherOrder.setVoucherId(voucherId);this.save(voucherOrder);return Result.ok(voucherOrderId);}
4.3 问题分析
其实,一人一单在高并发情况下也是会出现一人可以买多张优惠券的问题 ,这时我们就只加悲观锁进行限制。
解决方法,加悲观锁
但是,加悲观锁在单机情况下能解决安全问题,但是在集群模式下就不行了
为什么呢?
因为我们如果在集群模式下,有很多个JVM,每个JVM中的锁是不一样的。如下图所示,当线程1在JVM1中获取锁成功时,线程3也成功的获取了JVM2中的锁,因为两个锁是不是同一个,因此肯定能获取成功,这样我们就又无法保证在集群模式下实现一人一单的问题了。
怎么解决呢?我也还没学
接下去我会继续学习分布式锁,等我学会了再来分享。
看到这里的朋友记得点赞关注一下,我会持续学习并且更新,加油!!!
【Redis学习05】优惠券秒杀及其优化相关推荐
- Redis实战篇--优惠券秒杀
文章目录 Redis实战篇--优惠券秒杀 全局唯一ID 实现优惠券秒杀下单 超卖问题 一人一单 分布式锁 基于redis的分布式锁 Redis实战篇–优惠券秒杀 全局唯一ID 为什么需要全局唯一id? ...
- redis 优惠券秒杀逐步优化
根据redis set nx实现分布式锁,防止同一用户多次请求引起的多个线程同时下单,同时保证分布式系统下也不能同时下单,根据用户创建唯一key,然后进行加锁,设置锁过期时间,防止运行过程中出现意外错 ...
- Redis 学习 - 05 Node.js 客户端操作 Redis、Pipeline 流水线
使用编程语言客户端操作 Redis 目前我们进行的操作都是通过 Redis 的命令行客户端 redis-cli 进行的. 开发者也可以通过 Redis 图形管理软件操作,例如 RDM(Redis De ...
- 前端性能优化学习 05 请求和响应优化 01(DNS 解析、HTTP 长连接、HTTP2、避免重定向、压缩传输的数据资源)
请求和响应优化 目的:更快的内容到达时间. 核心思路: 更好的连接传输效率 更少的请求数量 更小的资源大小 合适的缓存策略 最佳实践: 减少 DNS 查找:每次主机名的解析都需要一次网络往返,从而增加 ...
- 【使用Redis分布式锁实现优惠券秒杀功能】-Redis学习笔记05
前言 本章节主要实现限时.限量优惠券秒杀功能,并利用分布式锁解决<超卖问题>.<一人一单问题>. 一.优惠券下单基本功能实现 1.功能介绍及流程图 2.代码实现 @Resour ...
- SpringBoot整合Redis实现优惠券秒杀服务(笔记+优化思路版)
本文属于看黑马的redis的学习笔记,记录了思路和优化流程,精简版最终版请点击这里查看. 文章目录 一.全局ID生成器 1.1 理论 1.1.1 全局唯一ID生成策略 1.2 代码(Redis自增) ...
- Redis 基础 - 优惠券秒杀《初步优化(异步秒杀)》
Redis基础 - 基本类型及常用命令 Redis基础 - Java客户端 Redis 基础 - 短信验证码登录 Redis 基础 - 用Redis查询商户信息 Redis 基础 - 优惠券秒杀< ...
- Redis(4)优惠券秒杀
优惠券秒杀 全局ID生成器 优惠券秒杀 秒杀实现 库存超卖 乐观锁实现 一人一单 分布式锁 分布式锁版本一 Redis分布式锁误删情况 解决分布式锁误删 分布式锁原子性问题 解决原子性问题 利用Jav ...
- 【Redis学习08】Redis消息队列实现异步秒杀
文章目录 1. 消息队列 1.1 基于List结构模拟消息队列 1.2 基于PubSub的消息队列 1.3 基于Stream的消息队列 2. 基于Stream的消息队列---消费者组 2.1 消费者组 ...
最新文章
- sql查table,VIEW,sp, function 及 trigger 物件
- 计算机动画专业要学什么课程,计算机动画制作专业主要课程有哪些?
- Vue项目中使用Echarts(一)
- SQL tp3.2 批量更新 saveAll
- 3D动画设计软件:Cinema 4D R25 for mac(c4d r25)
- 联想服务器怎么使用无线网卡,Lenovo无线装置设置界面无法调节无线网卡
- SSIS(简单数据抽取过程介绍)
- 钱我所欲也,健康我所欲也,舍钱而取健康也
- 说说程序员不解风情的瞬间
- 今天是印度已故心算大师Shakuntala Devi的84岁诞辰
- 从现实世界的角度去理解计算机领域的知识
- Cesium Primitives加载大量图标点
- Stateful Firewall和SPI(stateful packet inspection) Firewall介绍
- 江苏高等学校计算机等级考试大纲与样卷,江苏省高等学校计算机等级考试大纲(2015 年修订).pdf...
- 【5G NR】物理资源
- 南京人的“鸭子”情结 南京最好吃的top5鸭子介绍
- Notepad++介绍与安装
- 因此林潇不急我老婆怎么来的
- 移动端APM网络监控与优化方案
- 大学开学需要准备什么?_马立杰_新浪博客