JAVA语言企业项目实战(三)

简介: 教程来源 http://oplhc.cn/category/tech-trends.html 本文详解秒杀系统核心实现:对比数据库乐观锁、Redis预减库存+异步下单、分布式锁+Lua脚本三种方案,涵盖高并发选型与一致性权衡;统一响应格式、全局异常处理保障健壮性;结合消息队列削峰填谷,确保高性能与数据最终一致。

第三部分:核心业务实现:秒杀逻辑详解

3.1 三种秒杀方案对比
秒杀的核心是库存扣减。我们设计了三种方案,分别适用于不同并发量的场景。

方案一:数据库乐观锁(适合中等并发,约500-2000 QPS)
乐观锁的核心思想:不锁定资源,而是在更新时检查数据是否被修改过。

-- 乐观锁扣减库存的SQL
UPDATE seckill_goods 
SET stock_count = stock_count - 1, version = version + 1 
WHERE id = #{goodsId} AND stock_count > 0 AND version = #{oldVersion}

工作原理:
查询商品信息,获取当前version
扣减库存时,在WHERE条件中加入version = oldVersion
如果更新行数为0,说明版本已变化(有其他线程修改了库存),需要重试

优点:
不需要数据库锁,性能较高
实现简单
缺点:
高并发下重试次数多,CPU消耗大
不适合极高并发场景

方案二:Redis预减库存 + 异步下单(适合高并发,5000-20000 QPS)

Redis是内存数据库,单机QPS可达10万+。我们将库存存储在Redis中,秒杀请求先操作Redis,成功后再异步处理订单。

工作流程:
秒杀开始前,将库存预热到Redis
秒杀请求到达时,使用Redis的DECR命令原子扣减库存
扣减成功后,将用户ID存入Redis Set(用于防重复)
发送MQ消息,异步创建订单

优点:
性能极高,Redis单机可支撑10万QPS
通过异步处理削峰填谷
缺点:
实现复杂,需要处理缓存与数据库的一致性
Redis故障会影响秒杀

方案三:分布式锁 + Lua脚本(最安全,适合对一致性要求极高的场景)
使用Redisson分布式锁保证同一时间只有一个线程操作库存,使用Lua脚本保证操作的原子性。

优点:
数据一致性最强
避免超卖
缺点:
性能相对较低
需要维护分布式锁
3.2 统一响应结果:让前后端沟通更顺畅
在前后端分离的开发模式中,统一的响应格式至关重要。它让前端能统一处理成功和失败的情况。

public class Result<T> {
    private Integer code;      // 状态码:200成功,其他失败
    private String message;    // 提示信息
    private T data;            // 响应数据
    private Long timestamp;    // 时间戳

    // 成功响应(无数据)
    public static <T> Result<T> success() {
        return new Result<>(200, "success", null);
    }

    // 成功响应(带数据)
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "success", data);
    }

    // 失败响应
    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null);
    }
}

为什么需要timestamp?
帮助前端判断响应是否过期
便于问题排查时定位时间点
防止重放攻击(可以检查时间戳是否在合理范围内)

3.3 全局异常处理:别让异常信息暴露给用户
在Web应用中,异常处理非常重要。如果让系统异常直接暴露给用户,不仅体验差,还可能泄露敏感信息。

@RestControllerAdvice
public class GlobalExceptionHandler {

    // 处理业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        // 记录警告日志(不是错误,是预期内的业务失败)
        log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
        // 提取所有校验失败的信息
        String message = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.error(400, message);
    }

    // 处理系统异常(兜底)
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        // 记录完整错误堆栈,便于排查
        log.error("系统异常: {}", e.getMessage(), e);
        // 返回通用错误信息,不暴露内部细节
        return Result.error(500, "系统繁忙,请稍后再试");
    }
}

为什么需要全局异常处理?
统一格式:所有错误响应格式一致,前端易于处理
信息过滤:防止敏感信息(如SQL语句、文件路径)泄露
日志记录:集中记录异常,便于监控和排查
代码简化:业务代码中只需抛出异常,不需要到处try-catch

3.4 秒杀核心服务详解
秒杀服务是整个系统的核心,我们重点分析三种方案的实现。

方案一实现:数据库乐观锁

