总图
扣减库存流程的代码
- 消息队列rocket包下把生产者,消费者拆分
- rocket/producer/LocalTransactionListenerImpl——本地事务监听器
- rocket/consumer/IncreaseSalesConsumer——增加销量
- rocket/consumer/DecreaseStockConsumer——减少库存
更新销量
service/impl/OrderServiceImpl
订单生成完以后,异步更新销量——简单的发送异步消息
- 注入rocketMQ的bean
- 声明JSONobject对象。存入消息内容:商品id,数量
- 构建message对象,用
MessageBuilder方法构建,构建时把jsonobject对象转换成字符串存入 rocketMQTemplate.asyncSend()方法,异步发送消息。消息转换成二进制数据存到消息队列- 参数:目标主题+标签,消息内容,回调函数,超时时间
- 发送成功,记debug日志。失败记异常日志
更新销量消费者
rocket/consumer/IncreaseSalesConsumer
@Service注解。表明是独立的service@RocketMQMessageListener注解,标记消息监听器,声明消费者消费哪个主题+标签。可以接受消息队列的消息public class IncreaseSalesConsumer implements RocketMQListener<String>,声明消息格式String字符串- 注入itemservice
- 实现RocketMQListener接口的
onMessage方法- 当消息发送给队列后,队列会通知消费者消费
-
调用itemservice的增加销量接口,修改MySQL
-
发消息有数据转换过程。
- 消息用对象封装,转换成字符串,底层转成二进制数据发给队列。
- 队列通知消费者消费时,把二进制数据给消费者,消费者根据我们声明的泛型“RocketMQListener< String >”把二进制恢复成对应格式string。
- 我们再把字符串抓换成业务逻辑需要的数据格式。
预减库存
test/SimulateBackendData.java——缓存预热
- 测试代码中手动模拟缓存预热
- 把MySQL中商品+库存的数据加载到缓存
service/impl/ItemServiceImpl——实操业务逻辑
扣减缓存库存
decreaseStockInCache()方法,传入商品id,数量,直接扣redis中库存。返回布尔类型- 如果数量大于库存,扣完是负数,回补相同数量库存。扣减库存失败。
- 先判断参数合不合法
- 声明key,拼一下格式。
- 直接减库存。调用操作redis的
decrement(key,数量)方法,减少key对应的value库存值。返回扣减后的新库存值result - 如果result < 0,说明库存不足,就不能让你扣库存。所以回补库存
increaseStockInCache()方法,打印”回补库存完成“日志 - 如果刚好库存扣为0,在redis中增加一个售罄标识kv数据。key是“售罄标识语句+商品id”,value是任意数字。打印“售罄标识完成”日志
- 返回扣减后新库存值是否>=0。
- result < 0, 返回false,扣减失败
回补库存
传入商品id数量,拼key,调用操作redis的increment()方法增加库存值
库存流水
库存流水其实就是库存日志表
- item_stock_log表,id,item_id,amount,status。
- id不用自增
- status有0、1、2.生成时为0,更新成功1,更新失败2。
库存流水的增删改查操作。插件生成
- entity/ItemStockLog 流水表的实体类
- dao/ItemStockLogMapper 默认生成增删改查接口
- resourses/mappers/ItemStockLogMapper.xml sql文件
- 使用时直接调用mapper
扣减库存
rocket/consumer/DecreaseStockConsumer.java
- 类似更新销量,
@Service注解@RocketMQMessageListener监听器注解,用在消费者类上。参数定义:主题、消费者组、标签- 创建日志
- 注入itemservice
onMessage(message)方法,参数传入接收到的string格式message。- 字符串消息转换成json对象
- 从中解析出商品id和数量
- 调用itemService中原先操作MySQL的方法,
decreaseStock减少库存/持久化- 成功、出现问题,打印日志。
生成流水、发送消息,预减库存+创建订单、更新流水
rocket/producer/LocalTransactionListenerImpl 监听器实现类
-
监听器监听消息时,可以从消息头中获取到当前消息的tag标签,以此处理不同业务
-
返回值都是
RocketMOLocalTransactionState消息队列本地事务状态 -
注:
一个项目只能有一个处理事务消息的Listener,需要事务保障的业务,都需要走这个Listener。所以不止有扣减库存逻辑,还有创建订单等等逻辑,要判断
本地事务
——围绕创建订单
executeLocalTransaction()本地事务方法,返回值类型是RocketMOLocalTransactionState- 从message拿到消息头,获取消息的标签,转换成字符串
- 如果标签是“减少库存”,就创建订单
- 否则,就返回unknown。目前业务逻辑只有创建订单,没有其他操作
- 如果期间出现异常报错,记录日志,rollback回滚
创建订单
createOrder()方法- 通过传入的
arg参数,获取到/解析出:用户id,商品id,数量,活动id,流水id - 调用
orderService的createOreder()方法,创建订单。 - 因为引入了流水,创建订单后要更新流水,所以创建订单方法改了,传入参数增加流水id
- 成功打印日志,返回commit。失败打印日志,返回rollback
回查
checkLocalTransaction()方法- 同样先获取tag标签,判断业务
- 是扣减库存就调用当前类 的
checkStockStatus()方法,检查库存状态 - 否则,返回unknown
- 如果出异常,打印日志,返回rollback
查库存状态(第六步)
checkStockStatus()方法- 从message中得到二进制数据,还原成string,再还原成jsonobject对象
- 从jsonobject获取流水id
- 调用itemService的
findItemStockLogById()方法通过流水id查流水 - 如果查不到流水,说明有问题,返回rollback回滚
- 如果查到了状态为0(默认),说明没成功也没失败,返回unknown
- 如果查到了状态为1,说明成功,commit。否则为2,失败,回滚rollback
以上本地事务监听器类,⭐消息业务逻辑包括:本地事务,创建订单,回查+查库存
service/impl/OrderServiceImpl.java——创建订单方法(实操业务逻辑),本地事务调用这个类
-
创建订单方法
createOrder()。传入了流水id。 -
@Transactional注解,有事务保障,让订单生成和流水更新,同时成功或失败 -
判断判断判断。。。。
-
先预减库存。扣缓存的库存,返回标识successful是否成功。没成功抛异常。
-
成功,就创建订单。new一个order,存入各种数据。
-
异步更新销量
-
更新库存流水状态。因传入了流水id,直接调用itemService的
updateItemStockLogStatus(流水id,状态值)方法,由流水id更新状态为1
⭐包含预减库存、创建订单、异步更新销量、更新库存流水,的实操业务逻辑代码
⭐rocket包中都是消息队列的业务逻辑代码。实操业务逻辑是调用service的接口完成
⭐本篇内容包含前两大块。注意逻辑区分。
- 消息业务逻辑——rcoket包
- 实操业务逻辑——service/impl
- SQL代码——mapper
生成流水+发送消息
controller/OrderController
其中的create() 方法,生成流水+发送消息
其中调用orderService.createOrderAsync()方法,生成流水+发送消息,逻辑是异步创建订单但主要是发消息
service/impl/OrderSer
viceImpl.java——createOrderAsync()方法
createOrderAsync()方法,传入参数- 先判断redis中售罄标识是否存在。未售罄继续
- 调用
itemService.createItemStockLog()生成流水。打印日志- 看一下service/ItemServiceImpl.java的
createItemStockLog()方法- 传入参数,实例化log即流水。来简单封装
- 用uuid做id,存入log,默认状态为0
- 调用
itemStockLogMapper.insert,把流水存到数据库中 - 返回log流水
- 看一下service/ItemServiceImpl.java的
- 消息体。用body对象封装发送消息所需参数。包含:商品id+数量+流水id
- 事务成功时,发的消息被消费者接收;不成功,在回查时被接收。
- 消费者要扣减库存,需要itemId+amount
- 回查需要流水id,itemStockLogId
-
本地事务只在第三步做,单独一个arg对象封装参数
- LocalTransactionListenerlmpl.java的本地事务executeLocalTransaction方法有两个参数:事务消息msg+额外参数arg
-
new对象,put入参数:用户id、商品id、数量、活动id、流水id
- 封装好后,发送消息
- 定义消息发送目的地dest
- 由body消息体build构建message
- 发消息前debug日志,便于跟踪执行流程
- 调用
rocketMQTemplate.sendMessageInTransaction()方法,发送事务性消息。参数有:消息目的地dest,消息msg,消息参数arg。返回一个发送结果对象。 - 判断发送消息的返回结果的状态码,是未知或回滚就报错。都不是表示创建成功。
以上成功,表示controller/OrderController中生成流水和发送消息成功。 走完给前端返回正式消息
为什么要用事务,预减库存和扣减库存要保证原子性。
热点问题
消息丢失。
5、6可以采用多节点主从复制,从节点复制保存消息。 或者同步双写,但是影响性能
默认10秒以后
消息重投——会引起重复消费
削峰限流防刷
解决OrderController中create创建订单接口(对前端来说是接口),流量大的问题。 虽然做了异步,但是流量太大也不行。
限制进来的流量,令牌数,100w发1k个
“交易”业务最终走到OrderServiceImpl.java的createOrder()方法, 把验证迁移出来,单独做一个验证环节
- 加验证码。平滑流量,一秒流量平摊到多秒。
- 大闸。令牌有限(我设成库存10倍),用户抢。令牌有富余,才颁发。
- 库存很多,令牌颁发过多,加限流器。限制服务器单机TPS,防止崩溃。限制访问量接近服务器极限值,多余的请求拒绝。
- 用队列(线程池)做缓冲。拓宽单线程的瓶颈。
依赖注入,修改配置
采用easy-captcha组件
resources/application-dev.properties 配置线程池
- 核心线程数5
- 队列10。核心线程放满了放队列
- 最大扩展核心线程数30
验证码组件功能测试
SeckillApplicationTest.java测试验证码组件
- new一个file文件,声明文件路径、文件名
- 输出流绑定文件
- new一个SpeCaptcha对象,生成图片。定义图片宽度、高度、字符数
- 调用对象.out,把图片传到输出流
- 返回验证码图片的答案
字符、算式、gif图、汉字验证码同理
SimulateBackendData.java 测试代码模拟后台
-
活动商品不同,库存不同,大闸不同。每次上架商品,初始化大闸。项目没做后台功能,测试代码模拟
-
遍历活动商品,在redis中每个活动商品set新增对应的大闸kv,k:
promotion:gate:活动id,v:库存x10
OrderController.java——正式业务,前端接口
注入spring提供的线程池封装
声明限流器。每秒并发量1000.
创建订单前有几个步骤
1.获取验证码
⭐前端请求调用这个方法,响应一个图片
getCaptcha()方法- 登录后才能秒杀,所以参数有tocken
- 生成四字验证码图片
- 如果tocken不为空,从redis里获取用户
- 用户不为空,以用户维度,记录对应的生成的验证码。把key(captcha+用户id),value(生成验证码的答案)存入reids。一分钟过期。
configuration/WebMvcConfiguration 此处拦截器也做拦截
想要获取验证码,必须先验证登录状态。
- ⭐方法void无返回值,因为
响应返回一个图片/流,需要自己处理。所以注入HttpServletResponse response参数,要求spring传入response响应对象,我们从response响应对象中获取图片 - 设置响应对象格式,图片格式
- 从response里获取输出流
- specCaptcha.out利用输出流向客户端(前端)输出图片
2.请求令牌
前端请求验证码成功,才可以请求令牌
generateToken()方法,返回响应对象,参数:商品id,活动id,登录tocken,前端输入的验证码。- 获取登录的用户
- 判断验证码,为空抛异常报错。非空继续
- 拼出验证码key的格式,从redis里取真正的验证码(前面生成验证码时存的答案)
- 对比输入验证码和实际验证码,不相等报错。
equalsIgnoreCase()表示不区分大小写。 - 验证码正确,调用
promotionService接口的generateToken()方法,创建令牌
promotionServiceImpl.java实现类——包含验证+请求令牌
generateToken()方法- 验证售罄标识。有说明卖完,不发。无继续
- 用户为空、商品为空、活动为空/不对/状态不行,都不发。都正确继续
- 验证大闸,redis中获取该商品的大闸数,直接减1。小于零,大闸已用完,不发
- 验证都通过,生成令牌。k-【拼key格式,表明令牌的用户+商品+活动】。v-【uuid生成tocken】。对应kv存redis中,十分钟失效要再次申请
promotionServiceImpl.java实现类——包含验证+请求令牌
- 请求到令牌后,判断是否为空,为空抛异常。非空把秒杀令牌存入响应对象返回
3.创建订单
create()方法,返回响应对象。参数:商品id、数量、活动id、秒杀大闸令牌、登录令牌- ⭐限制单机流量。
rateLimiter.tryAcquire(timeout: 1, TimeUnit.SECONDS)尝试申请限流器的令牌,一秒之内申请到就继续。没申请到说明已达上限,抛异常
- 申请到,就获取登录用户
- 验证秒杀/活动令牌。正确继续
- ⭐加入队列等待。
taskExecutor.submit()方法,线程池提交一个线程去执行。- new Callable()
call()方法就是执行。调用异步下单方法orderService.createOrderAsync()- 这种方式返回对象,但我们不关注结果,只要不报错就行,返回null
- 验证队列处理结果
future.get()没报错创建订单成功。报错抛异常,下单失败- 返回正确的响应对象
前端。
- 单击购买,调用后端OrderController接口的获取验证码getCaptcha()方法,弹出框,显示验证码图片。
- 输入验证码,确定以后。走到前端回调函数。从框中得到输入的验证码,向服务器发起生成tocken请求,带上输入的验证码。异步。
- 验证码正确,下单成功。