使用Redis完成商品秒杀业务

在这里插入图片描述

  各位小伙伴们大家好,欢迎来到这个小扎扎的Redis 6专栏,在这个系列专栏中我对B站黑马的Redis教程进行一个总结,鉴于 看到就是学到、学到就是赚到 精神,这波依然是血赚 ┗|`O′|┛

💡Redis知识点速览

  • 🍖 全局唯一ID
    • 🥩 业务逻辑分析
    • 🥩 代码实现
  • 🍖 优惠券秒杀
    • 🥩 业务逻辑分析
    • 🥩 代码实现
  • 🍖 定量商品多卖问题
    • 🥩 业务逻辑分析
    • 🥩 乐观锁与悲观锁
    • 🥩 乐观锁代码实现
  • 🍖 一个用户限买一单
    • 🥩 业务逻辑分析
    • 🥩 代码实现

🍖 全局唯一ID

🥩 业务逻辑分析

  全局唯一ID是针对销量比较大的一些商品而言的,这类商品的成交量比较多,用户购买成功就会生成对应订单信息并保存到一张表中,而订单表的id如果使用数据库自增ID就存在一些问题,比如说id的规律性太强导致安全性极低,还有如果订单数量太多一张表存不下分成多张表存储的话就会出现ID冲突问题,于是我们需要一个全局ID生成器,保证ID在全局中都是唯一的

  使用Redis即可完成这种全局ID生成器的功能,具体实现就是一种类雪花算法,也就是符号位、时间戳、序列号三部分拼接形成一个ID,逻辑就是符号位0代表整数,时间戳确定具体到下订单的时候是哪一秒,至于序列号就是用于区分这一秒的订单,序列号使用redis的值自增来保证所有序列号不一致,原则上一秒中最多可以有232个不同的ID在这里插入图片描述

🥩 代码实现

/*** @author : mereign* @date : 2022/5/11 - 14:06* @desc : 全局ID生成器*/@Component
public class RedisIdGenerator {/*** 构造方法注入stringRedisTemplate对象*/private StringRedisTemplate stringRedisTemplate;public RedisIdGenerator(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}// 定义序列号的位数private static final int COUNT_BITS = 30;public long nextId(String keyPrefix) {// 生成从指定时间到现在的时间戳LocalDateTime beginTime = LocalDateTime.of(2022, 1, 1, 0, 0, 0);long beginTimeStamp = beginTime.toEpochSecond(ZoneOffset.UTC);long endTimeStamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);long timeStamp = endTimeStamp - beginTimeStamp;/*** 生成序列号 使用redis的incr方法 K值为"icr:" + keyPrefix + ":" + date* 也就是按照日期作为K 每下一次单V就自增1作为序列号添加到后面* 这样的话既避免了K固定带来的V超过最大阈值(redis中的V最大为2^64)* 而且还方便了统计一天、一个月、一年的订单量,在这段时间内最大的序列号就是它的最多订单数*/String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));Long sequenceId = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);// 拼接生成全局唯一ID并返回 两个二进制的拼接可以使用前一个数左移一定位数 后一个数与位移后的进行或运算return timeStamp << COUNT_BITS | sequenceId;}
}

🍖 优惠券秒杀

🥩 业务逻辑分析

  用户对秒杀商品下单的时候,后台业务需要先完成对商品时间的判断,判断该商品的秒杀活动是否开始或者有没有结束,但凡还未开始或者已经结束都无法下单;时间信息正确的话就判断该商品的活动库存还有没有剩余,如果已经卖完的话也无法下单。时间和库存的判断都是通过前端传过来的优惠券id,查出来该优惠券的时间和库存信息,如果条件都满足的话,将该商品券的库存扣除,然后创建订单返回订单id

🥩 代码实现

  controller层主要就是调用service接口里的secKillVoucher方法,所以整个业务逻辑代码全部都在接口的实现类中完成

@Resource
private ISeckillVoucherService seckillVoucherService;@Resource
private RedisIdGenerator generator;@Override
@Transactional
public Result secKillVoucher(Long voucherId) {// 查询优惠券SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);// 获取时间 判断秒杀活动是否开始或者结束if (seckillVoucher.getBeginTime().isAfter(LocalDateTime.now())) {return Result.fail("活动暂未开始");} else if (seckillVoucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("活动已经结束");}// 判断库存是否充足if (seckillVoucher.getStock() < 1) {return Result.fail("库存不足,活动结束");}// 扣减库存seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();// 创建订单 并返回idVoucherOrder order = new VoucherOrder();// 订单id(redis全局唯一id) 下单用户id(拦截器中做登录验证的用户id) 优惠券id(直接传过来的id)long orderId = generator.nextId("order");order.setId(orderId);order.setUserId(UserHolder.getUser().getId());order.setVoucherId(voucherId);save(order);return Result.ok(orderId);
}

🍖 定量商品多卖问题

🥩 业务逻辑分析

  像上面的优惠券秒杀的业务,优惠券或者商品的数量一般都是固定的,如果把这些数量都卖完之后应该就结束这个活动。但是现实中的秒杀业务都是多线程的,很多的用户同时等着活动开启一起点击下单,这样的话就极有可能出现线程安全问题也就是说最终成交的数量要多于活动商品的数量

  上述问题出现的原因就是多线程之间的执行顺序所引起,我们的秒杀业务里面是先查询库存数量大于1就产生订单,但是多线程之间的执行不会严格的按照这个顺序执行,而是交叉执行,如果最后只剩一张票的时候进来了两个线程AB,A查完B查AB查询结果都可以下单,A产生订单B再产生订单,此时就已经产生超卖

🥩 乐观锁与悲观锁

  解决线程问题的最好方法就是加锁,但是锁也分为悲观锁和乐观锁,悲观锁认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行,例如Synchronized、Lock等。乐观锁认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改,如果没有修改则更新数据,修改说明发生了安全问题

  很显然乐观锁的性能要显著高于悲观锁,因此采用乐观锁保证线程的原子性。乐观锁又有两种解决方案:版本号是指对修改的数据附带一个version字段值,每次更新的时候判断修改时的version与查询的时候是否一致,一致则修改。CAS机制全称为Compare And Swap译为先比较再交换,也就是将修改的数据本身作为版本号,每次更新的时候判断修改时的数据值与查询时的值是否相同,相同则修改,不同就说明发生了线程安全问题,在我们的这个售卖业务中,可以设置成只要库存大于0就可以执行成功

🥩 乐观锁代码实现

  乐观锁的核心就是,在更新数据的时候(也就是减少库存),判断一下库存是否大于0,如果判断失败的话也应该使该线程任务失败

// 扣减库存
boolean update = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();
// 更新失败说明在扣除库存的时候 库存小于等于0
if (!update) {return Result.fail("库存不足!");
}

🍖 一个用户限买一单

🥩 业务逻辑分析

  按照正常的业务逻辑,秒杀应该限制一个用户只能购买一次该商品,最简单的方法就是对user_id使用唯一索引,如果user_id重复就会抛出相关异常,但是这需要修改表结构。如果不修改标结果的话就需要扣除库存之前根据voucher_id和user_id查询订单表,如果存在的话就返回错误,否则说明该用户还未购买

🥩 代码实现

  单机(服务部署在一台tomcat服务器)的情况下,加synchronized 锁即可解决(查询判断用户是否下单和创建订单)业务的线程安全问题,但是这种情况就只能

// 单用户id(拦截器中做登录验证的用户id)
Long userId = UserHolder.getUser().getId();// 根据user_id加锁  intern方法是去字符常量池中查找值相同的,不加的话字符串值一样的地址不一样也会加上锁
synchronized (userId.toString().intern()) {// 查询优惠券// 判断库存是否充足// user_id和voucher_id联合查询订单数Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 订单数为1 就说明已经下过单了if (count.equals(1)) {return Result.fail("您已经购买过该商品了");}// 扣减库存  创建订单return Result.ok(orderId);
}

  以上加synchronized 锁的解决方案只适用于单机模式下,此时所有的请求过来都会按照userId去常量池中查找是否一致,一致的话就锁在一起防止一个用户购买多单。但是集群模式下所有的请求会经过Nginx的负载均衡轮询发送到集群上的所有服务器,如果一个用户的多个请求被分配到不同的服务器上的话,不同服务器中的JVM虚拟机里的静态常量池中的内容是不同步的,这样的话就会导致虽然userId一致但是各自所在的静态常量池中都没有,于是这个用户就可以在不同的服务器分别下单了。如果有用户使用脚本同时发送很多的下单请求,那么就会有极大的可能在每一个服务器中都下一单,那么如何解决这个问题呢?那就要学习分布式锁的内容了

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

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

相关文章

表格列隐藏_【excel每日提升】Excel隐藏列,不让别人打开!

【新朋友】点击标题下面蓝色字“王俊东“关注。 【老朋友】点击右上角&#xff0c;转发或分享本页面内容。excel系列课程excel特效系列课程开始了&#xff0c;今天第2节&#xff01;第1节&#xff1a;Excel有公式的单元格标记颜色&#xff0c;很简单&#xff01;第2节&#xf…

map与weakmap,ES6 Map和WeakMap有什么区别?

Looking this and this MDN pages it seems like the only difference between Maps and WeakMaps is a missing "size" property for WeakMaps. But is this true? Whats the difference between them?解决方案The experienced JavaScript programmer will notice…

“毕业季”|一个java开发实习生的OFFER之路

哈喽哈喽大家好&#xff0c;这里是小扎扎的博客。相信有关注过我的好盆友们可能会发现我已经有一段时间没有出来划水了&#xff0c;那么这段时间小扎扎都在干什么呢&#xff1f;没错&#xff01;我确实是去找实习了&#xff01;接下来就给大家介绍一下本次战役的战况如何 活动地…

virtualbox 该内存不能为written_系统提示“该内存不能为read”的原因和解决办法...

我们单位的电脑经常显示这个对话框&#xff0c;已经有好几年了&#xff0c;单位的老头们都不怎么懂电脑&#xff0c;我本人也不爱管闲事。但是出现这种对话框的原因是什么呢&#xff1f;又怎么解决呢&#xff1f;一般电脑经常出现蓝屏和死机&#xff0c;而且频繁出现。有时会出…

插件properties_Mybatis3系列 - 4. mybatis-config的properties属性详解

前两章简单的讲解了MyBatis的使用方式. 接下来先全局的说一下MyBatis的全局的xml配置详细说明.XML格式定义-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">configuration 的映射文件(根据定义顺序说明)properties 属性…

【Redis 6】秒杀业务——分布式锁

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的Redis 6专栏&#xff0c;在这个系列专栏中我对B站黑马的Redis教程进行一个总结&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f4a1;Redis知识点速览&#…

python读取文件最后几行_如何用python获取文件的最后一行,文件可能会比较大

展开全部 #!/usr/bin/env python import os import sys def get_last_n_lines(logfile, n): blk_size_max 4096 n_lines [] with open(logfile, rb) as fp: fp.seek(0, os.SEEK_END) cur_pos fp.tell() while cur_pos > 0 and len(n_lines) < n: blk_size min(blk_si…

360oauth token是什么意思_Oauth/access token

oauth/access_token第三放应用使用开始的request_token来换取用户授权过的Access_tokenURL格式标准的OAuth http返回格式HTTP请求方式POST请求数限制false请求参数oauth_consumer_key: 创建应用时生成的APP KEY。oauth_token:经过用户授权的Request Token。oauth_signature_met…

【Swagger】看这一篇就够了

各位小伙伴们大家好&#xff0c;欢迎跟着小扎扎一起学习【Swagger】这门技术&#xff0c;在本片博客中我对B站狂神的Swagger教程进行一个总结&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f4a1;Swagger知识点…

python mockito arg_that_wqingxiao

偶然间在脉脉上看到了一道头条的算法面试题按照题目的理解&#xff0c;简单的写了一个html网页pool开始{{index}}{{index}}var vm new Vue({el: #vue_det,data: {list: [],i: 0},methods: {details: function () {return this.site " - 学的不仅是技术&#xff0c;更是梦…

用python画大雄_python制作斗图生成器

网上各种带文字的表情图片都被大家玩坏了&#xff0c;今天就和大家一起用 python 亲自做一个带字表情图片生成器。 不知道大家有没有看到网上有很多人都在说 "人生苦短&#xff0c;我用 python"&#xff0c;这句话我之前也不是很理解&#xff0c;觉得人生苦短和用pyt…

【spring cloud】(一)使用idea创建可相互调用的多模块应用

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的spring cloud专栏&#xff0c;在这个系列专栏中我对B站尚硅谷阳哥的spring cloud教程进行一个总结&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f4a1;spr…

epson彩色打印机加墨水_爱普生打印机墨盒如何加墨?

展开全部掌握以下几点步骤&#xff0c;即可轻松加墨。1、首先&#xff0c;从打印机上取下墨盒&#xff0c;32313133353236313431303231363533e4b893e5b19e31333365646234这里就不好做介绍了&#xff0c;取下墨盒的方法因机而异。将墨盒上的一层贴纸撕掉&#xff0c;并拿出所需材…

python字符串百分号_Python字符串格式化的2种方法

本文介绍了Python字符串格式化&#xff0c;主要有两种方法&#xff0c;分享给大家&#xff0c;具体如下 用于字符串的拼接&#xff0c;性能更优。 字符串格式化有两种方式&#xff1a;百分号方式、format方式。 百分号方式比较老&#xff0c;而format方式是比较先进的&#xff…

【踩坑,已解决】spring cloud删除子模块后创建同名模块时遇到的删除不净,java、resources包失效,java、resources包被标记为模块等问题

&#x1f4a1;问题速览&#x1f4cc; 问题复现&#x1f4cc; 没有任何操作&#xff0c;model又出现了&#x1f4cc; 模块重建后java、resources包失效&#x1f4cc; 模块重建后java、resources包被标记为了模块&#x1f4cc; 问题复现 本人是在跟着B站尚硅谷的视频练习Spring c…

蓝牙扫描过程解析_智慧定位系统之蓝牙网关在室内定位技术的原理浅析-新导智能...

蓝牙室内定位技术是利用在室内安装的若干个蓝牙局域网接入点&#xff0c;把网络维持成根据多用户的基础网络连接形式&#xff0c;并确保蓝牙局域网接入点始终是这个微网的主设备&#xff0c;然后经过丈量信号强度对新加入的盲节点进行三角定位。苏州新导实时定位体系(RTLS)和室…

【spring cloud】(二)服务的注册发现——Eureka

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的spring cloud专栏&#xff0c;在这个系列专栏中我对B站尚硅谷阳哥的spring cloud教程进行一个总结&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f4a1;spr…

python选择题题库百度文库_大学Python程序题题库

程序设计 题目&#xff1a;补充 fun 函数&#xff0c;其功能是判断一个整数是否是素数&#xff0c;在主 函数 main 中输入一个整数&#xff0c;调用该 fun 函数进行判断并输 出结果。 要求&#xff1a;使用 math 相关函数 import math def fun(n): i,w2,0 if n<1: w1 while …

新手攻略熔炉_我的世界攻略:生存模式新手攻略

《我的世界》(英文:《minecraft》)是一款风靡全球的高自由度沙盒游戏&#xff0c;由瑞典MojangAB和4J Studios开发。国际版由微软Mojang工作室开发&#xff0c;中国版由网易代理。Minecraft着重于让玩家去探索、交互&#xff0c;并且改变一个由一立方米大小的方块动态生成的地图…

【spring cloud】(三)服务调用——Ribbon、OpenFeign

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的spring cloud专栏&#xff0c;在这个系列专栏中我对B站尚硅谷阳哥的spring cloud教程进行一个总结&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f4a1;服务…