@Transactional
public SeckillOrder seckillWithOptimisticLock(Long userId, Long goodsId) {
    // 1. 校验商品状态(是否存在、是否在秒杀时间内)
    SeckillGoods goods = validateSeckillGoods(goodsId);

    // 2. 检查用户是否已秒杀(防止重复)
    checkUserSeckillRecord(userId, goodsId);

    // 3. 乐观锁扣减库存
    // 关键点:WHERE条件中加入了stock_count > 0和version
    int updateCount = seckillGoodsMapper.decreaseStockWithVersion(goodsId, 1);
    if (updateCount == 0) {
        // 更新失败说明库存不足或版本冲突
        throw BusinessException.STOCK_NOT_ENOUGH;
    }

    // 4. 创建订单
    SeckillOrder order = createOrder(userId, goods);

    // 5. 记录秒杀记录
    saveSeckillRecord(userId, goodsId, order.getId(), 1);

    return order;
}

关键点解释:

@Transactional:保证库存扣减和订单创建在同一个事务中,要么都成功,要么都失败
decreaseStockWithVersion:使用乐观锁,避免行锁导致的性能问题
updateCount == 0:表示库存不足或版本冲突,需要告诉用户秒杀失败
方案二实现:Redis预减库存

public void seckillWithRedis(Long userId, Long goodsId) {
    // 1. Redis原子扣减库存
    String stockKey = "seckill:stock:" + goodsId;
    Long stock = redisTemplate.opsForValue().decrement(stockKey);

    // 如果库存小于0,说明已经卖完了
    if (stock == null || stock < 0) {
        // 恢复库存(decr后为负数,需要加回来)
        if (stock != null && stock < 0) {
            redisTemplate.opsForValue().increment(stockKey);
        }
        throw BusinessException.STOCK_NOT_ENOUGH;
    }

    // 2. 防止重复秒杀(使用Redis Set)
    String userKey = "seckill:user:" + goodsId;
    Boolean isSuccess = redisTemplate.opsForSet().add(userKey, userId.toString());
    if (Boolean.FALSE.equals(isSuccess)) {
        // 用户已参与,恢复库存
        redisTemplate.opsForValue().increment(stockKey);
        throw BusinessException.REPEAT_SECKILL;
    }

    // 3. 发送MQ消息,异步创建订单
    seckillMessageSender.sendMessage(userId, goodsId);
}

为什么使用Redis DECR?
DECR是原子操作,不用担心并发问题
Redis单线程模型,天然保证原子性
性能极高,单机可达10万+ QPS
为什么需要恢复库存?
DECR操作后如果库存变为负数,说明超卖了
需要将库存加回,保证数据正确性

3.5 消息队列异步处理:削峰填谷
秒杀场景下,如果所有订单创建都同步进行,数据库会承受巨大压力。使用消息队列可以将请求“削峰填谷”。

@Component
public class SeckillMessageConsumer {

    @RabbitListener(queues = "seckill.queue")
    public void handleMessage(SeckillDTO dto) {
        // 1. 再次校验库存(数据库层)
        SeckillGoods goods = seckillGoodsMapper.selectById(dto.getGoodsId());
        if (goods.getStockCount() <= 0) {
            log.warn("库存不足,放弃处理");
            return;
        }

        // 2. 乐观锁扣减数据库库存
        int updateCount = seckillGoodsMapper.decreaseStockWithVersion(
                dto.getGoodsId(), 1);
        if (updateCount == 0) {
            log.warn("扣减库存失败,放弃处理");
            return;
        }

        // 3. 创建订单
        SeckillOrder order = new SeckillOrder();
        order.setOrderNo(generateOrderNo());
        order.setUserId(dto.getUserId());
        order.setGoodsId(goods.getId());
        order.setGoodsName(goods.getGoodsName());
        order.setGoodsPrice(goods.getSeckillPrice());
        seckillOrderMapper.insert(order);

        log.info("订单创建成功: {}", order.getOrderNo());
    }
}

