Java秒杀系统设计与实现
📋 问题背景
秒杀系统是电商平台的核心功能,也是Java面试中的超高频考点。在实际工作中,秒杀场景具有以下特点:
- 高并发:瞬间大量用户涌入
- 库存有限:商品数量远少于用户数量
- 时间集中:在特定时间点爆发
- 数据一致性:防止超卖问题
🎯 核心技术挑战
1. 高并发冲击
- 数据库连接池耗尽
- 服务器资源不足
- 响应时间过长
2. 超卖问题
- 库存扣减不准确
- 并发修改数据
- 事务隔离级别
3. 系统稳定性
- 服务雪崩
- 缓存击穿
- 数据库压力
🛠️ 技术解决方案
架构设计图
用户请求 → 负载均衡 → 网关限流 → 秒杀服务 → Redis缓存 → 消息队列 → 数据库
核心技术栈
- Spring Boot - 微服务框架
- Redis - 缓存和分布式锁
- RabbitMQ - 异步处理
- MySQL - 数据持久化
- Redisson - 分布式锁实现
💻 完整代码实现
1. 秒杀商品实体类
java
@Entity
@Table(name = "seckill_goods")
@Data
public class SeckillGoods {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String goodsName;
private BigDecimal price;
private Integer stock;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer version; // 乐观锁版本号
@CreationTimestamp
private LocalDateTime createTime;
@UpdateTimestamp
private LocalDateTime updateTime;
}
2. 秒杀订单实体类
java
@Entity
@Table(name = "seckill_order")
@Data
public class SeckillOrder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private Long goodsId;
private String orderNo;
private BigDecimal price;
private Integer status; // 0-待支付 1-已支付 2-已取消
@CreationTimestamp
private LocalDateTime createTime;
}
3. Redis配置类
java
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 设置序列化器
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setConnectionPoolSize(50)
.setConnectionMinimumIdleSize(10);
return Redisson.create(config);
}
}
4. 秒杀服务核心实现
java
@Service
@Slf4j
public class SeckillService {
@Autowired
private SeckillGoodsRepository goodsRepository;
@Autowired
private SeckillOrderRepository orderRepository;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
@Autowired
private RabbitTemplate rabbitTemplate;
private static final String STOCK_KEY = "seckill:stock:";
private static final String USER_ORDER_KEY = "seckill:user:";
private static final String LOCK_KEY = "seckill:lock:";
/**
* 预热库存到Redis
*/
public void preloadStock(Long goodsId) {
SeckillGoods goods = goodsRepository.findById(goodsId).orElse(null);
if (goods != null) {
redisTemplate.opsForValue().set(STOCK_KEY + goodsId, goods.getStock());
log.info("预热商品库存到Redis: goodsId={}, stock={}", goodsId, goods.getStock());
}
}
/**
* 秒杀主流程 - 方案一:Redis + 分布式锁
*/
@Transactional(rollbackFor = Exception.class)
public Result<String> seckillWithRedisLock(Long goodsId, Long userId) {
// 1. 参数校验
if (goodsId == null || userId == null) {
return Result.fail("参数错误");
}
// 2. 检查用户是否已经购买过
String userOrderKey = USER_ORDER_KEY + goodsId + ":" + userId;
if (redisTemplate.hasKey(userOrderKey)) {
return Result.fail("您已经购买过该商品");
}
// 3. 获取分布式锁
RLock lock = redissonClient.getLock(LOCK_KEY + goodsId);
try {
// 尝试获取锁,最多等待1秒,锁定10秒后自动释放
if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
// 4. 检查Redis库存
String stockKey = STOCK_KEY + goodsId;
Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
if (stock == null || stock <= 0) {
return Result.fail("商品已售罄");
}
// 5. 扣减Redis库存
Long remainStock = redisTemplate.opsForValue().decrement(stockKey);
if (remainStock < 0) {
// 库存不足,回滚
redisTemplate.opsForValue().increment(stockKey);
return Result.fail("商品已售罄");
}
// 6. 创建订单(异步处理)
SeckillOrderDTO orderDTO = new SeckillOrderDTO();
orderDTO.setGoodsId(goodsId);
orderDTO.setUserId(userId);
orderDTO.setOrderNo(generateOrderNo());
// 发送到消息队列异步处理
rabbitTemplate.convertAndSend("seckill.order.exchange",
"seckill.order.create", orderDTO);
// 7. 标记用户已购买
redisTemplate.opsForValue().set(userOrderKey, "1", 30, TimeUnit.MINUTES);
log.info("秒杀成功: userId={}, goodsId={}, remainStock={}",
userId, goodsId, remainStock);
return Result.success("秒杀成功,正在生成订单...");
} else {
return Result.fail("系统繁忙,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.fail("系统异常");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 秒杀主流程 - 方案二:Lua脚本原子操作
*/
public Result<String> seckillWithLua(Long goodsId, Long userId) {
// Lua脚本保证原子性
String luaScript =
"local stockKey = KEYS[1] " +
"local userKey = KEYS[2] " +
"local userId = ARGV[1] " +
"-- 检查用户是否已购买 " +
"if redis.call('exists', userKey) == 1 then " +
" return -1 " +
"end " +
"-- 检查库存 " +
"local stock = redis.call('get', stockKey) " +
"if not stock or tonumber(stock) <= 0 then " +
" return 0 " +
"end " +
"-- 扣减库存并标记用户 " +
"redis.call('decr', stockKey) " +
"redis.call('setex', userKey, 1800, '1') " +
"return 1";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
List<String> keys = Arrays.asList(
STOCK_KEY + goodsId,
USER_ORDER_KEY + goodsId + ":" + userId
);
Long result = redisTemplate.execute(script, keys, userId.toString());
if (result == -1) {
return Result.fail("您已经购买过该商品");
} else if (result == 0) {
return Result.fail("商品已售罄");
} else {
// 异步创建订单
SeckillOrderDTO orderDTO = new SeckillOrderDTO();
orderDTO.setGoodsId(goodsId);
orderDTO.setUserId(userId);
orderDTO.setOrderNo(generateOrderNo());
rabbitTemplate.convertAndSend("seckill.order.exchange",
"seckill.order.create", orderDTO);
return Result.success("秒杀成功,正在生成订单...");
}
}
/**
* 异步创建订单
*/
@RabbitListener(queues = "seckill.order.queue")
@Transactional(rollbackFor = Exception.class)
public void createOrderAsync(SeckillOrderDTO orderDTO) {
try {
// 1. 查询商品信息
SeckillGoods goods = goodsRepository.findById(orderDTO.getGoodsId()).orElse(null);
if (goods == null) {
log.error("商品不存在: goodsId={}", orderDTO.getGoodsId());
return;
}
// 2. 使用乐观锁扣减数据库库存
int updated = goodsRepository.decreaseStock(orderDTO.getGoodsId(), goods.getVersion());
if (updated == 0) {
log.warn("数据库库存扣减失败: goodsId={}", orderDTO.getGoodsId());
// 回滚Redis库存
redisTemplate.opsForValue().increment(STOCK_KEY + orderDTO.getGoodsId());
return;
}
// 3. 创建订单
SeckillOrder order = new SeckillOrder();
order.setUserId(orderDTO.getUserId());
order.setGoodsId(orderDTO.getGoodsId());
order.setOrderNo(orderDTO.getOrderNo());
order.setPrice(goods.getPrice());
order.setStatus(0); // 待支付
orderRepository.save(order);
log.info("订单创建成功: orderNo={}", orderDTO.getOrderNo());
} catch (Exception e) {
log.error("创建订单失败: {}", e.getMessage(), e);
// 回滚Redis库存
redisTemplate.opsForValue().increment(STOCK_KEY + orderDTO.getGoodsId());
}
}
/**
* 生成订单号
*/
private String generateOrderNo() {
return "SK" + System.currentTimeMillis() +
String.format("%04d", new Random().nextInt(10000));
}
}
5. 秒杀控制器
java
@RestController
@RequestMapping("/api/seckill")
@Slf4j
public class SeckillController {
@Autowired
private SeckillService seckillService;
/**
* 秒杀接口 - 限流版本
*/
@PostMapping("/buy/{goodsId}")
@RateLimiter(name = "seckill", fallbackMethod = "seckillFallback")
public Result<String> seckill(@PathVariable Long goodsId,
@RequestHeader("userId") Long userId) {
try {
return seckillService.seckillWithLua(goodsId, userId);
} catch (Exception e) {
log.error("秒杀异常: goodsId={}, userId={}, error={}",
goodsId, userId, e.getMessage(), e);
return Result.fail("系统繁忙,请稍后重试");
}
}
/**
* 限流降级方法
*/
public Result<String> seckillFallback(Long goodsId, Long userId, Exception e) {
return Result.fail("当前访问人数过多,请稍后重试");
}
/**
* 预热库存
*/
@PostMapping("/preload/{goodsId}")
public Result<String> preloadStock(@PathVariable Long goodsId) {
seckillService.preloadStock(goodsId);
return Result.success("库存预热成功");
}
}
6. 数据库层优化
java
@Repository
public interface SeckillGoodsRepository extends JpaRepository<SeckillGoods, Long> {
/**
* 乐观锁扣减库存
*/
@Modifying
@Query("UPDATE SeckillGoods g SET g.stock = g.stock - 1, g.version = g.version + 1 " +
"WHERE g.id = :goodsId AND g.stock > 0 AND g.version = :version")
int decreaseStock(@Param("goodsId") Long goodsId, @Param("version") Integer version);
/**
* 查询可秒杀商品
*/
@Query("SELECT g FROM SeckillGoods g WHERE g.startTime <= :now AND g.endTime >= :now AND g.stock > 0")
List<SeckillGoods> findAvailableGoods(@Param("now") LocalDateTime now);
}
🚀 性能优化策略
1. 多级缓存架构
java
@Component
public class MultiLevelCache {
private final LoadingCache<String, Object> localCache;
private final RedisTemplate<String, Object> redisTemplate;
public MultiLevelCache() {
this.localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> redisTemplate.opsForValue().get(key));
}
public Object get(String key) {
try {
return localCache.get(key);
} catch (Exception e) {
return redisTemplate.opsForValue().get(key);
}
}
}
2. 限流策略
java
@Component
public class RateLimitAspect {
private final RedisTemplate<String, String> redisTemplate;
@Around("@annotation(rateLimiter)")
public Object around(ProceedingJoinPoint point, RateLimiter rateLimiter) throws Throwable {
String key = "rate_limit:" + getClientId();
String luaScript =
"local key = KEYS[1] " +
"local limit = tonumber(ARGV[1]) " +
"local window = tonumber(ARGV[2]) " +
"local current = redis.call('incr', key) " +
"if current == 1 then " +
" redis.call('expire', key, window) " +
"end " +
"return current";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
Long count = redisTemplate.execute(script,
Collections.singletonList(key),
String.valueOf(rateLimiter.limit()),
String.valueOf(rateLimiter.window()));
if (count > rateLimiter.limit()) {
throw new RateLimitException("请求过于频繁");
}
return point.proceed();
}
}
📊 监控与告警
1. 关键指标监控
java
@Component
public class SeckillMetrics {
private final MeterRegistry meterRegistry;
private final Counter successCounter;
private final Counter failCounter;
private final Timer responseTimer;
public SeckillMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.successCounter = Counter.builder("seckill.success").register(meterRegistry);
this.failCounter = Counter.builder("seckill.fail").register(meterRegistry);
this.responseTimer = Timer.builder("seckill.response.time").register(meterRegistry);
}
public void recordSuccess() {
successCounter.increment();
}
public void recordFail() {
failCounter.increment();
}
public Timer.Sample startTimer() {
return Timer.start(meterRegistry);
}
}
🎯 面试要点总结
常见面试问题及答案
Q: 如何防止超卖问题?
A:
- 乐观锁:使用version字段,更新时检查版本号
- 分布式锁:Redis分布式锁保证同一时间只有一个线程操作
- Lua脚本:保证Redis操作的原子性
- 数据库约束:设置库存字段非负约束
Q: 如何应对高并发?
A:
- 读写分离:读操作走缓存,写操作走数据库
- 异步处理:使用消息队列异步创建订单
- 限流降级:接口限流,超出阈值直接返回失败
- CDN加速:静态资源使用CDN分发
Q: 缓存和数据库数据一致性如何保证?
A:
- 最终一致性:通过消息队列异步同步数据
- 双写模式:先写数据库,再写缓存
- 失效模式:先删缓存,再写数据库
- 分布式事务:使用Seata等分布式事务框架
💡 实际工作应用
1. 电商秒杀场景
- 双11、618大促活动
- 限时抢购商品
- 新品首发预售
2. 票务系统
- 演唱会门票抢购
- 火车票抢票系统
- 景区门票预订
3. 红包系统
- 微信红包抢夺
- 优惠券发放
- 积分兑换活动
🔧 扩展优化方案
1. 预扣库存方案
java
public class PreDeductStock {
/**
* 预扣库存,减少数据库压力
*/
public boolean preDeductStock(Long goodsId, Integer quantity) {
String key = "pre_deduct:" + goodsId;
String luaScript =
"local stock = redis.call('get', KEYS[1]) " +
"if not stock or tonumber(stock) < tonumber(ARGV[1]) then " +
" return 0 " +
"end " +
"redis.call('decrby', KEYS[1], ARGV[1]) " +
"return 1";
// 执行Lua脚本
Long result = redisTemplate.execute(script,
Collections.singletonList(key), quantity.toString());
return result == 1;
}
}
2. 分段锁优化
java
public class SegmentLock {
private static final int SEGMENT_COUNT = 16;
public String getSegmentKey(Long goodsId) {
int segment = (int) (goodsId % SEGMENT_COUNT);
return "seckill:lock:" + goodsId + ":segment:" + segment;
}
}
📚 学习建议
1. 基础知识掌握
- Redis数据结构和命令
- 分布式锁原理
- 消息队列使用
- 数据库事务隔离级别
2. 实践项目
- 搭建完整的秒杀系统
- 进行压力测试
- 监控系统性能指标
- 优化系统瓶颈
3. 面试准备
- 熟练掌握核心代码实现
- 理解各种解决方案的优缺点
- 准备系统设计图和架构说明
- 练习口述技术方案
总结:秒杀系统是一个综合性很强的技术场景,涉及高并发、分布式、缓存、消息队列等多个技术领域。掌握好秒杀系统的设计和实现,对于Java开发者的技术成长和面试准备都有很大帮助。
关键是要理解每个技术方案解决的具体问题,以及在不同场景下如何选择合适的技术方案。通过实际项目练习,加深对高并发系统设计的理解。