基于之前的秒杀功能的优化(包括Sentinel在SpringBoot中的简单应用)
这篇博客主要是对自己之前写的博客的一次优化,可以结合下面两篇博客进行这篇博客的阅读:
对自己关于秒杀功能的一次访谈与实战-CSDN博客
SpringBoot中使用Sharding-JDBC实战(实战+版本兼容+Bug解决)-CSDN博客
开始正题:
一、新增亮点
1.引入Sentinel进行限流
1.1 依赖
这里我是用的SpringCloud中Sentinel的依赖,所以也需要引入SpringCloud的依赖:
在root的pom.xml中引入下面依赖
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-alibaba-dependencies -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2023.0.1.0</version>
<type>pom</type>
</dependency>
在server的pom.xml中引入Sentinel的依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactI
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>
1.2 初始化限流配置
这里需要自定义资源的名称,用于后续注解中加入,还有定义限流规则,限流数量
package com.quick.config;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 初始化限流配置
*/
@Component
public class SentinelRuleConfig implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
List<FlowRule> rules = new ArrayList<>();
// 创建抢购秒杀积分包的 Sentinel 规则
FlowRule seckillIntegralPackageRule = new FlowRule();
seckillIntegralPackageRule.setResource("seckill_integral-package");
seckillIntegralPackageRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 限流模式:按 QPS
seckillIntegralPackageRule.setCount(40); // 每秒最多允许 40 人
rules.add(seckillIntegralPackageRule);
// 加载规则到 Sentinel
FlowRuleManager.loadRules(rules);
}
}
1.3 自定义流控策略
package com.quick.handler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.quick.exception.SentinelException;
/**
* 自定义流控策略
*/
public class CustomBlockHandler {
public static void seckillIntegralPackageBlockHandlerMethod(BlockException exception) {
throw new SentinelException("当前抢购人数过多,请稍后再试...");
}
}
1.4 接口上注解使用
/**
* 秒杀积分包实现,加入订单
* @param integralPackageId 积分包id
* @return 订单id
*/
@SentinelResource(
value = "seckill_integral-package",
blockHandler = "seckillIntegralPackageBlockHandlerMethod",
blockHandlerClass = CustomBlockHandler.class
)
@PostMapping("seckillIntegralPackage/{id}")
@Operation(summary = "用户抢购秒杀积分包")
public Result<Long> seckillIntegralPackage(@PathVariable("id") Long integralPackageId) {
return integralPackageOrderService.seckillIntegralPackage(integralPackageId);
}
对秒杀进行限流,能够让秒杀系统更加稳定。
2.对积分订单分表
参考下面我这个博客,里面详细讲解分表操作和使用Jmeter进行压测:
SpringBoot中使用Sharding-JDBC实战(实战+版本兼容+Bug解决)-CSDN博客
二、完善删除功能
1.重构秒杀券表
在之前删除秒杀券是进行直接的表中数据删除,这次修改是进行假删除,增加一个 is_delete 字段,修改后的表结构如下:
2.重构管理端删除秒杀券功能
3.重构管理端查看积分包功能
4.重构管理端启用禁用积分包
5.重构管理端修改积分包功能
6.重构用户查看积分包功能
7.重构用户查看积分包库存功能
8.重构用户查看自己的积分包订单
这样子的优化是,即使管理员下架了积分包,用户还能看到自己曾经抢购过的积分包。
IntegralPackageOrderVO:
/**
* <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 = "积分包状态(0已下架,1抢购中)")
private Integer IntegralPackageStatus;
@Schema(description = "是否使用(0未使用 1已使用)")
private Integer hasUse;
@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;
}
IntegralPackageStatus:
package com.quick.constant;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "积分包状态(0已下架,1抢购中)")
public class IntegralPackageStatus {
//抢购中
public static final Integer PANIC_BUYING = 1;
//已下架
public static final Integer REMOVED = 0;
}
IntegralPackageOrderServiceImpl:
@Override
public List<IntegralPackageOrderVO> getMyOrder() {
// 获取当前用户ID
Long userId = BaseContext.getCurrentId();
// 缓存键
String cacheKey = INTEGRALPACKAGE_MY_ORDER_KEY + userId;
// 从缓存中获取数据
String cachedData = stringRedisTemplate.opsForValue().get(cacheKey);
if (cachedData != null) {
// 如果缓存中有数据,直接反序列化返回
return JSONUtil.toList(cachedData, IntegralPackageOrderVO.class);
}
// 从数据库查询用户订单
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);
// 判断积分包状态
Integer integralPackageStatus = IntegralPackageStatus.PANIC_BUYING;
if (integralPackage.getIsDelete() == 0) {
integralPackageStatus = IntegralPackageStatus.REMOVED;
}
// 完善积分订单信息
integralPackageOrderVO.setOrderId(integralPackageOrder.getId());
integralPackageOrderVO.setHasUse(integralPackageOrder.getHasUse());
integralPackageOrderVO.setCreateTime(integralPackageOrder.getCreateTime());
integralPackageOrderVO.setIntegralPackageStatus(integralPackageStatus);
integralPackageOrderVOS.add(integralPackageOrderVO);
}
// 序列化结果并存入缓存,设置过期时间为一天
String jsonData = JSONUtil.toJsonStr(integralPackageOrderVOS);
stringRedisTemplate.opsForValue().set(cacheKey, jsonData, 1,TimeUnit.DAYS);
return integralPackageOrderVOS;
}
9.重构积分订单的积分计入用户
这里的主要优化是防止缓存穿透和对计入实现加分布式锁
@Override
public String addIntegralInUser(Long orderId,Long integral) {
Long userId = BaseContext.getCurrentId();
String cacheKey = INTEGRALPACKAGE_INPUT_KEY + orderId;
// 从缓存中读取
String json = stringRedisTemplate.opsForValue().get(cacheKey);
if (json == null) {
// 查询数据库
LambdaQueryWrapper<IntegralPackageOrder> orderLambdaQueryWrapper = Wrappers.lambdaQuery(IntegralPackageOrder.class)
.eq(IntegralPackageOrder::getUserId, userId)
.eq(IntegralPackageOrder::getId, orderId);
IntegralPackageOrder integralPackageOrder = integralPackageOrderMapper.selectOne(orderLambdaQueryWrapper);
if (integralPackageOrder == null) {
// 缓存空值防止缓存穿透
stringRedisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(integralPackageOrder), 3, TimeUnit.MINUTES);
throw new IntegralPackageOrderException("暂无可用秒杀积分包订单可计入用户积分");
}
// 写入缓存
stringRedisTemplate.opsForValue().set(cacheKey, JSONUtil.toJsonStr(integralPackageOrder), 3, TimeUnit.MINUTES);
}
// 不为空,解析
IntegralPackageOrder integralPackageOrder = JSONUtil.toBean(json, IntegralPackageOrder.class);
// 校验字段是否有效
if (integralPackageOrder.getHasUse() == null || Objects.equals(integralPackageOrder.getHasUse(), 1)) {
throw new IntegralPackageOrderException("该积分包订单已使用,无法重复计入");
}
// 创建锁对象
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 {
User user = userMapper.selectById(userId);
user.setWallet(user.getWallet() + integral);
userMapper.updateById(user);
UserUpdate userUpdate = UserUpdate.builder().userId(user.getId()).build();
userUpdateService.saveOrUpdate(userUpdate);
// 更新订单状态为已使用
integralPackageOrder.setHasUse(1);
int update = integralPackageOrderMapper.updateById(integralPackageOrder);
if (update == 1) {
return "积分计入成功!";
}
} finally {
redisLock.unlock();
}
return "系统繁忙,积分计入失败";
}