在商城项目中,秒杀功能可以说是必不可少的,下面我将使用Spring Boot集成Redis、RabbitMQ、MyBatis-Plus和MySQL来实现一个简单的秒杀系统,系统将包含以下核心功能:
使用Redis进行库存预减和用户限流;
使用RabbitMQ进行异步下单,提高系统吞吐量;
使用MyBatis-Plus操作MySQL数据库;
利用Redis执行Lua脚本的原子性防止商品超卖;
接口限流(使用Redis或网关限流,这里使用gateway实现简单限流);
CREATE TABLE `seckill_goods` (
`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '秒杀商品ID',
`origin_id` BIGINT(20) NOT NULL COMMENT '原始商品ID',
`name` VARCHAR(100) NOT NULL COMMENT '商品名称',
`price` DECIMAL(10,2) NOT NULL DEFAULT '0.00' COMMENT '原价',
`seckill_price` DECIMAL(10,2) NOT NULL COMMENT '秒杀价',
`stock` INT(11) NOT NULL COMMENT '库存数量',
`start_time` DATETIME NOT NULL COMMENT '开始时间',
`end_time` DATETIME NOT NULL COMMENT '结束时间',
`is_active` TINYINT(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
`version` INT(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_time` (`start_time`,`end_time`),
KEY `idx_origin` (`origin_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀商品表';
CREATE TABLE `seckill_order` (
`id` VARCHAR(32) NOT NULL COMMENT '订单ID(时间戳+用户ID哈希)',
`user_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '用户ID',
`goods_id` BIGINT(20) UNSIGNED NOT NULL COMMENT '秒杀商品ID',
`quantity` INT(11) NOT NULL DEFAULT '1' COMMENT '购买数量',
`price` DECIMAL(10,2) NOT NULL COMMENT '实际支付价格',
`status` TINYINT(4) NOT NULL DEFAULT '0' COMMENT '0-未支付 1-已支付 2-已取消',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_goods` (`user_id`,`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀订单表';
@Configuration
public class GatewayConfig {
private static final Logger log = LoggerFactory.getLogger(GatewayConfig.class);
// 限流过滤器
@Bean
@Order(-1)
public GlobalFilter rateLimitFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String ip = Objects.requireNonNull(request.getRemoteAddress()).getAddress().getHostAddress();
String path = request.getPath().value();
// 秒杀接口特殊限流
if (path.startsWith("/api/seckill")) {
if (!rateLimiter.tryAcquire(ip, 1, 5, TimeUnit.SECONDS)) {
log.warn("IP限流触发: {}", ip);
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
}
}
return chain.filter(exchange);
};
}
// 路由配置
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("seckill_route", r -> r.path("/api/seckill/**")
.filters(f -> f.stripPrefix(2)
// 添加请求头
.addRequestHeader("X-Seckill-Token", "secure_token")
.circuitBreaker(config -> config
.setName("seckillCircuitBreaker")
// 设置回调方法请求地址(失败时)
.setFallbackUri("forward:/fallback")))
// 为秒杀请求配置负载均衡
.uri("lb://seckill-service"))
.route("order_route", r -> r.path("/api/order/**")
.filters(f -> f.stripPrefix(2))
.uri("lb://order-service"))
.build();
}
// 断路器降级处理
@RequestMapping("/fallback")
public Mono> fallback() {
return Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("{\"code\":503,\"message\":\"服务不可用,请稍后重试\"}"));
}
}
@Service
public class RedisRateLimiter {
private final RedisTemplate redisTemplate;
public RedisRateLimiter(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 参数含义:redis的key、窗口期内可请求的最大次数、周期、时间单位
public boolean tryAcquire(String key, int permits, int period, TimeUnit timeUnit) {
String redisKey = "rate_limit:" + key;
long current = System.currentTimeMillis();
long windowStart = current - timeUnit.toMillis(period);
return redisTemplate.execute(new SessionCallback() {
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) {
operations.multi();
// 创建一条记录
operations.opsForZSet().add(redisKey, current, current);
// 删除不在窗口期的请求记录
operations.opsForZSet().removeRangeByScore(redisKey, 0, windowStart);
// 计算窗口期内请求的次数
operations.opsForZSet().zCard(redisKey);
// 设置过期时间,提高内存利用率
operations.expire(redisKey, period + 1, timeUnit);
List
@Service
@Slf4j
public class SeckillServiceImpl implements SeckillService {
private final RedisTemplate redisTemplate;
private final RabbitTemplate rabbitTemplate;
private final SeckillGoodsMapper seckillGoodsMapper;
private static final String STOCK_KEY = "seckill:stock:%s";
private static final String USER_LIMIT_KEY = "seckill:user:%s:%s";
// Lua脚本原子操作
private static final DefaultRedisScript SECKILL_SCRIPT = new DefaultRedisScript<>();
static {
SECKILL_SCRIPT.setLocation(new ClassPathResource("lua/seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
@Override
@Transactional(rollbackFor = Exception.class)
public SeckillResponse handleSeckill(Long userId, Long goodsId) {
// 1. 校验商品有效性
SeckillGoods goods = seckillGoodsMapper.selectById(goodsId);
if (goods == null || !goods.getIsActive()) {
return SeckillResponse.fail("秒杀商品不存在");
}
long now = System.currentTimeMillis();
if (now < goods.getStartTime().getTime()) {
return SeckillResponse.fail("秒杀未开始");
}
if (now > goods.getEndTime().getTime()) {
return SeckillResponse.fail("秒杀已结束");
}
// 2. Redis原子操作
String stockKey = String.format(STOCK_KEY, goodsId);
String userLimitKey = String.format(USER_LIMIT_KEY, userId, goodsId);
List keys = Arrays.asList(stockKey, userLimitKey);
Long result = redisTemplate.execute(
SECKILL_SCRIPT,
keys,
userId, goodsId, 1 // 每人限购1件
);
if (result == null) {
return SeckillResponse.fail("系统异常");
}
// 3. 处理结果
if (result == 1) {
// 发送MQ消息
SeckillMessage message = new SeckillMessage(userId, goodsId, goods.getSeckillPrice());
rabbitTemplate.convertAndSend("seckill.exchange", "seckill.route", message);
return SeckillResponse.success("秒杀成功,正在生成订单");
} else {
return SeckillResponse.fail(switch (result.intValue()) {
case -1 -> "库存不足";
case 0 -> "超出购买限制";
case -2 -> "重复请求";
default -> "秒杀失败";
});
}
}
// 初始化库存到Redis
@PostConstruct
public void initStockToRedis() {
List activeGoods = seckillGoodsMapper.selectList(
new LambdaQueryWrapper()
.eq(SeckillGoods::getIsActive, 1)
);
for (SeckillGoods goods : activeGoods) {
String stockKey = String.format(STOCK_KEY, goods.getId());
redisTemplate.opsForValue().set(stockKey, goods.getStock());
redisTemplate.expireAt(stockKey, goods.getEndTime());
}
}
}
-- KEYS[1]: 库存key (seckill:stock:{goodsId})
-- KEYS[2]: 用户限购key (seckill:user:{userId}:{goodsId})
-- ARGV[1]: userId
-- ARGV[2]: goodsId
-- ARGV[3]: 购买限制数量
-- 检查用户是否已经购买过
local userPurchased = redis.call('EXISTS', KEYS[2])
if userPurchased == 1 then
local purchasedCount = tonumber(redis.call('GET', KEYS[2]))
if purchasedCount >= tonumber(ARGV[3]) then
return 0; -- 超出限购数量
end
-- 检查是否重复请求(已成功购买但未达限购)
if purchasedCount > 0 then
return -2; -- 重复请求
end
end
-- 检查库存
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock <= 0 then
return -1; -- 库存不足
end
-- 扣减库存
redis.call('DECR', KEYS[1])
-- 更新用户购买记录
if userPurchased == 0 then
redis.call('SET', KEYS[2], 1)
redis.call('EXPIREAT', KEYS[2], 172800) -- 2天有效期
else
redis.call('INCR', KEYS[2])
end
return 1; -- 成功
@Configuration
public class RabbitMQConfig {
@Bean
public TopicExchange seckillExchange() {
return new TopicExchange("seckill.exchange", true, false);
}
@Bean
public Queue seckillQueue() {
return new Queue("seckill.queue", true);
}
@Bean
public Binding seckillBinding() {
return BindingBuilder.bind(seckillQueue())
.to(seckillExchange())
.with("seckill.route");
}
@Bean
public Jackson2JsonMessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
ConnectionFactory connectionFactory,
Jackson2JsonMessageConverter messageConverter) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(messageConverter);
factory.setPrefetchCount(100); // 预取消息数量
return factory;
}
// 生产者确认回调
@Bean
public RabbitTemplate.ConfirmCallback confirmCallback() {
return (correlationData, ack, cause) -> {
if (!ack) {
log.error("MQ消息发送失败: {}", cause);
// TODO: 处理失败逻辑
}
};
}
}
@Service
@Slf4j
public class OrderConsumer {
private final OrderService orderService;
private final RedisTemplate redisTemplate;
// 监听秒杀队列
@RabbitListener(queues = "seckill.queue")
public void handleSeckillMessage(SeckillMessage message) {
log.info("收到秒杀消息: {}", message);
Long userId = message.getUserId();
Long goodsId = message.getGoodsId();
BigDecimal price = message.getPrice();
try {
// 幂等性检查
if (orderService.isOrderExists(userId, goodsId)) {
log.warn("重复订单: userId={}, goodsId={}", userId, goodsId);
return;
}
// 创建订单
String orderId = generateOrderId(userId);
SeckillOrder order = new SeckillOrder();
order.setId(orderId);
order.setUserId(userId);
order.setGoodsId(goodsId);
order.setQuantity(1);
order.setPrice(price);
boolean success = orderService.createOrder(order);
if (success) {
// 缓存订单
redisTemplate.opsForValue().set(
"order:" + orderId,
order,
30, TimeUnit.MINUTES
);
}
} catch (Exception e) {
log.error("订单创建失败: {}", e.getMessage());
// 重试机制
throw new AmqpRejectAndDontRequeueException("处理失败,入死信队列");
}
}
// 生成订单ID:时间戳+用户ID哈希
private String generateOrderId(Long userId) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String timestamp = sdf.format(new Date());
int hash = Math.abs(userId.hashCode()) % 10000;
return timestamp + String.format("%04d", hash);
}
}
@Service
public class OrderServiceImpl extends ServiceImpl
implements OrderService {
private static final int MAX_RETRY = 3; // 乐观锁最大重试次数
@Override
public boolean createOrder(SeckillOrder order) {
// 使用分布式锁防止重复创建
RLock lock = redissonClient.getLock("order:lock:" + order.getUserId());
try {
lock.lock(3, TimeUnit.SECONDS);
// 再次检查订单是否存在
if (isOrderExists(order.getUserId(), order.getGoodsId())) {
return false;
}
// 插入订单
return save(order);
} finally {
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
@Override
public boolean isOrderExists(Long userId, Long goodsId) {
return lambdaQuery()
.eq(SeckillOrder::getUserId, userId)
.eq(SeckillOrder::getGoodsId, goodsId)
.exists();
}
// 定时更新库存到数据库
@Scheduled(fixedRate = 60000) // 每分钟同步一次
public void syncStockToDB() {
List goodsList = seckillGoodsMapper.selectList(
new LambdaQueryWrapper()
.eq(SeckillGoods::getIsActive, 1)
);
for (SeckillGoods goods : goodsList) {
String stockKey = String.format("seckill:stock:%s", goods.getId());
Integer redisStock = (Integer) redisTemplate.opsForValue().get(stockKey);
if (redisStock != null && !redisStock.equals(goods.getStock())) {
seckillGoodsMapper.updateStock(goods.getId(), redisStock, goods.getVersion());
}
}
}
}
UPDATE seckill_goods
SET
stock = #{newStock},
version = version + 1
WHERE
id = #{goodsId}
AND version = #{version}
public interface SeckillGoodsMapper extends BaseMapper {
@Update("UPDATE seckill_goods SET stock = #{newStock}, version = version + 1 " +
"WHERE id = #{goodsId} AND version = #{version}")
int updateStock(@Param("goodsId") Long goodsId,
@Param("newStock") Integer newStock,
@Param("version") Integer oldVersion);
}
application-prod.yml
spring:
redis:
host: redis-cluster
port: 6379
password: ${REDIS_PASSWORD}
lettuce:
pool:
max-active: 200
max-idle: 50
min-idle: 10
max-wait: 1000ms
rabbitmq:
host: rabbitmq-cluster
port: 5672
username: ${RABBITMQ_USER}
password: ${RABBITMQ_PASS}
virtual-host: /seckill
publisher-confirm-type: correlated
publisher-returns: true
listener:
simple:
prefetch: 100
acknowledge-mode: manual
datasource:
url: jdbc:mysql://mysql-cluster:3306/seckill_db?useSSL=false&allowPublicKeyRetrieval=true
username: ${DB_USER}
password: ${DB_PASS}
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 5
min-idle: 5
max-active: 50
max-wait: 3000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
cloud:
gateway:
httpclient:
connect-timeout: 1000
response-timeout: 3s
pool:
max-connections: 1000
max-idle-time: 30000
seckill:
redis:
prefix: "sec:" # Redis键前缀
mq:
exchange: seckill.exchange
queue: seckill.queue
routing-key: seckill.route
17
3.2.0
2023.0.0
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-starter-validation
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.boot
spring-boot-starter-data-redis
org.redisson
redisson-spring-boot-starter
3.25.0
org.springframework.boot
spring-boot-starter-amqp
com.mysql
mysql-connector-j
runtime
com.baomidou
mybatis-plus-boot-starter
3.5.5
com.alibaba
druid-spring-boot-starter
1.2.20
org.projectlombok
lombok
true
com.fasterxml.jackson.core
jackson-databind
org.springframework.boot
spring-boot-starter-test
test
org.springframework.amqp
spring-rabbit-test
test
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import