给自己复盘用的tjxt笔记day11第二部分

异步领券

优化方案分析

对于高并发问题,优化的思路有异步写和合并写。

其中,合并写请求比较适合应用在写频率较高,写数据比较简单的场景。而异步写则更适合应用在业务比较复杂,业务链较长的场景。

显然,领券业务更适合使用异步写方案。

思路分析与设计

不过这里存在一个问题:

并不是每一个用户都有领券资格,具体要校验了资格才知道。那我们在发送MQ消息后,就要返回给用户结果了,此时该告诉用户是领券成功还是失败呢

显然,无论告诉他哪种结果都不一定正确。因此,我们应该将校验领券资格的逻辑前置,在校验完成后再发MQ消息,完成数据库写操作:

方案进一步改进:

但是,校验领券资格的部分依然会有多次数据库查询,还需要加锁。效率提升并不明显,怎么办?

为了进一步提高效率,我们可以把优惠券相关数据缓存到Redis中,这样就可以基于Redis完成资格校验

优惠券缓存

缓存内容

优惠券资格校验需要校验的内容包括:

  • 优惠券发放时间

  • 优惠券库存

  • 用户限领数量

因此,为了减少对Redis内存的消耗,在构建优惠券缓存的时候,我们并不需要把所有优惠券信息写入缓存,而是只保存上述字段即可。

注意!!!!

既然要在缓存中保存优惠券库存,并且校验库存是否充足。那就必须在每次校验通过后,立刻扣减Redis中缓存的库存,否则缓存中库存一直不变,起不到校验是否超发的目的。

缓存数据结构

为了便于我们修改缓存中的库存数据,这里建议采用Hash结构,将库存作为Hash的一个字段,将来只需要通过HINCRBY命令即可修改。

Redis中的数据结构大概如图:

KEY(couponId)

field

value

couponId:10

issueBeginTime

20230327

issueEndTime

20230501

totalNum

100

userLimit

1

couponId:20

issueBeginTime

20230827

issueEndTime

20230901

totalNum

200

userLimit

2

上述结构中记录了券的每人限领数量:userLimit , 但是用户已经领取的数量并没有记录

一个券可能被多个用户领取,每个用户的已领取数量都需要记录。显然,还是Hash结构更加适合:

KEY(couponId)

field(userId)

value(count)

couponId:10

uid:110

1

uid:120

1

uid:130

1

uid:140

1

缓存KEY前缀

注意!!!

优惠券的缓存该何时添加呢?

优惠券一旦发放,就可能有用户来领券,因此应该在发放优惠券的同时直接添加优惠券缓存。而暂停发放时则应该将优惠券的缓存删除,下次再次发放时重新添加。

添加缓存

private final StringRedisTemplate redisTemplate;@Transactional
@Override
public void beginIssue(CouponIssueFormDTO dto) {// 1.查询优惠券Coupon coupon = getById(dto.getId());if (coupon == null) {throw new BadRequestException("优惠券不存在!");}// 2.判断优惠券状态,是否是暂停或待发放if(coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != PAUSE){throw new BizIllegalException("优惠券状态错误!");}// 3.判断是否是立刻发放LocalDateTime issueBeginTime = dto.getIssueBeginTime();LocalDateTime now = LocalDateTime.now();boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);// 4.更新优惠券// 4.1.拷贝属性到POCoupon c = BeanUtils.copyBean(dto, Coupon.class);// 4.2.更新状态if (isBegin) {c.setStatus(ISSUING);c.setIssueBeginTime(now);}else{c.setStatus(UN_ISSUE);}// 4.3.写入数据库updateById(c);// 5.添加缓存,前提是立刻发放的if (isBegin) {coupon.setIssueBeginTime(c.getIssueBeginTime());coupon.setIssueEndTime(c.getIssueEndTime());cacheCouponInfo(coupon);}// 6.判断是否需要生成兑换码,优惠券类型必须是兑换码,优惠券状态必须是待发放if(coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == CouponStatus.DRAFT){coupon.setIssueEndTime(c.getIssueEndTime());codeService.asyncGenerateCode(coupon);}
}private void cacheCouponInfo(Coupon coupon) {// 1.组织数据Map<String, String> map = new HashMap<>(4);map.put("issueBeginTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueBeginTime())));map.put("issueEndTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueEndTime())));map.put("totalNum", String.valueOf(coupon.getTotalNum()));map.put("userLimit", String.valueOf(coupon.getUserLimit()));// 2.写缓存redisTemplate.opsForHash().putAll(PromotionConstants.COUPON_CACHE_KEY_PREFIX + coupon.getId(), map);
}

