基于SpringBoot+Redis+Lua 實(shí)現(xiàn)高并發(fā)秒殺系統(tǒng)
基于 SpringBoot+Redis+Lua 構(gòu)建高并發(fā)秒殺系統(tǒng)的設(shè)計(jì)思路,本文將聚焦核心代碼實(shí)現(xiàn),從環(huán)境配置、庫(kù)存預(yù)熱、Lua 腳本、秒殺接口到異步下單、限流防護(hù),完整呈現(xiàn)可直接落地的代碼方案,幫你快速搭建穩(wěn)定高效的秒殺系統(tǒng)。
一、環(huán)境準(zhǔn)備與依賴配置
1. 項(xiàng)目依賴(pom.xml)
首先在 SpringBoot 項(xiàng)目中引入核心依賴,包括 Redis、消息隊(duì)列(以 RabbitMQ 為例)、SpringBoot 核心組件等:
<!-- SpringBoot核心依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 消息隊(duì)列(RabbitMQ) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- Lua腳本支持 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.7.0</version>
</dependency>
<!-- 工具類 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.11</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 核心配置(application.yml)
配置 Redis、RabbitMQ 連接信息,以及秒殺系統(tǒng)核心參數(shù):
spring:
# Redis配置
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 0
lettuce:
pool:
max-active: 16 # 最大連接數(shù)
max-idle: 8 # 最大空閑連接
min-idle: 4 # 最小空閑連接
# RabbitMQ配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual # 手動(dòng)ACK確認(rèn)
concurrency: 5 # 消費(fèi)者并發(fā)數(shù)
# 秒殺系統(tǒng)配置
seckill:
redis:
stock-key-prefix: "seckill:goods:" # 商品庫(kù)存Key前綴
user-key-prefix: "seckill:goods:users:" # 已秒殺用戶Key前綴
rabbitmq:
queue-name: "seckill_order_queue" # 訂單消息隊(duì)列名
exchange-name: "seckill_order_exchange" # 訂單交換機(jī)名
routing-key: "seckill.order" # 路由鍵
rate-limit:
qps: 1000 # 全局限流QPS
二、核心工具類與常量定義
1. 秒殺常量類(SeckillConstant)
統(tǒng)一管理 Redis 鍵前綴、消息隊(duì)列參數(shù)等常量,避免硬編碼:
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 秒殺系統(tǒng)常量類
*/
@Getter
@AllArgsConstructor
public class SeckillConstant {
// Redis鍵前綴
public static final String STOCK_KEY_PREFIX = "seckill:goods:";
public static final String USER_KEY_PREFIX = "seckill:goods:users:";
public static final String REQUEST_ID_KEY_PREFIX = "seckill:request:";
// 秒殺結(jié)果狀態(tài)碼
public static final int SECKILL_SUCCESS = 1; // 秒殺成功
public static final int SECKILL_FAIL_STOCK = 0; // 庫(kù)存不足
public static final int SECKILL_FAIL_REPEAT = 2; // 重復(fù)秒殺
// 消息隊(duì)列參數(shù)
public static final String ORDER_QUEUE_NAME = "seckill_order_queue";
public static final String ORDER_EXCHANGE_NAME = "seckill_order_exchange";
public static final String ORDER_ROUTING_KEY = "seckill.order";
}
2. Redis 工具類(RedisTemplateConfig)
配置 RedisTemplate,支持 String、Hash 等數(shù)據(jù)結(jié)構(gòu)操作,以及 Lua 腳本執(zhí)行:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisTemplateConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// String序列化器
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// JSON序列化器
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
// Key使用String序列化
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// Value使用JSON序列化
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
三、庫(kù)存預(yù)熱實(shí)現(xiàn)(活動(dòng)前數(shù)據(jù)加載)
秒殺活動(dòng)開(kāi)始前,將商品庫(kù)存、活動(dòng)狀態(tài)從數(shù)據(jù)庫(kù)加載到 Redis,避免活動(dòng)中頻繁訪問(wèn)數(shù)據(jù)庫(kù):
1. 商品實(shí)體類(GoodsDTO)
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 秒殺商品DTO
*/
@Data
public class GoodsDTO {
private Long goodsId; // 商品ID
private String goodsName; // 商品名稱
private BigDecimal seckillPrice; // 秒殺價(jià)格
private Integer stock; // 秒殺庫(kù)存
private Date startTime; // 活動(dòng)開(kāi)始時(shí)間
private Date endTime; // 活動(dòng)結(jié)束時(shí)間
private Integer status; // 活動(dòng)狀態(tài):0-未開(kāi)始,1-進(jìn)行中,2-已結(jié)束
}
2. 庫(kù)存預(yù)熱服務(wù)(StockWarmUpService)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 庫(kù)存預(yù)熱服務(wù)
*/
@Service
public class StockWarmUpService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 預(yù)熱單個(gè)商品庫(kù)存到Redis
*/
public void warmUpGoodsStock(GoodsDTO goodsDTO) {
if (goodsDTO == null || goodsDTO.getStock() <= 0) {
throw new IllegalArgumentException("商品信息或庫(kù)存無(wú)效");
}
// 1. 存儲(chǔ)商品庫(kù)存和狀態(tài)(Hash結(jié)構(gòu))
String stockKey = SeckillConstant.STOCK_KEY_PREFIX + goodsDTO.getGoodsId();
Map<String, Object> goodsMap = new HashMap<>();
goodsMap.put("stock", goodsDTO.getStock());
goodsMap.put("status", goodsDTO.getStatus());
goodsMap.put("startTime", goodsDTO.getStartTime().getTime());
goodsMap.put("endTime", goodsDTO.getEndTime().getTime());
redisTemplate.opsForHash().putAll(stockKey, goodsMap);
// 2. 初始化已秒殺用戶集合(Set結(jié)構(gòu),用于去重)
String userKey = SeckillConstant.USER_KEY_PREFIX + goodsDTO.getGoodsId();
redisTemplate.opsForSet().add(userKey, new Object[]{}); // 初始化空集合
}
/**
* 批量預(yù)熱商品庫(kù)存(適用于多商品秒殺活動(dòng))
*/
public void batchWarmUpGoodsStock(java.util.List<GoodsDTO> goodsDTOList) {
for (GoodsDTO goodsDTO : goodsDTOList) {
warmUpGoodsStock(goodsDTO);
}
}
}
3. 定時(shí)預(yù)熱觸發(fā)(可選)
通過(guò)定時(shí)任務(wù)在活動(dòng)開(kāi)始前 10 分鐘自動(dòng)預(yù)熱庫(kù)存:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class SeckillScheduledTask {
@Autowired
private StockWarmUpService stockWarmUpService;
@Autowired
private GoodsMapper goodsMapper; // 自定義Mapper,查詢待秒殺商品
/**
* 每天00:00預(yù)熱當(dāng)天秒殺商品庫(kù)存(可根據(jù)實(shí)際活動(dòng)時(shí)間調(diào)整)
*/
@Scheduled(cron = "0 0 0 * * ?")
public void scheduledWarmUpStock() {
List<GoodsDTO> seckillGoods = goodsMapper.selectTodaySeckillGoods();
stockWarmUpService.batchWarmUpGoodsStock(seckillGoods);
}
}
四、核心:Lua 腳本實(shí)現(xiàn)原子扣減庫(kù)存
通過(guò) Lua 腳本將 “庫(kù)存檢查、重復(fù)秒殺攔截、庫(kù)存扣減” 封裝為原子操作,從根本上解決并發(fā)沖突:
1. Lua 腳本文件(seckill_stock.lua)
在resources/lua目錄下創(chuàng)建 Lua 腳本:
-- 傳入?yún)?shù):KEYS[1] = 商品ID,ARGV[1] = 用戶ID
local goodsId = KEYS[1]
local userId = ARGV[1]
-- 定義Redis鍵
local stockKey = "seckill:goods:" .. goodsId
local userKey = "seckill:goods:users:" .. goodsId
-- 1. 檢查商品活動(dòng)狀態(tài)和庫(kù)存
local goodsStatus = redis.call("HGET", stockKey, "status")
if goodsStatus ~= "1" then
return 3 -- 活動(dòng)未開(kāi)始或已結(jié)束
end
local stock = redis.call("HGET", stockKey, "stock")
if not stock or tonumber(stock) <= 0 then
return 0 -- 庫(kù)存不足
end
-- 2. 檢查用戶是否已秒殺(避免重復(fù)下單)
if redis.call("SISMEMBER", userKey, userId) == 1 then
return 2 -- 重復(fù)秒殺
end
-- 3. 原子扣減庫(kù)存并記錄用戶
redis.call("HINCRBY", stockKey, "stock", -1)
redis.call("SADD", userKey, userId)
return 1 -- 秒殺成功
2. Lua 腳本加載與執(zhí)行服務(wù)(LuaScriptService)
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.StaticScriptSource;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
/**
* Lua腳本執(zhí)行服務(wù)
*/
@Service
public class LuaScriptService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private DefaultRedisScript<Integer> seckillStockScript;
/**
* 初始化加載Lua腳本
*/
@PostConstruct
public void initLuaScript() {
seckillStockScript = new DefaultRedisScript<>();
// 加載腳本文件
seckillStockScript.setScriptSource(new StaticScriptSource(
new ClassPathResource("lua/seckill_stock.lua").getContentAsString()
));
// 設(shè)置返回值類型
seckillStockScript.setResultType(Integer.class);
}
/**
* 執(zhí)行秒殺庫(kù)存扣減腳本
*/
public Integer executeSeckillScript(Long goodsId, Long userId) {
List<String> keys = Collections.singletonList(goodsId.toString());
// 執(zhí)行腳本:keys=商品ID,args=用戶ID
return redisTemplate.execute(
seckillStockScript,
keys,
userId.toString()
);
}
}
五、秒殺接口實(shí)現(xiàn)(含限流、防重復(fù)提交)
1. 限流注解與 AOP 實(shí)現(xiàn)(RateLimitAspect)
基于 Redis 實(shí)現(xiàn)分布式限流,攔截超 QPS 請(qǐng)求:
import cn.hutool.core.util.StrUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Collections;
/**
* 限流AOP切面
*/
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Value("${seckill.rate-limit.qps}")
private int qps;
private DefaultRedisScript<Long> rateLimitScript;
@PostConstruct
public void initRateLimitScript() {
// 令牌桶限流Lua腳本
String script = "local key = KEYS[1]\n" +
"local now = tonumber(ARGV[1])\n" +
"local qps = tonumber(ARGV[2])\n" +
"local window = 1\n" + // 窗口時(shí)間1秒
"local maxToken = qps\n" +
"local lastTime = redis.call('HGET', key, 'lastTime')\n" +
"local tokenCount = redis.call('HGET', key, 'tokenCount')\n" +
"if not lastTime or not tokenCount then\n" +
" redis.call('HSET', key, 'lastTime', now)\n" +
" redis.call('HSET', key, 'tokenCount', maxToken - 1)\n" +
" redis.call('EXPIRE', key, window)\n" +
" return 1\n" +
"end\n" +
"local interval = now - tonumber(lastTime)\n" +
"local addToken = math.floor(interval / 1000 * qps)\n" +
"tokenCount = math.min(addToken + tonumber(tokenCount), maxToken)\n" +
"if tokenCount <= 0 then\n" +
" return 0\n" +
"end\n" +
"redis.call('HSET', key, 'lastTime', now)\n" +
"redis.call('HSET', key, 'tokenCount', tokenCount - 1)\n" +
"return 1";
rateLimitScript = new DefaultRedisScript<>(script, Long.class);
}
// 定義限流注解切入點(diǎn)
@Pointcut("@annotation(com.seckill.annotation.RateLimit)")
public void rateLimitPointcut() {}
@Around("rateLimitPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String key = "seckill:rate:limit:global"; // 全局限流Key(可擴(kuò)展為IP/用戶維度)
Long now = System.currentTimeMillis();
Long result = redisTemplate.execute(
rateLimitScript,
Collections.singletonList(key),
now.toString(),
String.valueOf(qps)
);
if (result == 1) {
return joinPoint.proceed(); // 獲得令牌,執(zhí)行方法
} else {
throw new RuntimeException("請(qǐng)求過(guò)于頻繁,請(qǐng)稍后再試");
}
}
}
2. 防重復(fù)提交注解(RepeatSubmit)
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
// 防重復(fù)提交過(guò)期時(shí)間(默認(rèn)5秒)
int expireSeconds() default 5;
}
3. 秒殺接口(SeckillController)
import cn.hutool.core.util.IdUtil;
import com.seckill.annotation.RateLimit;
import com.seckill.annotation.RepeatSubmit;
import com.seckill.dto.SeckillRequestDTO;
import com.seckill.dto.SeckillResponseDTO;
import com.seckill.service.LuaScriptService;
import com.seckill.service.OrderMessageService;
import com.seckill.service.StockWarmUpService;
import com.seckill.util.SeckillConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.Objects;
/**
* 秒殺接口
*/
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private LuaScriptService luaScriptService;
@Autowired
private OrderMessageService orderMessageService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 秒殺核心接口(含限流、防重復(fù)提交)
*/
@PostMapping("/doSeckill")
@RateLimit // 限流注解
@RepeatSubmit(expireSeconds = 5) // 防重復(fù)提交(5秒內(nèi)同一請(qǐng)求ID不可重復(fù)提交)
public SeckillResponseDTO doSeckill(@Valid @RequestBody SeckillRequestDTO requestDTO) {
Long goodsId = requestDTO.getGoodsId();
Long userId = requestDTO.getUserId();
String requestId = requestDTO.getRequestId(); // 前端生成的唯一請(qǐng)求ID
// 1. 防重復(fù)提交校驗(yàn)(基于Redis緩存請(qǐng)求ID)
String requestKey = SeckillConstant.REQUEST_ID_KEY_PREFIX + requestId;
Boolean hasRequest = redisTemplate.hasKey(requestKey);
if (Boolean.TRUE.equals(hasRequest)) {
return SeckillResponseDTO.fail("請(qǐng)勿重復(fù)提交請(qǐng)求");
}
redisTemplate.opsForValue().set(requestKey, "1", 5); // 緩存5秒
try {
// 2. 執(zhí)行Lua腳本扣減庫(kù)存
Integer seckillResult = luaScriptService.executeSeckillScript(goodsId, userId);
// 3. 處理秒殺結(jié)果
if (SeckillConstant.SECKILL_SUCCESS == seckillResult) {
// 秒殺成功,發(fā)送訂單消息到消息隊(duì)列
orderMessageService.sendOrderMessage(goodsId, userId);
return SeckillResponseDTO.success("秒殺成功,正在創(chuàng)建訂單...");
} else if (SeckillConstant.SECKILL_FAIL_STOCK == seckillResult) {
return SeckillResponseDTO.fail("庫(kù)存不足,秒殺失敗");
} else if (SeckillConstant.SECKILL_FAIL_REPEAT == seckillResult) {
return SeckillResponseDTO.fail("您已參與過(guò)該商品秒殺,請(qǐng)勿重復(fù)搶購(gòu)");
} else {
return SeckillResponseDTO.fail("活動(dòng)未開(kāi)始或已結(jié)束");
}
} finally {
// 4. 移除防重復(fù)提交緩存(可選,根據(jù)業(yè)務(wù)需求調(diào)整)
redisTemplate.delete(requestKey);
}
}
}
4. 秒殺請(qǐng)求 / 響應(yīng) DTO
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 秒殺請(qǐng)求DTO
*/
@Data
public class SeckillRequestDTO {
@NotNull(message = "商品ID不能為空")
private Long goodsId;
@NotNull(message = "用戶ID不能為空")
private Long userId;
@NotNull(message = "請(qǐng)求ID不能為空")
private String requestId; // 前端生成的唯一請(qǐng)求ID,用于防重復(fù)提交
}
/**
* 秒殺響應(yīng)DTO
*/
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillResponseDTO {
private int code; // 響應(yīng)碼:0-失敗,1-成功
private String message; // 響應(yīng)消息
public static SeckillResponseDTO success(String message) {
return new SeckillResponseDTO(1, message);
}
public static SeckillResponseDTO fail(String message) {
return new SeckillResponseDTO(0, message);
}
}
六、消息隊(duì)列異步下單實(shí)現(xiàn)
1. 訂單消息實(shí)體(OrderMessage)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 訂單消息實(shí)體
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderMessage {
private Long orderId; // 訂單ID(可在消息發(fā)送時(shí)生成)
private Long goodsId; // 商品ID
private Long userId; // 用戶ID
private BigDecimal seckillPrice; // 秒殺價(jià)格
private Long createTime; // 創(chuàng)建時(shí)間戳
}
2. 消息隊(duì)列配置(RabbitMQConfig)
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* RabbitMQ配置
*/
@Configuration
public class RabbitMQConfig {
/**
* 聲明訂單隊(duì)列
*/
@Bean
public Queue orderQueue() {
// 隊(duì)列持久化
return new Queue(SeckillConstant.ORDER_QUEUE_NAME, true);
}
/**
* 聲明訂單交換機(jī)
*/
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(SeckillConstant.ORDER_EXCHANGE_NAME, true, false);
}
/**
* 綁定隊(duì)列和交換機(jī)
*/
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue())
.to(orderExchange())
.with(SeckillConstant.ORDER_ROUTING_KEY);
}
}
3. 訂單消息發(fā)送服務(wù)(OrderMessageService)
import cn.hutool.core.util.IdUtil;
import com.seckill.dto.OrderMessage;
import com.seckill.util.SeckillConstant;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
* 訂單消息發(fā)送服務(wù)
*/
@Service
public class OrderMessageService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 發(fā)送秒殺訂單消息
*/
public void sendOrderMessage(Long goodsId, Long userId) {
// 生成訂單ID(雪花算法或UUID)
Long orderId = IdUtil.getSnowflake().nextId();
// 構(gòu)建訂單消息
OrderMessage orderMessage = new OrderMessage();
orderMessage.setOrderId(orderId);
orderMessage.setGoodsId(goodsId);
orderMessage.setUserId(userId);
orderMessage.setSeckillPrice(new BigDecimal("99.00")); // 示例秒殺價(jià)格
orderMessage.setCreateTime(System.currentTimeMillis());
// 發(fā)送消息到RabbitMQ
rabbitTemplate.convertAndSend(
SeckillConstant.ORDER_EXCHANGE_NAME,
SeckillConstant.ORDER_ROUTING_KEY,
orderMessage
);
}
}
4. 訂單消息消費(fèi)服務(wù)(OrderConsumerService)
import com.rabbitmq.client.Channel;
import com.seckill.dto.OrderMessage;
import com.seckill.mapper.OrderMapper;
import com.seckill.pojo.Order;
import com.seckill.util.SeckillConstant;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* 訂單消息消費(fèi)服務(wù)(異步創(chuàng)建訂單)
*/
@Service
public class OrderConsumerService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@RabbitListener(queues = SeckillConstant.ORDER_QUEUE_NAME)
public void consumeOrderMessage(OrderMessage orderMessage, Channel channel, Message message) throws IOException {
try {
// 1. 校驗(yàn)消息合法性
if (orderMessage == null || orderMessage.getGoodsId() == null || orderMessage.getUserId() == null) {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
return;
}
// 2. 異步創(chuàng)建訂單
Order order = new Order();
order.setOrderId(orderMessage.getOrderId());
order.setGoodsId(orderMessage.getGoodsId());
order.setUserId(orderMessage.getUserId());
order.setSeckillPrice(orderMessage.getSeckillPrice());
order.setCreateTime(orderMessage.getCreateTime());
order.setStatus(0); // 0-待支付
orderMapper.insert(order);
// 3. 手動(dòng)ACK確認(rèn)消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 4. 處理異常:消息重投或記錄日志(根據(jù)業(yè)務(wù)需求調(diào)整)
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
// 回滾Redis庫(kù)存(補(bǔ)償機(jī)制)
rollbackStock(orderMessage.getGoodsId());
}
}
/**
* 訂單創(chuàng)建失敗,回滾Redis庫(kù)存
*/
private void rollbackStock(Long goodsId) {
String stockKey = SeckillConstant.STOCK_KEY_PREFIX + goodsId;
redisTemplate.opsForHash().increment(stockKey, "stock", 1);
}
}
七、防重復(fù)提交 AOP 實(shí)現(xiàn)(RepeatSubmitAspect)
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import com.seckill.annotation.RepeatSubmit;
import com.seckill.dto.SeckillRequestDTO;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 防重復(fù)提交AOP切面
*/
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Pointcut("@annotation(repeatSubmit)")
public void repeatSubmitPointcut(RepeatSubmit repeatSubmit) {}
@Around("repeatSubmitPointcut(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
// 獲取請(qǐng)求參數(shù)中的SeckillRequestDTO
Object[] args = joinPoint.getArgs();
SeckillRequestDTO requestDTO = null;
for (Object arg : args) {
if (arg instanceof SeckillRequestDTO) {
requestDTO = (SeckillRequestDTO) arg;
break;
}
}
if (requestDTO == null || Objects.isNull(requestDTO.getRequestId())) {
throw new IllegalArgumentException("請(qǐng)求ID不能為空");
}
// 構(gòu)建請(qǐng)求Key
String requestKey = "seckill:request:" + requestDTO.getRequestId();
Boolean hasKey = redisTemplate.hasKey(requestKey);
if (Boolean.TRUE.equals(hasKey)) {
throw new RuntimeException("請(qǐng)勿重復(fù)提交請(qǐng)求");
}
// 緩存請(qǐng)求ID
redisTemplate.opsForValue().set(requestKey, "1", repeatSubmit.expireSeconds(), TimeUnit.SECONDS);
try {
return joinPoint.proceed();
} finally {
// 可根據(jù)業(yè)務(wù)需求決定是否移除緩存
// redisTemplate.delete(requestKey);
}
}
}
八、核心代碼總結(jié)與部署建議
1. 代碼核心亮點(diǎn)
- 原子性保障:通過(guò) Lua 腳本封裝庫(kù)存校驗(yàn)、扣減和用戶去重,避免超賣與重復(fù)秒殺;
- 流量分層攔截:結(jié)合 Redis 令牌桶限流、防重復(fù)提交 AOP,從源頭減少無(wú)效請(qǐng)求;
- 異步解耦:RabbitMQ 異步處理訂單創(chuàng)建,提升接口響應(yīng)速度,降低數(shù)據(jù)庫(kù)壓力;
- 數(shù)據(jù)一致性:消息消費(fèi)失敗時(shí)回滾 Redis 庫(kù)存,確保緩存與數(shù)據(jù)庫(kù)數(shù)據(jù)同步;
- 可擴(kuò)展性:支持庫(kù)存分片、預(yù)售模式等復(fù)雜場(chǎng)景擴(kuò)展,適配不同業(yè)務(wù)需求。
2. 部署與測(cè)試建議
- Redis 部署:采用主從復(fù)制 + 哨兵模式,確保高可用;超大規(guī)模秒殺建議使用 Redis Cluster 分片存儲(chǔ)庫(kù)存;
- 消息隊(duì)列:開(kāi)啟 RabbitMQ 持久化與鏡像隊(duì)列,避免消息丟失;根據(jù)流量調(diào)整消費(fèi)者并發(fā)數(shù);
- 壓力測(cè)試:使用 JMeter 模擬 10 萬(wàn) + 并發(fā)請(qǐng)求,重點(diǎn)測(cè)試庫(kù)存準(zhǔn)確性、接口響應(yīng)時(shí)間和系統(tǒng)穩(wěn)定性;
- 監(jiān)控告警:通過(guò) Prometheus+Grafana 監(jiān)控 Redis 庫(kù)存、消息隊(duì)列堆積量、接口 QPS 等核心指標(biāo),設(shè)置閾值告警。
本文完整呈現(xiàn)了 SpringBoot+Redis+Lua 秒殺系統(tǒng)的核心代碼,覆蓋從庫(kù)存預(yù)熱到異步下單的全流程,代碼可直接復(fù)制到項(xiàng)目中使用,只需根據(jù)實(shí)際業(yè)務(wù)調(diào)整參數(shù)與擴(kuò)展功能。
到此這篇關(guān)于基于SpringBoot+Redis+Lua 實(shí)現(xiàn)高并發(fā)秒殺系統(tǒng)的文章就介紹到這了,更多相關(guān)SpringBoot Redis Lua高并發(fā)秒殺內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot+Redis隊(duì)列實(shí)現(xiàn)Java版秒殺的示例代碼
- Springboot+redis+Vue實(shí)現(xiàn)秒殺的項(xiàng)目實(shí)踐
- springboot?+rabbitmq+redis實(shí)現(xiàn)秒殺示例
- SpringBoot+RabbitMQ+Redis實(shí)現(xiàn)商品秒殺的示例代碼
- 基于Redis結(jié)合SpringBoot的秒殺案例詳解
- SpringBoot之使用Redis實(shí)現(xiàn)分布式鎖(秒殺系統(tǒng))
- SpringBoot使用Redisson實(shí)現(xiàn)分布式鎖(秒殺系統(tǒng))
- springboot集成redis實(shí)現(xiàn)簡(jiǎn)單秒殺系統(tǒng)
相關(guān)文章
mybatis如何獲取剛剛新插入數(shù)據(jù)的主鍵值id
這篇文章主要介紹了mybatis如何獲取剛剛新插入數(shù)據(jù)的主鍵值id問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
SpringMVC配置攔截器實(shí)現(xiàn)登錄控制的方法
這篇文章主要介紹了SpringMVC配置攔截器實(shí)現(xiàn)登錄控制的方法,SpringMVC讀取Cookie判斷用戶是否登錄,對(duì)每一個(gè)action都要進(jìn)行判斷,有興趣的可以了解一下。2017-03-03
Java實(shí)現(xiàn)查找Excel數(shù)據(jù)并高亮顯示
在日常的開(kāi)發(fā)工作中,我們經(jīng)常需要處理各種格式的數(shù)據(jù),本文將為您詳細(xì)介紹如何利用強(qiáng)大的第三方庫(kù) Spire.XLS for Java,輕松實(shí)現(xiàn) Excel 數(shù)據(jù)的查找與高亮功能,有需要的小伙伴可以了解下2025-09-09
完美解決SpringCloud-OpenFeign使用okhttp替換不生效問(wèn)題
這篇文章主要介紹了完美解決SpringCloud-OpenFeign使用okhttp替換不生效問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02
java實(shí)現(xiàn)學(xué)生成績(jī)信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)學(xué)生成績(jī)信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07
Netty框架實(shí)現(xiàn)TCP/IP通信的完美過(guò)程
這篇文章主要介紹了Netty框架實(shí)現(xiàn)TCP/IP通信,這里使用的是Springboot+Netty框架,使用maven搭建項(xiàng)目,需要的朋友可以參考下2021-07-07
java+jdbc+mysql+socket搭建局域網(wǎng)聊天室
這篇文章主要為大家詳細(xì)介紹了java+jdbc+mysql+socket搭建局域網(wǎng)聊天室,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01

