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

相关文章
|
6天前
|
人工智能 数据可视化 安全
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
本文详解如何用阿里云Lighthouse一键部署OpenClaw,结合飞书CLI等工具,让AI真正“动手”——自动群发、生成科研日报、整理知识库。核心理念:未来软件应为AI而生,CLI即AI的“手脚”,实现高效、安全、可控的智能自动化。
18006 12
王炸组合!阿里云 OpenClaw X 飞书 CLI,开启 Agent 基建狂潮!(附带免费使用6个月服务器)
|
17天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
29546 141
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
7天前
|
人工智能 JSON 监控
Claude Code 源码泄露:一份价值亿元的 AI 工程公开课
我以为顶级 AI 产品的护城河是模型。读完这 51.2 万行泄露的源码,我发现自己错了。
4612 20
|
6天前
|
人工智能 API 开发者
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案
阿里云百炼Coding Plan Lite已停售,Pro版每日9:30限量抢购难度大。本文解析原因,并提供两大方案:①掌握技巧抢购Pro版;②直接使用百炼平台按量付费——新用户赠100万Tokens,支持Qwen3.5-Max等满血模型,灵活低成本。
1453 3
阿里云百炼 Coding Plan 售罄、Lite 停售、Pro 抢不到?最新解决方案