前言
花了一天,基于之前学的黑马点评中的秒杀优惠券功能,自己也在自己小程序实现类似秒杀的功能,我实现的是积分包秒杀,在黑马点评只是实现用户端进行秒杀购买,但是没有实现管理端和用户端之间的交互过程,黑马使用的是redis实现消息队列的功能,在这边我使用rebbitmq来实现异步发消息,也算是对之前学习黑马点评做的一次巩固拓展吧。在里面还有个人对秒杀功能的思维导图和对缓存业务的流程图,算是写的比较认真的一次博客吧。下面这个是优化后的版本,可以在看完这篇文章后来看看具体优化
基于之前的秒杀功能的优化(包括Sentinel在SpringBoot中的简单应用)-CSDN博客主要对之前的秒杀功能博客的一个完善,这里面涉及亮点的有使用Sentinel对接口进行限流和使用Sharding-JDBC对积分订单进行分表。
https://blog.csdn.net/qq_73440769/article/details/143997794?spm=1001.2014.3001.5501
SpringBoot中使用Sharding-JDBC实战(实战+版本兼容+Bug解决)-CSDN博客文章浏览阅读543次,点赞7次,收藏15次。这里整理的是使用SpringBoot3.2.4和ShardingSphere-JDBC5.5.0进行分表操作,里面有遇到的bug并且解决的流程,还有结合自己之前做的秒杀博客进行测试分表,很详细https://blog.csdn.net/qq_73440769/article/details/143992138?spm=1001.2014.3001.5501
目录
一、先说数据表设计
-- 积分包
create table integral_package
(
id bigint auto_increment
primary key,
name varchar(50) not null comment '积分包名',
integral int null comment '积分',
description varchar(300) null comment '积分包描述',
stock int null comment '头像包库存',
status int not null comment '状态 0:禁用,1:启用',
begin_time datetime null comment '开始时间',
end_time datetime null comment '结束时间',
create_time datetime null comment '创建时间',
update_time datetime null comment '更新时间',
create_user int null comment '创建人',
update_user int null comment '修改人'
)
comment '积分包';
-- 积分包订单
create table integral_package_order
(
id bigint not null comment '主键'
primary key,
user_id bigint unsigned not null comment '抢购积分包的用户id',
integral_package_id bigint unsigned not null comment '抢购的积分包id',
create_time timestamp default CURRENT_TIMESTAMP not null comment '抢购时间'
)
comment '积分包订单' row_format = COMPACT;
大致的表结构如上
二、结合代码讲解业务流程
在最后会给出全部的思维导图结构,在文章的最后会有相关的全部代码
1、秒杀功能基本是从下面这三个方向去着手分析
管理端用来管理积分包的增删改查,基于用户使用。用户端的话无非就两种(1、查询所有秒杀积分包信息展示在小程序上,2、还有查询库存。关于为什么我两个分开在后续会讲)。用户参与秒杀功能,无非就是两种可能性(用户参与秒杀抢购,用户查看自己的订单)--对于本次积分包而言,参与秒杀功能抢到后就积分加在用户身上了,用户查看自己的订单相当于用户可以比较明显的看到自己参与了这次获得,抢到了哪些积分包
2、分别讲解各个分支的主要实现思路
(1)秒杀积分包管理端
主要分成上面这五部分,增删改查,在我看来核心在启用/禁用积分包这部分功能
-1- 添加积分包
增没什么好说的 ,逻辑固定,对于添加积分包在黑马点评里面的逻辑是将积分缓存放入redis了,但是在我看来,我加了个启用禁用,能够不需要一添加积分包就加入缓存,毕竟你添加秒杀积分包那一时刻还不需要让他参与秒杀,是开始秒杀不久前加在比较合适。
-2- 删除积分包
对于下面赘述的事情比较多,这里我在截图中结合代码的形式讲解了一下
不处于禁用状态不能删除积分包
-3- 修改积分包
由于关于缓存的事情都在启用禁用那边拦截了,这里写实现就再简单不过了,
不处于禁用状态不能修改积分包
-4- 查询所有积分包
-5- 启用/禁用积分包
在我看来这里是重中之重,对于管理端来说。
对于这里处理,如果启用就将秒杀积分包的库存放入缓存,如果禁用就清除缓存,但是实际上由于通过异步将库存同步在数据库了,删除缓存是不会有什么影响的,在下一次如果不删除积分包的话看到还有库存再启用也是一样的,那个库存也是数据库的库存。由于用户在小程序上看这个秒杀积分包肯定不能通过数据库去看,本来秒杀这个业务人就多了,还这样子打在数据库上,肯定不行,就需要加上一个缓存秒杀积分包的信息,就是下面这个:
// 清除秒杀包缓存信息
cleanCache(INTEGRALPACKAGE_VOLIST_ALL_KEY);
但是这个缓存信息不包括库存,在我的设计上将库存和固定死的信息分开,具体的在后续再讲到。
该部分思维导图如下:
(2)秒杀积分包用户端
-1- 查询所有秒杀积分包信息
两种情况:数据库中有积分包,数据库中没积分包
数据库中有积分包:
数据库中没积分包:
-2- 根据积分包id去查询库存
有可能说,如果有用不存在的id去传,人家做缓存击穿,你怎么去避免呢,当然是在redis放一些假数据防止缓存击穿了,设置生效3分钟的缓存。
该部分思维导图如下:
(3)用户参与秒杀功能
-1- 用户抢购秒杀积分包
在这里趋向于讲秒杀的部分,对于缓存穿透也可以参考上面的防止缓存击穿。
这里主要讲两点关于处理秒杀的主要实现:
使用redisson实现分布式锁确保同一时间只有一个用户能够处理特定的秒杀请求
使用lua脚本和Redis 的
SISMEMBER
命令保证原子性,保证一人一单
下面通过文心一言对两个的介绍:
Redisson 的主要作用
Redisson
是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid)。它提供了丰富的分布式数据结构和工具,包括分布式锁、分布式集合、分布式信号量等。在这个秒杀场景中,Redisson
主要用于实现分布式锁。
- 分布式锁:
- 秒杀活动通常面临高并发访问,如果多个用户同时请求同一个秒杀商品,可能会导致数据不一致的问题。
- 通过
Redisson
实现的分布式锁,可以确保同一时间只有一个用户能够处理特定的秒杀请求,从而避免了并发问题。- 在这段代码中,
redisLock.tryLock()
尝试获取锁,如果成功,则当前用户可以继续执行秒杀逻辑;如果失败,则说明有其他用户正在处理秒杀,当前用户请求被拒绝。- 锁释放:
- 在
finally
块中调用redisLock.unlock()
,确保无论秒杀操作是否成功,锁都会被释放,以避免死锁问题。Lua 脚本的主要作用
Lua 是一种轻量级的脚本语言,Redis 支持通过 Lua 脚本执行多个命令,并保证这些命令作为一个原子操作执行。在这个秒杀场景中,Lua 脚本主要用于检查库存和下单逻辑的原子性操作。
- 原子性操作:
- 秒杀活动中,库存检查和下单操作需要作为一个原子操作执行,以避免超卖或重复下单的问题。
- 通过 Lua 脚本,可以将多个 Redis 命令(如检查库存、减少库存、记录订单等)封装为一个原子操作,确保这些命令在执行过程中不会被其他命令打断。
- 减少网络开销:
- 如果没有 Lua 脚本,每个操作都需要与 Redis 服务器进行一次网络通信,这会增加网络开销和延迟。
- 通过 Lua 脚本,可以将多个操作合并为一个请求,减少网络通信次数,提高性能。
- 安全性:
- 在 Lua 脚本中,所有操作都在 Redis 服务器端执行,避免了客户端执行操作可能带来的安全漏洞。
- 总结如下:
1. Redisson 分布式锁和 Lua 脚本的作用
Redisson 分布式锁:
确保在高并发环境中,只有一个线程可以获取锁并执行抢购逻辑。这可以防止多个线程同时进入同一操作,降低数据竞争的风险。
但仅依靠锁并不能解决一人多单的问题,因为一个用户在锁释放后可能再次尝试抢购。
Lua 脚本:
Lua 脚本在 Redis 中是原子执行的,确保整个操作(如获取锁、执行抢购逻辑等)不会被其他操作打断。
然而,Lua 脚本本身并不能直接防止一个用户多次抢购。
2. Redis 集合的 sadd 操作
sadd 操作:
利用 Redis 的集合数据结构,将用户 ID 存储在集合中。
当用户尝试抢购时,执行 sadd 操作,如果返回值为 1,表示该用户成功抢购(即用户 ID 之前不在集合中);如果返回值为 0,表示该用户已存在于集合中,抢购失败,防止了同一用户多次抢购。
3. 综合说明
整体流程:
用户请求抢购时,首先尝试获取 Redisson 分布式锁。
获取锁后,执行 Lua 脚本进行抢购逻辑。
Lua 脚本中使用 sadd 操作将用户 ID 添加到 Redis 集合中。
根据 sadd 的返回值判断该用户是否已经抢购过,从而实现一人一单的逻辑。
1.在库存足够的时候,先查是否库存足够,足够的话就去争抢分布式锁
2.拿到分布式锁,然后去执行lua脚本,执行原子性操作
在程序一开始的时候,就加载lua脚本:
先拿到锁的用户就先执行lua脚本:
该执行过程由于不需要传入redis的key,所以这里传一个Collections.emptyList(),但是需要传lua脚本需要的参数,一个是积分包的id,一个是用户的id,传入积分包id一个作用就是扣减库存,第二个就是一人一单,讲积分包的id和用户的id放入redis的集合中,该集合只能一个插入一次,如果sismember这个指令后返回的是1则说明存在该缓存,然后结束脚本,结合sadd指令一个集合只能一个插入一次保证用户只能下一次单。
3.通过脚本的返回执行相应操作
4.保证一人一单后发送异步消息,通过rabbitmq实现异步消息,然后实现下单逻辑
发送者:
接收者:
执行下单功能:
下单功能就是比较基础的一些操作,这里就不多做讲述了
5.释放锁,然后返回订单号
-2- 用户查看自己的积分包订单
这个业务比较简单不多做赘述。
该部分思维导图如下:
实现秒杀从管理端到用户端到用户使用大致简化思路就是上面这些,如果有大佬还有什么其他见解欢迎评论一起讨论一下,下面给出总的思维导图:
对于上面那几个基本思路可能看起来很容易理解,但是如深度去对比缓存之间的使用,还有业务的交互,代码的交互,可能就有不一样的理解
三、缓存之间的使用
这里用一个自己建的图来表示缓存之间的关系:
管理员添加积分包后启用积分包,创建库存缓存,有了库存说明秒杀开始,用户登录小程序,查看积分包,创建积分包缓存,用来其他用户可以直接访问缓存,用户参与秒杀后通过库存缓存扣减积分包库存,然后创建一个用户与积分包之间关系的缓存,该缓存即使禁用积分包,但是如果积分包不完全删除,存在重新上架的可能性,就不能在启用禁用积分包的位置删除该缓存,用户与积分包之间关系的缓存应该在管理员删除积分包的时候删除缓存。秒杀结束后,禁用积分包删除积分包库存缓存,删除积分包所有信息缓存,至此,整个秒杀业务关于缓存的流程结束。
四、接口测试展示
秒杀活动开始
1、管理员添加积分包
2、管理员启用积分包
3、用户查看积分包
4、用户参与秒杀活动
积分包库存减一
秒杀活动结束
5、用户查看积分包订单
6、管理员禁用积分包
只剩下用户与积分包之间关系的缓存
7、用户查看积分包
只剩下假数据
但是用户还能看到自己的积分包订单
8、管理员删除积分包
用户再次查看自己的积分包订单:
缓存中也没有了
上述就是接口测试的流程。
五、JMeter压测
1、测试一人一单
先提前准备好100库存
开始压测
token:
由于上面的积分包不对,为缓存击穿的情况:
调整正确后:
压测后redis:
数据库:
都没有存在超卖现象。
2、测试多人高并发
这里我之前写了一个表来放一些token,然后将表里的token写入txt来测试
package com.quick.login;
import cn.hutool.json.JSONUtil;
import com.quick.entity.UserToken;
import com.quick.mapper.UserMapper;
import com.quick.mapper.UserTokenMapper;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.util.List;
@SpringBootTest
public class TokenTest {
@Resource
private UserMapper userMapper;
@Resource
private UserTokenMapper userTokenMapper;
@Test
public void getToken(){
String tokenFilePath = "tokens.txt"; // 存储Token的文件路径
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(tokenFilePath));
List<UserToken> userTokens = userTokenMapper.selectList(null);
for (UserToken userToken : userTokens) {
String json = JSONUtil.toJsonStr(userToken.getToken()+",");
writer.write(json);
writer.newLine();
}
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
开始测试
看redis:
看数据库
不存在超卖现象
六、主要代码
在下面的代码中,已经设置了假删除,删除数据并不会真实的从数据库中删除,只是设置了假删除的标签,这样子用户查询自己的订单的时候也可以找到自己的订单。
数据库表更改后的结构如下:
在代码中不知道为啥异步消息更新用户的时候会失效,所以我用户更新积分相对于上面的分析独立出来一个接口,单独用来更新用户积分信息,如果有大佬知道为什么异步更新的时候为什么用户更新失效,然而其他的更新成功可以滴滴我一下。
pom.xml:
<!-- 提供缓存功能-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
具体分布式锁可以参考我另外一篇博客:
关于RabbitMQ可以参考我另外一篇博客:
SpringBoot中整合RabbitMQ(测试+部署上线 最完整)_springboot rabbitmq-CSDN博客
common:
package com.quick.constant;
/**
* Redis中key的常量类
* 各种关键字信息可填入这里
*/
public class RedisConstant {
/**
* 积分包相关redis的key
*/
public static final String INTEGRALPACKAGE_STOCK_KEY = "integral_package:stock:"; // 秒杀积分包库存+积分包id
public static final String INTEGRALPACKAGE_ORDER_KEY = "integral_package:order:"; // 秒杀积分包订单+订单id
public static final String INTEGRALPACKAGE_VOLIST_ALL_KEY = "integral_package:VOList:all"; // 秒杀积分包所有数据
public static final String INTEGRALPACKAGE_VOLIST_NONE_KEY = "integral_package:VOList:none"; // 秒杀积分包假数据
public static final String INTEGRALPACKAGE_VOLIST_ZERO_STOCK_KEY = "integral_package:VOList:zeroStock"; // 缓存假数据
}
/**
* Redisson分布式锁常量类
*/
public class RedissonConstant {
/**
* 积分包秒杀加入订单分布式锁
*/
public static final String LOCK_INTEGRALPACKAGE_ORDER_KEY = "quick:lock:integralPackage-order:";
}
package com.quick.constant;
/**
* 信息提示常量类
* 各种关键字信息可填入这里
*/
public class MessageConstant {
public static final String INTEGRALPACKAGE_BE_ENABLE = "该名称的积分包已启用,不能删除或修改";
public static final String INTEGRALPACKAGE_IS_NULL = "当前头像包为空,不能删除或修改";
public static final String DUPLICATE_ORDERS_ARE_NOT_ALLOWED="不允许重复下单";
public static final String NOT_ENOUGH_STOCK=" 库存不足";
public static final String SEND_CREATE_INTEGRALPACKAGE_ORDER_MESSAGE_FAIL = "发送添加积分包订单消息失败";
}
package com.quick.constant;
/**
* 状态常量,启用或者禁用
*/
public class StatusConstant {
//启用
public static final Integer ENABLE = 1;
//禁用
public static final Integer DISABLE = 0;
}
package com.quick.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
/**
* 通过redis生成唯一id工具类
*/
@Component
public class RedisIdWorker {
/**
* 开始时间戳(配置化),用于计算ID的时间部分
*/
@Value("${quick.redis-worker.begin-timestamp:1719312960}")
private long beginTimestamp;
/**
* 序列号的位数(配置化),决定了每天生成的最大ID数量
*/
@Value("${quick.redis-worker.count-bits:32}")
private int countBits;
/**
* 序列号最大值,根据序列号位数计算得出
*/
private final long countMax = (1L << countBits) - 1;
// Redis操作模板类,用于与Redis交互
private final StringRedisTemplate stringRedisTemplate;
public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 生成下一个全局唯一ID
*
* @param keyPrefix Redis键的前缀,用于区分不同的业务场景
* @return 返回生成的ID
*/
public long nextId(String keyPrefix) {
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowSecond - beginTimestamp;
// 检查时间戳是否有效
if (timestamp < 0) {
throw new RuntimeException(String.format("时钟回退。拒绝生成ID,当前时间戳为: %d", timestamp + beginTimestamp));
}
// 2. 生成序列号
// 2.1. 获取当前日期,精确到天
String date = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
String redisKey = "icr:" + keyPrefix + ":" + date;
// 2.2. 自增长获取序列号
long count = stringRedisTemplate.opsForValue().increment(redisKey);
// 3. 拼接时间戳和序列号,返回生成的ID
return (timestamp << countBits) | count;
}
}
pojo:
dto:
package com.quick.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 积分包
* </p>
*
* @author bluefoxyu
* @since 2024-09-29
*/
@Data
public class IntegralPackageDTO implements Serializable {
private Long id;
@Schema(description = "积分包名")
private String name;
@Schema(description = "积分")
private Long integral;
@Schema(description = "积分包描述")
private String description;
@Schema(description = "头像包库存")
private Long stock;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "开始时间")
private LocalDateTime beginTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "结束时间")
private LocalDateTime endTime;
}
entity:
package com.quick.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 积分包
* </p>
*
* @author bluefoxyu
* @since 2024-09-29
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Schema(description="积分包")
public class IntegralPackage implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "积分包名")
private String name;
@Schema(description = "积分")
private Long integral;
@Schema(description = "积分包描述")
private String description;
@Schema(description = "头像包库存")
private Long stock;
@Schema(description = "状态 0:禁用,1:启用")
private Integer status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "开始时间")
private LocalDateTime beginTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "结束时间")
private LocalDateTime endTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建时间")
@TableField(value = "create_time",fill = FieldFill.INSERT)
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "修改时间")
@TableField(value = "update_time",fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@Schema(description = "创建人")
@TableField(value = "create_user",fill = FieldFill.INSERT)
private Long createUser;
@Schema(description = "修改人")
@TableField(value = "update_user",fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
}
package com.quick.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 积分包订单
* </p>
*
* @author bluefoxyu
* @since 2024-10-15
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("integral_package_order")
public class IntegralPackageOrder implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private Long id;
@Schema(description = "抢购积分包的用户id")
private Long userId;
@Schema(description = "抢购的积分包id")
private Long integralPackageId;
@Schema(description = "抢购时间")
@TableField(value = "create_time",fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}
vo:
package com.quick.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 积分包订单
* </p>
*
* @author bluefoxyu
* @since 2024-10-15
*/
@Data
public class IntegralPackageOrderVO implements Serializable {
@Schema(description = "积分包订单id")
private Long orderId;
@Schema(description = "积分包名")
private String name;
@Schema(description = "积分")
private Long integral;
@Schema(description = "积分包描述")
private String description;
@Schema(description = "抢购时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "开始时间")
private LocalDateTime beginTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "结束时间")
private LocalDateTime endTime;
}
package com.quick.vo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* <p>
* 积分包
* </p>
*
* @author bluefoxyu
* @since 2024-09-29
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Schema(description="积分包VO")
public class IntegralPackageVO implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(description = "积分包名")
private String name;
@Schema(description = "积分")
private Long integral;
@Schema(description = "积分包描述")
private String description;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "开始时间")
private LocalDateTime beginTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "结束时间")
private LocalDateTime endTime;
}
controller:
package com.quick.controller.admin;
import com.quick.dto.IntegralPackageDTO;
import com.quick.entity.IntegralPackage;
import com.quick.result.Result;
import com.quick.service.IIntegralPackageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* 积分包 前端控制器
* </p>
*
* @author bluefoxyu
* @since 2024-09-29
*/
@RestController("adminIntegralPackageController")
@RequestMapping("/admin/integral-package")
@Tag(name = "管理端-积分包接口")
@Slf4j
public class IntegralPackageController {
@Resource
private IIntegralPackageService integralPackageService;
// 添加积分包
@PostMapping
@Operation(summary = "添加积分包")
public Result<String> save(@RequestBody IntegralPackageDTO integralPackageDTO){
integralPackageService.saveIntegralPackage(integralPackageDTO);
return Result.success("添加积分包成功!");
}
// 删除积分包
@DeleteMapping("/deleteByIds")
@Operation(summary = "删除积分包")
public Result<String> delete(@RequestParam List<Long> ids){
integralPackageService.removeBy(ids);
return Result.success("删除积分包成功!");
}
// 修改积分包
@PutMapping("/update")
@Operation(summary = "修改积分包")
public Result<String> update(@RequestBody IntegralPackageDTO integralPackageDTO){
integralPackageService.updateIntegralPackage(integralPackageDTO);
return Result.success("修改积分包成功!");
}
//查询积分包
@GetMapping("/selectAll")
@Operation(summary = "查询所有积分包")
public Result<List<IntegralPackage>> selectAll(){
return Result.success(integralPackageService.getList());
}
// 启用/禁用积分包
@PutMapping("/startOrStop/{integralPackageId}/{status}")
@Operation(summary = "启用/禁用积分包")
public Result<String> startOrStop(@PathVariable Integer status,@PathVariable Long integralPackageId){
integralPackageService.startOrStop(status, integralPackageId);
return Result.success("启用/禁用积分包成功!");
}
}
package com.quick.controller.user;
import com.quick.result.Result;
import com.quick.service.IIntegralPackageOrderService;
import com.quick.vo.IntegralPackageOrderVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* 积分包订单 前端控制器
* </p>
*
* @author bluefoxyu
* @since 2024-10-15
*/
@RestController("userIntegralPackageOrderController")
@RequestMapping("/user/integral-package-order")
@Tag(name = "C端-积分包订单接口")
public class IntegralPackageOrderController {
@Resource
private IIntegralPackageOrderService integralPackageOrderService;
/**
* 秒杀积分包实现,加入订单
* @param integralPackageId 积分包id
* @return 订单id
*/
@PostMapping("seckillIntegralPackage/{id}")
@Operation(summary = "用户抢购秒杀积分包")
public Result<Long> seckillIntegralPackage(@PathVariable("id") Long integralPackageId) {
return integralPackageOrderService.seckillIntegralPackage(integralPackageId);
}
@GetMapping("getMyOrder")
@Operation(summary = "用户查看自己的积分包订单")
public Result<List<IntegralPackageOrderVO>> getMyOrder() {
return Result.success(integralPackageOrderService.getMyOrder());
}
@PutMapping("addIntegralInUser")
@Operation(summary = "将积分订单中的积分计入用户积分中")
public Result<String> addIntegralInUser(@RequestBody IntegralPackageOrderVO integralPackageOrderVO) {
return Result.success(integralPackageOrderService.addIntegralInUser(integralPackageOrderVO));
}
}
package com.quick.controller.user;
import com.quick.result.Result;
import com.quick.service.IIntegralPackageOrderService;
import com.quick.vo.IntegralPackageOrderVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* <p>
* 积分包订单 前端控制器
* </p>
*
* @author bluefoxyu
* @since 2024-10-15
*/
@RestController("userIntegralPackageOrderController")
@RequestMapping("/user/integral-package-order")
@Tag(name = "C端-积分包订单接口")
public class IntegralPackageOrderController {
@Resource
private IIntegralPackageOrderService integralPackageOrderService;
/**
* 秒杀积分包实现,加入订单
* @param integralPackageId 积分包id
* @return 订单id
*/
@PostMapping("seckillIntegralPackage/{id}")
@Operation(summary = "用户抢购秒杀积分包")
public Result<Long> seckillIntegralPackage(@PathVariable("id") Long integralPackageId) {
return integralPackageOrderService.seckillIntegralPackage(integralPackageId);
}
@GetMapping("getMyOrder")
@Operation(summary = "用户查看自己的积分包订单")
public Result<List<IntegralPackageOrderVO>> getMyOrder() {
return Result.success(integralPackageOrderService.getMyOrder());
}
}
service:
package com.quick.service;
import com.quick.entity.IntegralPackageOrder;
import com.baomidou.mybatisplus.extension.service.IService;
import com.quick.result.Result;
import com.quick.vo.IntegralPackageOrderVO;
import java.util.List;
/**
* <p>
* 积分包订单 服务类
* </p>
*
* @author bluefoxyu
* @since 2024-10-15
*/
public interface IIntegralPackageOrderService extends IService<IntegralPackageOrder> {
/**
* 秒杀积分包实现,加入订单
* @param integralPackageId 积分包id
* @return 订单id
*/
Result<Long> seckillIntegralPackage(Long integralPackageId);
void createIntegralPackageOrder(IntegralPackageOrder integralPackageOrder);
List<IntegralPackageOrderVO> getMyOrder();
String addIntegralInUser(IntegralPackageOrderVO integralPackageOrderVO);
}
package com.quick.service;
import com.quick.dto.IntegralPackageDTO;
import com.quick.entity.IntegralPackage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.quick.vo.IntegralPackageVO;
import java.util.List;
/**
* <p>
* 积分包 服务类
* </p>
*
* @author bluefoxyu
* @since 2024-09-29
*/
public interface IIntegralPackageService extends IService<IntegralPackage> {
void saveIntegralPackage(IntegralPackageDTO integralPackageDTO);
void removeBy(List<Long> ids);
void startOrStop(Integer status, Long id);
void updateIntegralPackage(IntegralPackageDTO integralPackageDTO);
List<IntegralPackageVO> getIntegralPackageList();
Long getStockByIntegralPackageId(Long integralPackageId);
List<IntegralPackage> getList();
}
impl:
package com.quick.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.quick.constant.MessageConstant;
import com.quick.constant.RedisConstant;
import com.quick.constant.StatusConstant;
import com.quick.dto.IntegralPackageDTO;
import com.quick.entity.IntegralPackage;
import com.quick.entity.IntegralPackageOrder;
import com.quick.exception.IntegralPackageException;
import com.quick.mapper.IntegralPackageMapper;
import com.quick.mapper.IntegralPackageOrderMapper;
import com.quick.service.IIntegralPackageService;
import com.quick.vo.IntegralPackageVO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static com.quick.constant.RedisConstant.INTEGRALPACKAGE_VOLIST_ALL_KEY;
import static com.quick.constant.RedisConstant.INTEGRALPACKAGE_VOLIST_NONE_KEY;
/**
* <p>
* 积分包 服务实现类
* </p>
*
* @author bluefoxyu
* @since 2024-09-29
*/
@Slf4j
@Service
public class
IntegralPackageServiceImpl extends ServiceImpl<IntegralPackageMapper, IntegralPackage> implements IIntegralPackageService {
@Resource
private IntegralPackageMapper integralPackageMapper;
@Resource
private IntegralPackageOrderMapper integralPackageOrderMapper;
@Resource
private StringRedisTemplate stringRedisTemplate;
private static final LocalDateTime localDateTime = LocalDateTime.of(2024, 6, 25, 10, 56);
@Override
@Transactional(rollbackFor = Exception.class)
public void saveIntegralPackage(IntegralPackageDTO integralPackageDTO) {
IntegralPackage integralPackage=new IntegralPackage();
BeanUtils.copyProperties(integralPackageDTO,integralPackage);
integralPackage.setStatus(StatusConstant.DISABLE);
integralPackageMapper.insert(integralPackage);
}
@Override
@Transactional
public void removeBy(List<Long> ids) {
// 检查传入的ID列表是否为空
if (ids == null || ids.isEmpty()) {
throw new IllegalArgumentException("ID列表不能为空。");
}
// 构建查询条件,查找对应的积分包
LambdaQueryWrapper<IntegralPackage> lambdaQueryWrapper = Wrappers.lambdaQuery(IntegralPackage.class)
.in(IntegralPackage::getId, ids);
List<IntegralPackage> integralPackageList = integralPackageMapper.selectList(lambdaQueryWrapper);
// 遍历积分包,检查状态,如果启用则抛出异常
for (IntegralPackage integralPackage : integralPackageList) {
if (integralPackage.getStatus().equals(StatusConstant.ENABLE)) {
throw new IntegralPackageException(MessageConstant.INTEGRALPACKAGE_BE_ENABLE);
}
}
// 删除积分包的同时,删除积分包与用户关系缓存
ids.forEach(integralPackageId -> {
// 查找订单
LambdaQueryWrapper<IntegralPackageOrder> orderLambdaQueryWrapper = Wrappers.lambdaQuery(IntegralPackageOrder.class)
.eq(IntegralPackageOrder::getIntegralPackageId, integralPackageId);
List<IntegralPackageOrder> orders = integralPackageOrderMapper.selectList(orderLambdaQueryWrapper);
// 获取用户ID
List<Long> userIdsToDelete = orders.stream().map(IntegralPackageOrder::getUserId).distinct().toList(); // 去重用户ID
// 删除积分包与用户关系缓存
removeCache(integralPackageId, userIdsToDelete);
// 最后删除积分包记录,假删除
LambdaUpdateWrapper<IntegralPackage> delete = Wrappers.lambdaUpdate(IntegralPackage.class)
.eq(IntegralPackage::getId, integralPackageId)
.set(IntegralPackage::getIsDelete, 1);
integralPackageMapper.update(delete);
});
}
/**
* 删除与积分包相关的缓存
*
* @param integralPackageId 积分包ID
* @param userIdsToDelete 需要删除的用户ID列表
*/
private void removeCache(Long integralPackageId, List<Long> userIdsToDelete) {
String cacheKey = RedisConstant.INTEGRALPACKAGE_ORDER_KEY + integralPackageId;
// 先检查缓存key是否存在
Boolean isKeyExists = stringRedisTemplate.hasKey(cacheKey);
if (Boolean.TRUE.equals(isKeyExists)) {
// 如果存在,批量删除用户ID对应的缓存
Long removedCount = stringRedisTemplate.opsForSet().remove(cacheKey, userIdsToDelete.stream().map(Object::toString).toArray());
if (removedCount == null || removedCount <= 0) {
throw new IntegralPackageException("缓存键 "+cacheKey+" 删除失败,未找到相关用户ID");
}
} else {
throw new IntegralPackageException("缓存键 "+cacheKey+"不存在,可能该积分包没有相关订单");
}
}
@Override
public void startOrStop(Integer status, Long id) {
LambdaQueryWrapper<IntegralPackage> eq = Wrappers.lambdaQuery(IntegralPackage.class)
.eq(IntegralPackage::getId, id)
.eq(IntegralPackage::getIsDelete,0);
IntegralPackage integralPackage = integralPackageMapper.selectOne(eq);
System.out.println(integralPackage);
if(integralPackage==null){
throw new IntegralPackageException(MessageConstant.INTEGRALPACKAGE_IS_NULL);
}
integralPackage.setStatus(status);
integralPackageMapper.updateById(integralPackage);
// 删除用户查看所有秒杀积分包缓存
cleanCache(RedisConstant.INTEGRALPACKAGE_VOLIST_ALL_KEY);
if(Objects.equals(status, StatusConstant.DISABLE)){
//清除库存缓存
cleanCache(RedisConstant.INTEGRALPACKAGE_STOCK_KEY+id);
}else {
// 如果传入为启用,则往redis中插入秒杀积分包信息
stringRedisTemplate.opsForValue().set(RedisConstant.INTEGRALPACKAGE_STOCK_KEY +id,
integralPackage.getStock().toString());
}
}
@Override
public void updateIntegralPackage(IntegralPackageDTO integralPackageDTO) {
LambdaQueryWrapper<IntegralPackage> eq = Wrappers.lambdaQuery(IntegralPackage.class)
.eq(IntegralPackage::getId, integralPackageDTO.getId())
.eq(IntegralPackage::getIsDelete,0);
IntegralPackage byId = integralPackageMapper.selectOne(eq);
if(Objects.equals(byId.getStatus(), StatusConstant.DISABLE)){
throw new IntegralPackageException(MessageConstant.INTEGRALPACKAGE_BE_ENABLE);
}
IntegralPackage integralPackage=new IntegralPackage();
BeanUtils.copyProperties(integralPackageDTO,integralPackage);
integralPackageMapper.updateById(integralPackage);
}
@Override
public List<IntegralPackageVO> getIntegralPackageList() {
// 1.先从redis中获取缓存数据
String cachedJson = stringRedisTemplate.opsForValue().get(INTEGRALPACKAGE_VOLIST_ALL_KEY);
List<IntegralPackageVO> cachedList = JSONUtil.toList(cachedJson, IntegralPackageVO.class);
if (cachedList != null && !cachedList.isEmpty()) {
return cachedList;
}
// 2. Redis中不存在有效数据,从数据库查询
LambdaQueryWrapper<IntegralPackage> queryWrapper = Wrappers.lambdaQuery(IntegralPackage.class)
.eq(IntegralPackage::getStatus, StatusConstant.ENABLE)
.eq(IntegralPackage::getIsDelete,0)
.le(IntegralPackage::getBeginTime, LocalDateTime.now())
.ge(IntegralPackage::getEndTime, LocalDateTime.now());
List<IntegralPackage> dbResult = integralPackageMapper.selectList(queryWrapper);
if (dbResult != null && !dbResult.isEmpty()) {
// 2.1 数据库查询到数据,转换为VO对象并存入Redis
List<IntegralPackageVO> finalIntegralPackageVOS = new ArrayList<>();
dbResult.forEach(integralPackage -> {
IntegralPackageVO vo = BeanUtil.copyProperties(integralPackage, IntegralPackageVO.class);
finalIntegralPackageVOS.add(vo);
});
// 2.1.1 将结果缓存到Redis
stringRedisTemplate.opsForValue().set(INTEGRALPACKAGE_VOLIST_ALL_KEY, JSONUtil.toJsonStr(finalIntegralPackageVOS));
return finalIntegralPackageVOS;
} else {
// 返回的数据集合对象
List<IntegralPackageVO> finalIntegralPackageVOS = new ArrayList<>();
// 2.2 数据库中没有数据,处理假数据防止缓存击穿
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(INTEGRALPACKAGE_VOLIST_NONE_KEY))) {
// 2.2.1 存在假数据缓存的key,返回假数据
String fakeDataJson = stringRedisTemplate.opsForValue().get(INTEGRALPACKAGE_VOLIST_NONE_KEY);
finalIntegralPackageVOS = JSONUtil.toList(fakeDataJson, IntegralPackageVO.class);
} else {
// 2.2.2 Redis中没有假数据缓存,创建假数据并缓存
IntegralPackageVO fakeVO = createFakeIntegralPackageVO();
finalIntegralPackageVOS.add(fakeVO);
// 存入Redis
stringRedisTemplate.opsForValue().set(INTEGRALPACKAGE_VOLIST_NONE_KEY, JSONUtil.toJsonStr(finalIntegralPackageVOS));
}
return finalIntegralPackageVOS;
}
}
/**
* 创建一个假数据用于防止缓存击穿
* @return IntegralPackageVO 假数据对象
*/
private IntegralPackageVO createFakeIntegralPackageVO() {
return IntegralPackageVO.builder()
.id(0L)
.name("暂无秒杀积分包")
.integral(0L)
.description("暂无描述")
.beginTime(localDateTime)
.endTime(localDateTime)
.build();
}
@Override
public Long getStockByIntegralPackageId(Long integralPackageId) {
String stockStr = stringRedisTemplate.opsForValue().get(RedisConstant.INTEGRALPACKAGE_STOCK_KEY + integralPackageId);
if (stockStr==null){
// 可能是redis缓存被误删
LambdaQueryWrapper<IntegralPackage> eq = Wrappers.lambdaQuery(IntegralPackage.class)
.eq(IntegralPackage::getId, integralPackageId)
.eq(IntegralPackage::getStatus, StatusConstant.ENABLE)
.eq(IntegralPackage::getIsDelete,0);
IntegralPackage byId = integralPackageMapper.selectOne(eq);
if (byId != null) {
stockStr = byId.getStock().toString();
// 如果传入为启用,则往redis中插入秒杀积分包信息
stringRedisTemplate.opsForValue().set(RedisConstant.INTEGRALPACKAGE_STOCK_KEY +integralPackageId,
stockStr);
}else {
stringRedisTemplate.opsForValue()
.set(RedisConstant.INTEGRALPACKAGE_STOCK_KEY + integralPackageId, "0",3, TimeUnit.MINUTES);
return 0L;
}
}
return Long.parseLong(stockStr);
}
@Override
public List<IntegralPackage> getList() {
LambdaQueryWrapper<IntegralPackage> eq = Wrappers.lambdaQuery(IntegralPackage.class).eq(IntegralPackage::getIsDelete, 0);
return integralPackageMapper.selectList(eq);
}
private void cleanCache(String pattern){
Set<String> keys = stringRedisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
stringRedisTemplate.delete(keys);
}
}
}
package com.quick.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.quick.constant.MessageConstant;
import com.quick.constant.RedissonConstant;
import com.quick.context.BaseContext;
import com.quick.entity.IntegralPackage;
import com.quick.entity.IntegralPackageOrder;
import com.quick.entity.User;
import com.quick.exception.FavoriteException;
import com.quick.exception.IntegralPackageException;
import com.quick.exception.IntegralPackageOrderException;
import com.quick.listener.IntegralPackageListener;
import com.quick.mapper.IntegralPackageMapper;
import com.quick.mapper.IntegralPackageOrderMapper;
import com.quick.mapper.UserMapper;
import com.quick.result.Result;
import com.quick.service.IIntegralPackageOrderService;
import com.quick.utils.RedisIdWorker;
import com.quick.vo.IntegralPackageOrderVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.quick.constant.RedisConstant.INTEGRALPACKAGE_STOCK_KEY;
/**
* <p>
* 积分包订单 服务实现类
* </p>
*
* @author bluefoxyu
* @since 2024-10-15
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class IntegralPackageOrderServiceImpl extends ServiceImpl<IntegralPackageOrderMapper, IntegralPackageOrder> implements IIntegralPackageOrderService {
private final RedisIdWorker redisIdWorker;
private final RedissonClient redissonClient;
private final StringRedisTemplate stringRedisTemplate;
private final IntegralPackageOrderMapper integralPackageOrderMapper;
private final IntegralPackageMapper integralPackageMapper;
private final RabbitTemplate rabbitTemplate;
private final UserMapper userMapper;
// 加载lua脚本,执行脚本
//定义了一个静态的、不可变的(final)DefaultRedisScript 对象,用于加载和执行一个 Lua 脚本。
private static final DefaultRedisScript<Long> SECKILLINTEGRALPACKAGE_SCRIPT;
static {
SECKILLINTEGRALPACKAGE_SCRIPT = new DefaultRedisScript<>();
SECKILLINTEGRALPACKAGE_SCRIPT.setLocation(new ClassPathResource("lua/seckillIntegralPackage.lua"));
SECKILLINTEGRALPACKAGE_SCRIPT.setResultType(Long.class); //设置脚本的返回类型
}
/**
* 秒杀积分包实现,加入订单
* @param integralPackageId 积分包id
* @return 订单id
*/
@Override
public Result<Long> seckillIntegralPackage(Long integralPackageId) {
Long userId = BaseContext.getCurrentId();
long orderId = redisIdWorker.nextId("integralPackageOrder");
// 用缓存的key来防止缓存击穿
String cacheKey = INTEGRALPACKAGE_STOCK_KEY + integralPackageId;
// 预先判断积分包ID有效性
if (Boolean.FALSE.equals(stringRedisTemplate.hasKey(cacheKey))) {
// 积分包ID无效,设置空值缓存,并设置过期时间
stringRedisTemplate.opsForValue().set(cacheKey, "null", 5, TimeUnit.MINUTES); // 设置过期时间为5分钟
throw new IntegralPackageException(MessageConstant.INVALID_INTEGRAL_PACKAGE_ID);
}
if (integralPackageId==0){
throw new IntegralPackageException(MessageConstant.NOT_ENOUGH_STOCK);
}
// 创建锁对象
RLock redisLock = redissonClient.getLock(RedissonConstant.LOCK_INTEGRALPACKAGE_ORDER_KEY + userId);
// 尝试获取锁,不传参默认失败不等待,传参毫秒值默认获取锁失败等待
boolean isLock = redisLock.tryLock();
// 判断
if (!isLock) {
// 获取锁失败,直接返回失败或者重试
throw new IntegralPackageException(MessageConstant.DUPLICATE_ORDERS_ARE_NOT_ALLOWED);
}
try {
// 1.执行lua脚本 Collections.emptyList() 是用于传递给 Lua 脚本的键(keys)列表。
Long result = stringRedisTemplate.execute(
SECKILLINTEGRALPACKAGE_SCRIPT, // 脚本对象
Collections.emptyList(),
integralPackageId.toString(), userId.toString() // 传入脚本的两个参数
);
assert result != null;
// 拿到脚本的结果返回int类型比较
int r = result.intValue();
// 2.判断结果是否为0
if (r != 0) {
// 2.1.不为0 ,代表没有购买资格
return Result.error(r == 1 ? MessageConstant.NOT_ENOUGH_STOCK: MessageConstant.DUPLICATE_ORDERS_ARE_NOT_ALLOWED);
}
// 发布异步消息下单
IntegralPackageOrder integralPackageOrder = IntegralPackageOrder.builder()
.id(orderId)
.userId(userId)
.integralPackageId(integralPackageId)
.hasUse(0)
.build();
// 发送消息 (交换机+绑定的key和发送的消息)
try {
rabbitTemplate.convertAndSend(
IntegralPackageListener.INTEGRALPACKAGE_CREATE_DIRECT,
IntegralPackageListener.INTEGRALPACKAGE_CREATE_SUCCESS,
integralPackageOrder);
} catch (Exception e) {
throw new FavoriteException(MessageConstant.SEND_CREATE_INTEGRALPACKAGE_ORDER_MESSAGE_FAIL);
}
}finally {
// 释放锁
redisLock.unlock();
}
// 3.返回订单id
return Result.success(orderId);
}
@Override
public void createIntegralPackageOrder(IntegralPackageOrder integralPackageOrder) {
// 获取积分包ID和用户ID
Long integralPackageId = integralPackageOrder.getIntegralPackageId();
Long userId = integralPackageOrder.getUserId();
// 1. 查询是否已存在该用户的订单,避免重复下单
LambdaQueryWrapper<IntegralPackageOrder> orderLambdaQueryWrapper = Wrappers.lambdaQuery(IntegralPackageOrder.class)
.eq(IntegralPackageOrder::getIntegralPackageId, integralPackageId)
.eq(IntegralPackageOrder::getUserId, userId);
Long count = integralPackageOrderMapper.selectCount(orderLambdaQueryWrapper);
if (count > 0) {
// 如果订单已存在,返回错误信息
Result.error(MessageConstant.DUPLICATE_ORDERS_ARE_NOT_ALLOWED);
return;
}
// 2. 扣减库存
LambdaUpdateWrapper<IntegralPackage> lambdaUpdateWrapper = Wrappers.lambdaUpdate(IntegralPackage.class)
.setSql("stock=stock-1") // 减少库存
.eq(IntegralPackage::getId, integralPackageId) // 根据积分包ID查询
.gt(IntegralPackage::getStock, 0); // 确保库存大于0
int update = integralPackageMapper.update(lambdaUpdateWrapper);
if (update == 0) {
// 库存不足时返回错误信息
Result.error(MessageConstant.NOT_ENOUGH_STOCK);
return;
}
// 3. 插入新的订单记录
integralPackageOrderMapper.insert(integralPackageOrder);
/*// 4. 查询积分包,获取积分值
IntegralPackage integralPackage = integralPackageMapper.selectById(integralPackageId);
Long integral = integralPackage != null ? integralPackage.getIntegral() : 0L;
//5. 更新用户钱包(增加积分)
User user = userMapper.selectById(userId);
// 增加用户钱包中的积分
Long updatedWallet = user.getWallet() + integral;
user.setWallet(updatedWallet);
userMapper.updateById(user);*/
}
@Override
public List<IntegralPackageOrderVO> getMyOrder() {
Long userId = BaseContext.getCurrentId();
LambdaQueryWrapper<IntegralPackageOrder> orderLambdaQueryWrapper = Wrappers.lambdaQuery(IntegralPackageOrder.class)
.eq(IntegralPackageOrder::getUserId, userId);
List<IntegralPackageOrder> integralPackageOrders = integralPackageOrderMapper.selectList(orderLambdaQueryWrapper);
List<IntegralPackageOrderVO>integralPackageOrderVOS=new ArrayList<>();
for (IntegralPackageOrder integralPackageOrder : integralPackageOrders) {
Long integralPackageId = integralPackageOrder.getIntegralPackageId();
IntegralPackage integralPackage = integralPackageMapper.selectById(integralPackageId);
// 拷贝积分包信息
IntegralPackageOrderVO integralPackageOrderVO = BeanUtil.copyProperties(integralPackage, IntegralPackageOrderVO.class);
// 拷贝积分订单信息
integralPackageOrderVO.setOrderId(integralPackageOrder.getId());
integralPackageOrderVO.setHasUse(integralPackageOrder.getHasUse());
integralPackageOrderVO.setCreateTime(integralPackageOrder.getCreateTime());
integralPackageOrderVOS.add(integralPackageOrderVO);
}
return integralPackageOrderVOS;
}
@Override
public String addIntegralInUser(IntegralPackageOrderVO integralPackageOrderVO) {
Long userId = BaseContext.getCurrentId();
// 创建锁对象
RLock redisLock = redissonClient.getLock(RedissonConstant.LOCK_INTEGRALPACKAGEORDER_MYORDER_KEY + userId);
// 尝试获取锁,不传参默认失败不等待,传参毫秒值默认获取锁失败等待
boolean isLock = redisLock.tryLock();
// 判断
if (!isLock) {
// 获取锁失败,直接返回失败或者重试
throw new IntegralPackageException(MessageConstant.DUPLICATE_ORDERS_ADD_USER_ARE_NOT_ALLOWED);
}
try {
if (integralPackageOrderVO.getHasUse() == 1) {
throw new IntegralPackageOrderException(MessageConstant.INTEGRAL_HAS_ADD);
}
Long integral = integralPackageOrderVO.getIntegral();
User user = userMapper.selectById(userId);
user.setWallet(user.getWallet()+integral);
userMapper.updateById(user);
return "积分计入成功!";
}finally {
redisLock.unlock();
}
}
}
lua:
-- 1.参数列表
-- 1.1.积分包id
local integralPackageId = ARGV[1]
-- 1.2.用户id
local userId = ARGV[2]
-- 2.数据key
-- 2.1.库存key 'seckill:stock:' .. voucherId ==java中的字符串拼接
local stockKey = 'integral_package:stock:' .. integralPackageId
-- 2.2.订单key
local orderKey = 'integral_package:order:' .. integralPackageId
-- 3.脚本业务
-- 3.1.判断库存是否充足 get stockKey
-- tonumber 该函数可以将字符串转成数字
if(tonumber(redis.call('get', stockKey)) <= 0) then
-- 3.2.库存不足,返回1
return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
-- sismember 是 Redis 的集合命令,用于检查某个元素(在这里是 userId)是否属于指定的集合(在这里是 orderKey)。
-- 返回 1: 表示 userId 是集合中的成员(即该用户已经下过单)。
-- 返回 0: 表示 userId 不是集合中的成员(即该用户尚未下单)。
if(redis.call('sismember', orderKey, userId) == 1) then
-- 3.3.存在,说明是重复下单,返回2
return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
-- sadd 操作中插入集合的元素不允许重复,如果插入一个元素不存在返回1,存在则返回0
redis.call('sadd', orderKey, userId)
return 0
mq:
package com.quick.listener;
import com.quick.entity.IntegralPackageOrder;
import com.quick.service.IIntegralPackageOrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
/**
* 积分包下单监听器
*/
@Component
@RequiredArgsConstructor
public class IntegralPackageListener {
private final IIntegralPackageOrderService iIntegralPackageOrderService;
public static final String INTEGRALPACKAGE_CREATE_SUCCESS_QUEUE = "IntegralPackageOrder.createIntegralPackageOrder.success.queue";
public static final String INTEGRALPACKAGE_CREATE_DIRECT = "createIntegralPackageOrder.direct";
public static final String INTEGRALPACKAGE_CREATE_SUCCESS = "createIntegralPackageOrder.success";
// 异步实现积分包下单操作
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = INTEGRALPACKAGE_CREATE_SUCCESS_QUEUE, durable = "true"), // 队列 起名规则(服务名+业务名+成功+队列),durable持久化
exchange = @Exchange(name = INTEGRALPACKAGE_CREATE_DIRECT), // 交换机名称,交换机默认类型就行direct,所以不用配置direct
key = INTEGRALPACKAGE_CREATE_SUCCESS // 绑定的key
),
// 在@RabbitListener注解中指定容器工厂
containerFactory = "customContainerFactory")
@RabbitHandler
public void createIntegralPackageOrder(IntegralPackageOrder integralPackageOrder) {
iIntegralPackageOrderService.createIntegralPackageOrder(integralPackageOrder);
}
}
七、最后
在未来还会进行压测和小程序界面+web管理端的测试持续更新,对于前面那些见解欢迎大佬们提出见解,让我更加完善对于秒杀功能的实现,欢迎评论区留言~~