秒杀订单完成
This commit is contained in:
@@ -0,0 +1,143 @@
|
|||||||
|
package com.org.flashsalesystem.config;
|
||||||
|
|
||||||
|
import com.org.flashsalesystem.service.FlashSaleService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis服务初始化器
|
||||||
|
* 在应用启动时执行Redis相关的初始化操作
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class RedisInitializer implements ApplicationRunner {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FlashSaleService flashSaleService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) throws Exception {
|
||||||
|
log.info("========================================");
|
||||||
|
log.info("开始初始化Redis服务...");
|
||||||
|
log.info("========================================");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 测试Redis连接
|
||||||
|
testRedisConnection();
|
||||||
|
|
||||||
|
// 2. 预热活跃的秒杀活动库存
|
||||||
|
preloadActiveFlashSales();
|
||||||
|
|
||||||
|
// 3. 清理过期数据(可选)
|
||||||
|
cleanupExpiredData();
|
||||||
|
|
||||||
|
log.info("========================================");
|
||||||
|
log.info("Redis服务初始化完成!");
|
||||||
|
log.info("========================================");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("========================================");
|
||||||
|
log.error("Redis服务初始化失败: {}", e.getMessage());
|
||||||
|
log.error("========================================");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Redis连接
|
||||||
|
*/
|
||||||
|
private void testRedisConnection() {
|
||||||
|
try {
|
||||||
|
log.info("正在测试Redis连接...");
|
||||||
|
|
||||||
|
// 执行ping命令测试连接
|
||||||
|
String pong = redisTemplate.getConnectionFactory()
|
||||||
|
.getConnection()
|
||||||
|
.ping();
|
||||||
|
|
||||||
|
log.info("Redis连接测试成功: {}", pong);
|
||||||
|
|
||||||
|
// 测试基本操作
|
||||||
|
redisTemplate.opsForValue().set("system:health:check", "OK");
|
||||||
|
String healthCheck = (String) redisTemplate.opsForValue().get("system:health:check");
|
||||||
|
|
||||||
|
if ("OK".equals(healthCheck)) {
|
||||||
|
log.info("Redis基本操作测试成功");
|
||||||
|
redisTemplate.delete("system:health:check");
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Redis基本操作测试失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Redis连接测试失败", e);
|
||||||
|
throw new RuntimeException("Redis连接失败,请检查Redis服务状态", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预热活跃的秒杀活动库存
|
||||||
|
*/
|
||||||
|
private void preloadActiveFlashSales() {
|
||||||
|
try {
|
||||||
|
log.info("正在预热活跃秒杀活动库存...");
|
||||||
|
|
||||||
|
// 调用FlashSaleService的预热方法
|
||||||
|
flashSaleService.preloadAllActiveFlashSales();
|
||||||
|
|
||||||
|
log.info("活跃秒杀活动库存预热完成");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("预热秒杀活动库存失败", e);
|
||||||
|
// 不抛出异常,允许应用继续启动
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期数据
|
||||||
|
*/
|
||||||
|
private void cleanupExpiredData() {
|
||||||
|
try {
|
||||||
|
log.info("正在清理过期数据...");
|
||||||
|
|
||||||
|
// 清理过期的会话数据
|
||||||
|
cleanupExpiredSessions();
|
||||||
|
|
||||||
|
// 清理过期的缓存数据
|
||||||
|
cleanupExpiredCache();
|
||||||
|
|
||||||
|
log.info("过期数据清理完成");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("清理过期数据失败", e);
|
||||||
|
// 不抛出异常,允许应用继续启动
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期会话
|
||||||
|
*/
|
||||||
|
private void cleanupExpiredSessions() {
|
||||||
|
try {
|
||||||
|
// 可以在这里添加清理过期session的逻辑
|
||||||
|
log.debug("会话数据清理完成");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("清理会话数据失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理过期缓存
|
||||||
|
*/
|
||||||
|
private void cleanupExpiredCache() {
|
||||||
|
try {
|
||||||
|
// 可以在这里添加清理过期缓存的逻辑
|
||||||
|
log.debug("缓存数据清理完成");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("清理缓存数据失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -483,6 +483,31 @@ public class FlashSaleController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修复秒杀活动库存
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/repair-stock")
|
||||||
|
@Operation(summary = "修复秒杀库存", description = "修复指定秒杀活动的Redis库存数据")
|
||||||
|
public ResponseEntity<Map<String, Object>> repairFlashSaleStock(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
flashSaleService.repairFlashSaleStock(id);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("message", "库存数据修复成功");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("修复秒杀库存失败", e);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("success", false);
|
||||||
|
response.put("message", e.getMessage());
|
||||||
|
|
||||||
|
return ResponseEntity.badRequest().body(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前用户ID
|
* 获取当前用户ID
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -111,6 +111,16 @@ public class FlashSaleService {
|
|||||||
redisService.delete(stockKey); // 先删除可能存在的异常数据
|
redisService.delete(stockKey); // 先删除可能存在的异常数据
|
||||||
redisService.setString(stockKey, flashSale.getFlashStock().toString()); // 确保存储为字符串数字
|
redisService.setString(stockKey, flashSale.getFlashStock().toString()); // 确保存储为字符串数字
|
||||||
|
|
||||||
|
// 验证库存设置是否成功
|
||||||
|
String verifyStock = redisService.getString(stockKey);
|
||||||
|
log.info("秒杀活动库存初始化验证: flashSaleId={}, stockKey={}, setStock={}, actualStock={}",
|
||||||
|
flashSale.getId(), stockKey, flashSale.getFlashStock(), verifyStock);
|
||||||
|
|
||||||
|
if (!flashSale.getFlashStock().toString().equals(verifyStock)) {
|
||||||
|
log.error("库存初始化失败: flashSaleId={}, expected={}, actual={}",
|
||||||
|
flashSale.getId(), flashSale.getFlashStock(), verifyStock);
|
||||||
|
}
|
||||||
|
|
||||||
log.info("秒杀活动创建成功: ID={}", flashSale.getId());
|
log.info("秒杀活动创建成功: ID={}", flashSale.getId());
|
||||||
|
|
||||||
return buildFlashSaleDTO(flashSale, product);
|
return buildFlashSaleDTO(flashSale, product);
|
||||||
@@ -178,18 +188,70 @@ public class FlashSaleService {
|
|||||||
currentStock = redisService.getString(stockKey);
|
currentStock = redisService.getString(stockKey);
|
||||||
log.info("修复后库存: flashSaleId={}, stockKey={}, currentStock={}",
|
log.info("修复后库存: flashSaleId={}, stockKey={}, currentStock={}",
|
||||||
flashSale.getId(), stockKey, currentStock);
|
flashSale.getId(), stockKey, currentStock);
|
||||||
|
|
||||||
|
// 如果修复后仍然为空,直接设置库存
|
||||||
|
if (currentStock == null || currentStock.trim().isEmpty()) {
|
||||||
|
log.error("库存修复失败,直接重新设置库存: flashSaleId={}, stock={}",
|
||||||
|
flashSale.getId(), flashSale.getFlashStock());
|
||||||
|
redisService.setString(stockKey, flashSale.getFlashStock().toString());
|
||||||
|
currentStock = redisService.getString(stockKey);
|
||||||
|
log.info("强制设置库存后: flashSaleId={}, stockKey={}, currentStock={}",
|
||||||
|
flashSale.getId(), stockKey, currentStock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 最终验证库存数据有效性
|
||||||
|
currentStock = redisService.getString(stockKey);
|
||||||
|
if (currentStock == null || currentStock.trim().isEmpty()) {
|
||||||
|
log.error("库存数据最终验证失败: flashSaleId={}, stockKey={}", flashSale.getId(), stockKey);
|
||||||
|
return createFailResult("秒杀活动库存初始化失败,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
// 使用Lua脚本原子性扣减库存
|
// 使用Lua脚本原子性扣减库存
|
||||||
log.info("准备执行秒杀脚本: stockKey={}, quantity={}, userId={}",
|
log.info("准备执行秒杀脚本: stockKey={}, quantity={}, userId={}, currentStock={}",
|
||||||
stockKey, participateDTO.getQuantity(), userId);
|
stockKey, participateDTO.getQuantity(), userId, currentStock);
|
||||||
Long remainingStock = redisService.executeFlashSaleScript(stockKey, participateDTO.getQuantity());
|
Long remainingStock = redisService.executeFlashSaleScript(stockKey, participateDTO.getQuantity());
|
||||||
log.info("秒杀脚本执行完成: stockKey={}, remainingStock={}", stockKey, remainingStock);
|
log.info("秒杀脚本执行完成: stockKey={}, remainingStock={}", stockKey, remainingStock);
|
||||||
|
|
||||||
if (remainingStock < 0) {
|
if (remainingStock < 0) {
|
||||||
if (remainingStock == -1) {
|
if (remainingStock == -1) {
|
||||||
log.warn("秒杀库存key不存在或数据异常: flashSaleId={}, stockKey={}", flashSale.getId(), stockKey);
|
log.warn("秒杀库存key不存在或数据异常: flashSaleId={}, stockKey={}", flashSale.getId(), stockKey);
|
||||||
return createFailResult("秒杀活动库存信息异常,请刷新页面重试");
|
|
||||||
|
// 自动修复数据并重试一次
|
||||||
|
log.info("开始自动修复库存数据: flashSaleId={}", flashSale.getId());
|
||||||
|
try {
|
||||||
|
repairFlashSaleStock(flashSale.getId());
|
||||||
|
|
||||||
|
// 修复后重新验证库存
|
||||||
|
String repairedStock = redisService.getString(stockKey);
|
||||||
|
if (repairedStock != null && !repairedStock.trim().isEmpty()) {
|
||||||
|
log.info("库存修复成功,重新执行秒杀脚本: flashSaleId={}, stock={}",
|
||||||
|
flashSale.getId(), repairedStock);
|
||||||
|
|
||||||
|
// 重新执行秒杀脚本
|
||||||
|
Long retryResult = redisService.executeFlashSaleScript(stockKey,
|
||||||
|
participateDTO.getQuantity());
|
||||||
|
log.info("修复后重试结果: flashSaleId={}, result={}", flashSale.getId(), retryResult);
|
||||||
|
|
||||||
|
if (retryResult >= 0) {
|
||||||
|
// 修复后重试成功,继续正常流程
|
||||||
|
remainingStock = retryResult;
|
||||||
|
} else {
|
||||||
|
// 重试仍然失败
|
||||||
|
if (retryResult == -2) {
|
||||||
|
return createFailResult("商品已售罄");
|
||||||
|
} else {
|
||||||
|
return createFailResult("系统繁忙,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("库存修复失败: flashSaleId={}", flashSale.getId());
|
||||||
|
return createFailResult("系统繁忙,请稍后重试");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("自动修复库存失败: flashSaleId={}", flashSale.getId(), e);
|
||||||
|
return createFailResult("系统繁忙,请稍后重试");
|
||||||
|
}
|
||||||
} else if (remainingStock == -2) {
|
} else if (remainingStock == -2) {
|
||||||
log.info("秒杀库存不足: flashSaleId={}, 剩余库存不足", flashSale.getId());
|
log.info("秒杀库存不足: flashSaleId={}, 剩余库存不足", flashSale.getId());
|
||||||
return createFailResult("商品已售罄");
|
return createFailResult("商品已售罄");
|
||||||
@@ -378,7 +440,17 @@ public class FlashSaleService {
|
|||||||
redisService.delete(stockKey); // 先删除可能存在的异常数据
|
redisService.delete(stockKey); // 先删除可能存在的异常数据
|
||||||
redisService.setString(stockKey, flashSale.getFlashStock().toString()); // 确保存储为字符串数字
|
redisService.setString(stockKey, flashSale.getFlashStock().toString()); // 确保存储为字符串数字
|
||||||
|
|
||||||
log.info("秒杀活动预热完成: {}", flashSaleId);
|
// 验证预热是否成功
|
||||||
|
String verifyStock = redisService.getString(stockKey);
|
||||||
|
log.info("秒杀活动预热验证: flashSaleId={}, stockKey={}, setStock={}, actualStock={}",
|
||||||
|
flashSaleId, stockKey, flashSale.getFlashStock(), verifyStock);
|
||||||
|
|
||||||
|
if (!flashSale.getFlashStock().toString().equals(verifyStock)) {
|
||||||
|
log.error("秒杀活动预热失败: flashSaleId={}, expected={}, actual={}",
|
||||||
|
flashSaleId, flashSale.getFlashStock(), verifyStock);
|
||||||
|
} else {
|
||||||
|
log.info("秒杀活动预热完成: flashSaleId={}, stock={}", flashSaleId, verifyStock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -445,23 +517,108 @@ public class FlashSaleService {
|
|||||||
public void preloadAllActiveFlashSales() {
|
public void preloadAllActiveFlashSales() {
|
||||||
log.info("开始预热所有活跃秒杀活动库存");
|
log.info("开始预热所有活跃秒杀活动库存");
|
||||||
|
|
||||||
List<FlashSale> activeFlashSales = flashSaleRepository.findAll();
|
try {
|
||||||
|
// 获取所有秒杀活动(包括未开始、进行中的)
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
List<FlashSale> activeFlashSales = flashSaleRepository.findAll();
|
||||||
|
|
||||||
for (FlashSale flashSale : activeFlashSales) {
|
if (activeFlashSales.isEmpty()) {
|
||||||
try {
|
log.info("没有找到任何秒杀活动,跳过预热");
|
||||||
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
return;
|
||||||
|
|
||||||
// 使用set方法先存储,让系统能识别
|
|
||||||
redisService.set(stockKey, flashSale.getFlashStock());
|
|
||||||
|
|
||||||
log.info("预热秒杀活动库存: flashSaleId={}, stock={}",
|
|
||||||
flashSale.getId(), flashSale.getFlashStock());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("预热秒杀活动库存失败: flashSaleId={}", flashSale.getId(), e);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
log.info("所有活跃秒杀活动库存预热完成");
|
int successCount = 0;
|
||||||
|
int failCount = 0;
|
||||||
|
|
||||||
|
for (FlashSale flashSale : activeFlashSales) {
|
||||||
|
try {
|
||||||
|
String stockKey = FLASH_SALE_STOCK_PREFIX + flashSale.getId();
|
||||||
|
|
||||||
|
// 检查活动状态,只预热有效的活动
|
||||||
|
if (flashSale.getStatus() == 3) { // 已结束的活动跳过
|
||||||
|
log.debug("跳过已结束的秒杀活动: flashSaleId={}", flashSale.getId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证库存数据有效性
|
||||||
|
if (flashSale.getFlashStock() == null || flashSale.getFlashStock() < 0) {
|
||||||
|
log.warn("秒杀活动库存数据无效,跳过: flashSaleId={}, stock={}",
|
||||||
|
flashSale.getId(), flashSale.getFlashStock());
|
||||||
|
failCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前Redis中的库存
|
||||||
|
String currentStock = redisService.getString(stockKey);
|
||||||
|
log.debug("预热前检查库存: flashSaleId={}, stockKey={}, currentStock={}",
|
||||||
|
flashSale.getId(), stockKey, currentStock);
|
||||||
|
|
||||||
|
// 如果库存已存在且有效,不重复设置
|
||||||
|
if (currentStock != null && !currentStock.trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
Integer existingStock = Integer.parseInt(currentStock.trim());
|
||||||
|
if (existingStock >= 0 && existingStock <= flashSale.getFlashStock()) {
|
||||||
|
log.debug("库存已存在且有效,跳过设置: flashSaleId={}, existingStock={}",
|
||||||
|
flashSale.getId(), existingStock);
|
||||||
|
successCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
log.warn("现有库存格式异常,将重新设置: flashSaleId={}, currentStock={}",
|
||||||
|
flashSale.getId(), currentStock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除可能存在的异常数据
|
||||||
|
redisService.delete(stockKey);
|
||||||
|
|
||||||
|
// 设置库存数据 - 确保存储为字符串数字
|
||||||
|
redisService.setString(stockKey, flashSale.getFlashStock().toString());
|
||||||
|
|
||||||
|
// 验证设置是否成功
|
||||||
|
String verifyStock = redisService.getString(stockKey);
|
||||||
|
if (flashSale.getFlashStock().toString().equals(verifyStock)) {
|
||||||
|
log.info("预热秒杀活动库存成功: flashSaleId={}, stock={}, status={}",
|
||||||
|
flashSale.getId(), flashSale.getFlashStock(), getStatusText(flashSale.getStatus()));
|
||||||
|
successCount++;
|
||||||
|
} else {
|
||||||
|
log.error("预热秒杀活动库存验证失败: flashSaleId={}, expected={}, actual={}",
|
||||||
|
flashSale.getId(), flashSale.getFlashStock(), verifyStock);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("预热秒杀活动库存失败: flashSaleId={}", flashSale.getId(), e);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("所有秒杀活动库存预热完成: 总数={}, 成功={}, 失败={}",
|
||||||
|
activeFlashSales.size(), successCount, failCount);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("预热所有秒杀活动库存时发生异常", e);
|
||||||
|
throw new RuntimeException("预热秒杀活动库存失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态文本描述
|
||||||
|
*/
|
||||||
|
private String getStatusText(Integer status) {
|
||||||
|
if (status == null) return "未知";
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
return "未开始";
|
||||||
|
case 2:
|
||||||
|
return "进行中";
|
||||||
|
case 3:
|
||||||
|
return "已结束";
|
||||||
|
case 4:
|
||||||
|
return "已暂停";
|
||||||
|
default:
|
||||||
|
return "未知";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user