移除缓存

@Override
@Transactional
public void pauseIssue(Long id) {// 1.查询旧优惠券Coupon coupon = getById(id);if (coupon == null) {throw new BadRequestException("优惠券不存在");}// 2.当前券状态必须是未开始或进行中CouponStatus status = coupon.getStatus();if (status != UN_ISSUE && status != ISSUING) {// 状态错误,直接结束return;}// 3.更新状态boolean success = lambdaUpdate().set(Coupon::getStatus, PAUSE).eq(Coupon::getId, id).in(Coupon::getStatus, UN_ISSUE, ISSUING).update();if (!success) {// 可能是重复更新,结束log.error("重复暂停优惠券");}// 4.删除缓存redisTemplate.delete(PromotionConstants.COUPON_CACHE_KEY_PREFIX + id);
}

实现异步领券

根据前面的思路分析:

实现异步领券分为两步:

  • 改造领券逻辑,实现基于Redis的领取资格校验,然后发送MQ消息

  • 编写MQ监听器,监听到消息后执行领券逻辑

定义MQ消息规范

MQ消息通信规范如下:

参数

说明

Exchange

promotion.topic

Routing-Key

coupon:receive

Message

参数名

类型

说明

userId

Long

用户id

couponId

Long

优惠券id

基于Redis的领取资格校验

  @Override@Lock(name = "lock:coupon:#{couponId}")public void receiveCoupon(Long couponId) {// 1.查询优惠券Coupon coupon = queryCouponByCache(couponId);if (coupon == null) {throw new BadRequestException("优惠券不存在");}// 2.校验发放时间LocalDateTime now = LocalDateTime.now();if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {throw new BadRequestException("优惠券发放已经结束或尚未开始");}// 3.校验库存if (coupon.getIssueNum() >= coupon.getTotalNum()) {throw new BadRequestException("优惠券库存不足");}Long userId = UserContext.getUser();// 4.校验每人限领数量// 4.1.查询领取数量String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;Long count = redisTemplate.opsForHash().increment(key, userId.toString(), 1);// 4.2.校验限领数量if(count > coupon.getUserLimit()){throw new BadRequestException("超出领取数量");}// 5.扣减优惠券库存redisTemplate.opsForHash().increment(PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId, "totalNum", -1);// 6.发送MQ消息UserCouponDTO uc = new UserCouponDTO();uc.setUserId(userId);uc.setCouponId(couponId);mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, uc);}private Coupon queryCouponByCache(Long couponId) {// 1.准备KEYString key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId;// 2.查询Map<Object, Object> objMap = redisTemplate.opsForHash().entries(key);if (objMap.isEmpty()) {return null;}// 3.数据反序列化return BeanUtils.mapToBean(objMap, Coupon.class, false, CopyOptions.create());}

监听MQ并领券

@RabbitListener(bindings = @QueueBinding(value = @Queue(name = "coupon.receive.queue", durable = "true"),exchange = @Exchange(name = PROMOTION_EXCHANGE, type = ExchangeTypes.TOPIC),key = COUPON_RECEIVE))public void listenCouponReceiveMessage(UserCouponDTO uc){userCouponService.checkAndCreateUserCoupon(uc);}

// 移除了锁,这里不需要加锁了
@Transactional
@Override
public void checkAndCreateUserCoupon(UserCouponDTO uc) {// 1.查询优惠券Coupon coupon = couponMapper.selectById(uc.getCouponId());if (coupon == null) {throw new BizIllegalException("优惠券不存在!");}// 2.更新优惠券的已经发放的数量 + 1int r = couponMapper.incrIssueNum(coupon.getId());if (r == 0) {throw new BizIllegalException("优惠券库存不足!");}// 3.新增一个用户券saveUserCoupon(coupon, uc.getUserId());// 4.更新兑换码状态if (uc.getSerialNum()!= null) {codeService.lambdaUpdate().set(ExchangeCode::getUserId, uc.getUserId()).set(ExchangeCode::getStatus, ExchangeCodeStatus.USED).eq(ExchangeCode::getId, uc.getSerialNum()).update();}
}

 异步的兑换码领券

思路分析

  • 生成兑换码时,将优惠券及对应兑换码序列号的最大值缓存到Redis中

  • 改造兑换优惠券的功能,利用Redis完成资格校验,然后发送MQ消息(消息体中要增加传递兑换码的序列号

  • 改造领取优惠券的MQ监听器,添加标记兑换码状态为已兑换的功能

缓存兑换码

生成兑换码时,将优惠券及对应兑换码序列号的最大值缓存到Redis中

@Override
@Async("generateExchangeCodeExecutor")
public void asyncGenerateCode(Coupon coupon) {// 发放数量Integer totalNum = coupon.getTotalNum();// 1.获取Redis自增序列号Long result = serialOps.increment(totalNum);if (result == null) {return;}int maxSerialNum = result.intValue();List<ExchangeCode> list = new ArrayList<>(totalNum);for (int serialNum = maxSerialNum - totalNum + 1; serialNum <= maxSerialNum; serialNum++) {// 2.生成兑换码String code = CodeUtil.generateCode(serialNum, coupon.getId());ExchangeCode e = new ExchangeCode();e.setCode(code);e.setId(serialNum);e.setExchangeTargetId(coupon.getId());e.setExpiredTime(coupon.getIssueEndTime());list.add(e);}// 3.保存数据库saveBatch(list);// 4.写入Redis缓存,member:couponId,score:兑换码的最大序列号redisTemplate.opsForZSet().add(COUPON_RANGE_KEY, coupon.getId().toString(), maxSerialNum);
}

改造领券功能

改造兑换优惠券的功能,利用Redis完成资格校验,然后发送MQ消息(消息体中要增加传递兑换码的序列号

@Override
@Lock(name = "lock:coupon:#{T(com.tianji.common.utils.UserContext).getUser()}")
public void exchangeCoupon(String code) {// 1.校验并解析兑换码long serialNum = CodeUtil.parseCode(code);// 2.校验是否已经兑换 SETBIT KEY 4 1boolean exchanged = codeService.updateExchangeMark(serialNum, true);if (exchanged) {throw new BizIllegalException("兑换码已经被兑换过了");}try {// 3.查询兑换码对应的优惠券idLong couponId = codeService.exchangeTargetId(serialNum);if (couponId == null) {throw new BizIllegalException("兑换码不存在!");}Coupon coupon = queryCouponByCache(couponId);// 4.是否过期LocalDateTime now = LocalDateTime.now();if (now.isAfter(coupon.getIssueEndTime()) || now.isBefore(coupon.getIssueBeginTime())) {throw new BizIllegalException("优惠券活动未开始或已经结束");}// 5.校验每人限领数量Long userId = UserContext.getUser();// 5.1.查询领取数量String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;Long count = redisTemplate.opsForHash().increment(key, userId.toString(), 1);// 5.2.校验限领数量if(count > coupon.getUserLimit()){throw new BadRequestException("超出领取数量");}// 6.发送MQ消息通知UserCouponDTO uc = new UserCouponDTO();uc.setUserId(userId);uc.setCouponId(couponId);uc.setSerialNum((int) serialNum);mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, uc);} catch (Exception e) {// 重置兑换的标记 0codeService.updateExchangeMark(serialNum, false);throw e;}
}

@Override
public Long exchangeTargetId(long serialNum) {// 1.查询score值比当前序列号大的第一个优惠券Set<String> results = redisTemplate.opsForZSet().rangeByScore(COUPON_RANGE_KEY, serialNum, serialNum + 5000, 0L, 1L);if (CollUtils.isEmpty(results)) {return null;}// 2.数据转换String next = results.iterator().next();return Long.parseLong(next);
}

改造领取优惠券的MQ监听器,添加标记兑换码状态为已兑换的功能

// 移除了锁,这里不需要加锁了
@Transactional
@Override
public void checkAndCreateUserCoupon(UserCouponDTO uc) {// 1.查询优惠券Coupon coupon = couponMapper.selectById(uc.getCouponId());if (coupon == null) {throw new BizIllegalException("优惠券不存在!");}// 2.更新优惠券的已经发放的数量 + 1int r = couponMapper.incrIssueNum(coupon.getId());if (r == 0) {throw new BizIllegalException("优惠券库存不足!");}// 3.新增一个用户券saveUserCoupon(coupon, uc.getUserId());// 4.更新兑换码状态if (uc.getSerialNum()!= null) {codeService.lambdaUpdate().set(ExchangeCode::getUserId, uc.getUserId()).set(ExchangeCode::getStatus, ExchangeCodeStatus.USED).eq(ExchangeCode::getId, uc.getSerialNum()).update();}
}

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

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

相关文章

Python | Leetcode Python题解之第377题组合总和IV

题目&#xff1a; 题解&#xff1a; class Solution:def combinationSum4(self, nums: List[int], target: int) -> int:dp [1] [0] * targetfor i in range(1, target 1):for num in nums:if num < i:dp[i] dp[i - num]return dp[target]

MyBatis的学习————下篇

目录 一、动态SQL 简介 1、if标签 2、where标签 3、trim标签 4、choose、when、otherwise 5、foreach 5.1、批量删除 5.2、批量添加 6、sql标签 二、MyBatis的缓存 1、一级缓存 2、二级缓存 3、二级缓存的相关配置 4、MyBatis缓存查询的顺序 5、 第三方缓存EHCac…

【滑动窗口法解决子数组,子串问题】

前言 在leetCode题解中看到一位大佬针对滑动窗口法解决子数组&#xff0c;子串问题的总结&#xff0c;觉得总结的非常好&#xff0c;成功地将滑动窗口法变成了默写题&#xff0c;在这里学习记录一下。 适用于 76.最小覆盖子串 567.字符串的排列 438.找到字符串中所有字母异位词…

WPF- vs中的WPF应用项目模板 如何自己实现

读书笔记 1. 单个 c#文件的 空白window应用程序 (只展示了一个button按钮) 2.C#文件 和xml文件 的空白window程序 .xml文件作为程序的资源 (只一个button按钮) 3. xmal和c#共同编译 形如使用VS 创建WPF应用项目模板 1.新建一个wpf空白项目 ,添加一个主c#文件 和xaml文件(属…

手算神经网络MAC和FLOP

在本文中&#xff0c;我们将深入探讨神经网络背景下的 MAC&#xff08;乘法累加运算&#xff09;和 FLOP&#xff08;浮点运算&#xff09;概念。通过学习如何使用笔和纸手动计算这些内容&#xff0c;你将对各种网络结构的计算复杂性和效率有基本的了解。 这是 colab 笔记本中…

Java语言程序设计基础篇_编程练习题*17.13 (带GUI的组合文件工具)

目录 题目&#xff1a;*17.13 (带GUI的组合文件工具) 代码示例 结果展示 题目&#xff1a;*17.13 (带GUI的组合文件工具) 改写编程练习题17.12使之带有GUI&#xff0c;如图1721b所示 可以使用编程练习题17.11的GUI代码和编程练习题17.12的程序代码&#xff1a; Java语言…

linux系统使用 docker 来部署运行 mysql5.7 并配置 docker-compose-mysql.yml 文件

Docker是一个开源的容器化平台&#xff0c;旨在简化应用程序的创建、部署和管理。它基于OS-level虚拟化技术&#xff0c;通过将应用程序和其依赖项打包到一个称为容器的标准化单元中&#xff0c;使得应用程序可以在任何环境中快速、可靠地运行。 Docker的优势有以下几个方面&a…

SpringMVC 笔记篇

1.1 执行流程 1.1.5 DispatcherServlet的init()——> 创建Spring容器 ——> initStrategies()方法 在1.1.4中DispatcherServlet中的init()方法创建Spring容器之外&#xff0c;其实还会做一件特别重要的事&#xff0c;在FrameworkServlet中的refresh()方法执行之前&…

景联文科技提供运动数据采集服务

运动数据的重要性 运动数据的收集与分析对于提升个人健康管理和运动表现具有重要意义。 通过收集心率、步态、速度等生理和运动参数&#xff0c;不仅可以为运动员提供个性化的训练方案&#xff0c;帮助其优化表现&#xff0c;还能早期发现并预防伤病。对于普通健身者而言&…

CSS溢出——WEB开发系列20

在网页设计中&#xff0c;“溢出”是一个常见且重要的概念。它涉及到如何处理那些超出预定范围的内容&#xff0c;以确保网页的布局和视觉效果达到预期。 一、什么是溢出&#xff1f; 在 CSS 中&#xff0c;“溢出”&#xff08;overflow&#xff09;指的是内容超出其包含块的…

python-pptx - Python 操作 PPT 幻灯片

文章目录 一、关于 python-pptx设计哲学功能支持 二、安装三、入门1、你好世界&#xff01;例子2、Bullet 幻灯片示例3、add_textbox()示例4、add_picture()示例5、add_shape()示例6、add_table()示例7、从演示文稿中的幻灯片中提取所有文本 四、使用演示文稿1、打开演示文稿2、…

Marscode:程序员的智能伙伴,2024 活动震撼来袭

在程序员的世界里&#xff0c;高效的开发工具如同手中的利器&#xff0c;能让我们在代码的海洋中披荆斩棘。今天&#xff0c;我要向大家隆重介绍一款强大的智能开发工具——Marscode&#xff0c;以及它带来的精彩 2024 活动。 一、Marscode&#xff1a;智能开发的新势力 Mars…

SQLite的安装和使用

一、官网链接下载安装包 点击跳转 步骤&#xff1a;点击安装这个红框的dll以及红框下面的tools &#xff08;如果有navicat可以免上面这个安装步骤&#xff0c;安装上面这个是为了能在命令行敲SQL而已&#xff09; 二、SQLite的特点 嵌入的&#xff08;无服务器的&#x…

【机器学习】 7. 梯度下降法,随机梯度下降法SGD,Mini-batch SGD

梯度下降法,随机梯度下降法SGD,Mini-batch SGD 梯度下降法凸函数(convex)和非凸函数梯度更新方向选择步长的选择 随机梯度下降SGD(Stochastic Gradient Descent)梯度下降法&#xff1a;SGD: Mini-batch SGD 梯度下降法 从一个随机点开始决定下降方向&#xff08;重要&#xff…

基于PHP+MySQL组合开发的微信投票小程序 带完整的安装代码包以及搭建教程

系统概述 这款基于 PHPMySQL 组合开发的微信投票小程序是一款专门为满足各类投票需求而设计的应用程序。它利用 PHP 强大的服务器端编程能力和 MySQL 高效的数据存储和管理能力&#xff0c;为用户提供了一个稳定、可靠、功能丰富的投票平台。 该小程序支持多种投票类型&#…

centos安装docker并配置加速器

docker安装与卸载&#xff1a; 1、检查当前是否安装docker yum list installed | grep docker2、卸载docker 根据yum list installed | grep docker查询出来的内容&#xff0c;逐个进行删除 yum remove docker.x86 64 -y3、启动与关闭docker 4、删除/etc/docker文件夹 如果…

jmeter 响应乱码

Jmeter在做接口测试的时候的&#xff0c;如果接口响应的内容中有中文&#xff0c;jmeter的响应内容很可能显示乱码&#xff0c;为了规避这种出现乱码的问题&#xff0c;就要对jmeter的响应结果进行编码处理。 打开jmeter进行接口、压力、性能等测试&#xff0c;出现以下乱码问…

Java短剧系统新生态智能系统打造个性化影视体验小程序源码

短剧新生态&#xff0c;智能系统打造个性化影视体验✨&#x1f3ac; &#x1f389; 开篇&#xff1a;短剧新纪元&#xff0c;个性化体验来袭 在这个快节奏的时代&#xff0c;短剧以其精炼的剧情和高效的传播力&#xff0c;正逐渐成为影视娱乐的新宠儿&#xff01;&#x1f38…

如何恢复删除的微信好友?不留遗憾,这5招教你找回

在社交生活中&#xff0c;微信成为了我们日常沟通的重要工具之一。然而&#xff0c;偶尔因一时冲动或误操作删除了重要好友&#xff0c;当我们想找回好友时是否也在懊悔呢&#xff1f;如何恢复删除的微信好友就成为了我们必须学会的技能。那么&#xff0c;今天我们就来分享5种高…

10、ollama启动LLama_Factory微调大模型(llama.cpp)

在前面章节中介绍了如何使用LLama_Factory微调大模型&#xff0c;并将微调后的模型文件合并导出&#xff0c;本节我们我们看下如何使用ollama进行调用。 1、llama.cpp LLama_Factory训练好的模型&#xff0c;ollama不能直接使用&#xff0c;需要转换一下格式&#xff0c;我们…