为什么需要异步处理?
秒杀请求只需要快速响应“已收到请求”,用户可以稍后查看结果
订单创建涉及数据库写入,耗时较长,不适合同步处理
消息队列可以缓冲请求,避免数据库被打垮
为什么不直接信任Redis库存?
Redis可能宕机或数据丢失
最终一致性:Redis库存只是“预扣减”,真正的扣减必须在数据库完成
数据库是最终权威数据源
来源:
http://oplhc.cn/category/software-apps.html

相关文章
|
5月前
|
消息中间件 缓存 NoSQL
JAVA语言企业项目实战(一)
教程来源 http://oplhc.cn/ 本文深入剖析电商秒杀系统的设计与实现,直击高并发、数据一致性、系统稳定性等Java企业级开发核心挑战。涵盖单体→缓存→异步→微服务四阶段架构演进,详解库存防超卖、热点优化、限流降级等关键技术,助你打造高性能、高可用的实战级系统。
|
安全
[UDS] --- TesterPresent 0x3E
[UDS] --- TesterPresent 0x3E
979 1
|
存储 Docker 容器
Containerd数据持久化和网络管理
Containerd数据持久化和网络管理
|
7月前
|
缓存 人工智能 监控
Prompt Caching终极指南:Claude Code省钱核心+阿里云OpenClaw部署与缓存配置实战教程
在AI编码与智能体开发飞速发展的2026年,成本控制与响应速度成为核心痛点。而Claude Code之所以能实现“低价高效”,其底层核心基础设施——Prompt Caching(提示词缓存)功不可没。这项从设计之初就融入产品架构的技术,能让API调用成本降低90%、响应速度提升85%,彻底改变了AI工具的使用经济性。
3936 1
|
4月前
|
人工智能 中间件 索引
Markdown是什么?——AI时代最值得掌握的文档语言
在AI处理信息成为常态的今天,文档格式的竞争已从“人类看着美”转向“机器读着快”。Markdown凭借极致的Token效率、清晰的语义结构和与AI训练数据的高度契合,成为连接人与大模型的“默认语言”。本文用最简洁的方式解释:为什么Markdown既是AI的“母语”,也是你与AI高效协作的必备工具。
769 2
|
5月前
|
存储 运维 监控
智算中心建设项目一般过程解析
智算中心是支撑AI、大数据发展的新型算力基础设施。九章云极主导建设运营,覆盖立项、设计、部署等六阶段全流程,3年内目标纳管10万P算力。(239字)
|
5月前
|
NoSQL Java Redis
JAVA语言企业项目实战(六)
教程来源 http://qcycj.cn/category/jiujieshao.html 本文详解秒杀系统部署与运维实践:涵盖Docker容器化、Docker Compose一键编排、Prometheus监控告警;总结核心思想——流量控制、缓存为王、最终一致性;解析超卖防护、高可用保障等面试要点,并探讨架构演进的务实原则。
|
5月前
|
缓存 NoSQL Java
JAVA语言企业项目实战(四)
教程来源 http://oplhc.cn/category/artificial-intelligence.html 本节详解秒杀系统缓存优化:通过Redis预热热点数据降低数据库压力;针对缓存穿透(空值缓存)、击穿(分布式锁+双重检查)、雪崩(随机过期时间)三大问题,提供可落地的解决方案,全面提升系统并发性能与稳定性。
|
5月前
|
存储 Java 数据库
JAVA语言企业项目实战(二)
教程来源 http://oplhc.cn/category/hardware-review.html 本节详解秒杀系统数据库设计:强调读写分离、热点隔离、冗余字段与索引优化;详述用户、秒杀商品、订单及记录四张核心表结构,含BCrypt加密、乐观锁、唯一约束等关键设计;并给出HikariCP连接池合理配置策略。
|
11月前
|
消息中间件 监控 Kubernetes
别再乱排查了!Kafka 消息积压、重复、丢失,根源基本都是 Rebalance!
大家好,我是小富~分享一次Kafka消息积压排查经历:消费者组因Rebalance导致消费能力骤降。本文详解Rebalance触发场景(消费者变更、分区扩容、订阅变化、超时等),剖析其引发的消息积压、重复消费、丢失等问题根源,并提供优化方案:调优超时参数、手动提交offset、启用粘性分配策略、保障消费幂等性。掌握这些,轻松应对Kafka常见故障!
1974 0

热门文章

最新文章