Java工作案例

wen java案例 5

我为你整理了一个Java后端开发的典型工作案例,涵盖从需求分析到部署上线的完整流程,这个案例模拟了真实企业级开发场景。

Java工作案例


案例:电商平台「秒杀抢购系统」设计与开发

项目背景与需求分析

业务背景: 某电商平台计划在双11期间推出限量商品秒杀活动,预期瞬间流量是平时的100倍(约10万QPS),需要在保证系统稳定性的前提下,确保不超卖、不重复下单

核心需求

  1. 高并发秒杀接口(QPS ≥ 5,000)
  2. 严格的库存一致性(100件商品,绝不能卖出101件)
  3. 限流防刷(同一用户限购1件)
  4. 秒杀结果异步通知(邮件/短信)
  5. 系统监控与应急降级

技术选型与架构设计

技术栈: | 层次 | 技术选择 | |------|----------| | 开发语言 | Java 17 | | 框架 | Spring Boot 3.2 + MyBatis-Plus | | 缓存 | Redis 7(分布式锁 + 预减库存) | | 消息队列 | RocketMQ 5.x(异步订单处理) | | 数据库 | MySQL 8.x(分库分表:sharding-jdbc) | | 网关 | Spring Cloud Gateway(限流)+ Nginx | | 监控 | Prometheus + Grafana + SkyWalking |

架构流程图

用户请求 → Nginx (负载均衡/静态分离)
         → Spring Cloud Gateway (接口限流/Token验证)
         → 秒杀服务集群 (分布式锁 + 预减库存)
              ├── ① Redis 原子操作 (判断资格+扣库存)
              ├── ② 发送"秒杀成功"消息到RocketMQ
              └── ③ 返回"正在排队中"提示
         → 订单服务 (监听MQ, 异步创建订单)
              └── ④ 查询用户地址/优惠信息 → 写MySQL订单表
         → 通知服务 (SMS/邮件/站内信)

核心代码实现

数据库表设计(简化版)

-- 商品表
CREATE TABLE `seckill_product` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `name` VARCHAR(100),
    `stock` INT NOT NULL,            -- 总库存
    `sold` INT DEFAULT 0,            -- 已售数量(冗余字段,便于展示)
    `version` INT DEFAULT 0,         -- 乐观锁版本号
    `start_time` DATETIME,           -- 秒杀开始时间
    `end_time` DATETIME,             -- 秒杀结束时间
    `status` TINYINT DEFAULT 1,      -- 1=上架 0=下架
    INDEX idx_status (status)
);
-- 秒杀订单表
CREATE TABLE `seckill_order` (
    `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
    `product_id` BIGINT NOT NULL,
    `user_id` BIGINT NOT NULL,
    `order_no` VARCHAR(32) UNIQUE,   -- 全局唯一订单号
    `status` TINYINT DEFAULT 0,      -- 0=待支付 1=已支付 2=已取消
    `create_time` DATETIME,
    UNIQUE KEY uk_product_user (product_id, user_id)  -- 唯一约束防重复购买
);

Redis + Lua脚本实现原子扣减库存

为什么不直接用JVM锁或数据库锁?
单机JVM锁无法跨节点,数据库行锁在10万QPS下会击穿连接池,使用Redis单线程特性+Lua原子性,是最稳妥方案。

/** Redis秒杀脚本 */
public class SeckillScript {
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT = new DefaultRedisScript<>();
        SECKILL_SCRIPT.setResultType(Long.class);
        SECKILL_SCRIPT.setScriptText(
            "local stock = redis.call('get', KEYS[1]) " +         // 检查库存KEYS[1]: seckill:stock:{productId}
            "if (tonumber(stock) <= 0) then return -1 end " +      // 库存不足,返回-1
            "local isMember = redis.call('sismember', KEYS[2], ARGV[1]) " +  // 检查用户是否已参与 KEYS[2]: seckill:users:{productId}
            "if (isMember == 1) then return 0 end " +              // 重复购买,返回0
            "redis.call('decrby', KEYS[1], 1) " +                  // 扣减库存
            "redis.call('sadd', KEYS[2], ARGV[1]) " +              // 记录用户ID
            "return 1"
        );
    }
    public static long trySeckill(RedisTemplate redisTemplate, Long productId, Long userId) {
        List<String> keys = Arrays.asList(
            "seckill:stock:" + productId,
            "seckill:users:" + productId
        );
        Long result = redisTemplate.execute(SECKILL_SCRIPT, keys, userId.toString());
        return result == null ? -1 : result;
    }
}

Spring Boot 秒杀接口实现

@RestController
@RequestMapping("/api/seckill")
@Slf4j
public class SeckillController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private SeckillProductMapper productMapper;
    /**
     * 秒杀接口
     */
    @PostMapping("/doSeckill")
    public Result<String> doSeckill(@RequestParam("productId") Long productId,
                                     @RequestParam("userId") Long userId) {
        // 0. 提前在Nginx层做了限流(漏桶算法,每用户每秒最多3次请求)
        // 1. 查询秒杀商品是否存在且时间是否合法
        SeckillProduct product = getFromCache(productId);
        if (product == null || product.getStatus() != 1) {
            return Result.error("商品不存在或已下架");
        }
        LocalDateTime now = LocalDateTime.now();
        if (now.isBefore(product.getStartTime())) {
            return Result.error("秒杀尚未开始");
        }
        if (now.isAfter(product.getEndTime())) {
            return Result.error("秒杀已结束");
        }
        // 2. 执行Redis Lua脚本,原子性判断+扣库存
        long flag = SeckillScript.trySeckill(stringRedisTemplate, productId, userId);
        switch ((int) flag) {
            case -1:
                return Result.error("抱歉,商品已被抢光");
            case 0:
                return Result.error("您已抢购过此商品,请勿重复操作");
            case 1:
                // 3. 发送秒杀成功消息到MQ队列(异步生成订单)
                //    Topic: SECKILL_ORDER_TOPIC   Message: {"productId":xxx, "userId":xxx}
                SeckillMessage msg = new SeckillMessage(productId, userId);
                rocketMQTemplate.convertAndSend("seckill-success-topic", msg);
                log.info("[秒杀成功] userId={}, productId={}", userId, productId);
                // 4. 返回排队信息(前端展示"正在排队,请稍后查看订单")
                return Result.success("秒杀成功!订单已生成,请稍后查看");
            default:
                return Result.error("系统繁忙,请稍后重试");
        }
    }
    /** 从缓存查询商品(热点数据缓存到Redis) */
    private SeckillProduct getFromCache(Long productId) {
        String key = "seckill:product:" + productId;
        String json = stringRedisTemplate.opsForValue().get(key);
        if (json != null) return JSON.parseObject(json, SeckillProduct.class);
        // 缓存未命中 → 查询DB(这里应用了缓存穿透防护:布隆过滤器)
        SeckillProduct product = productMapper.selectById(productId);
        if (product != null) {
            stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
        }
        return product;
    }
}

MQ消费者——异步创建订单

@Component
@Slf4j
public class SeckillOrderConsumer {
    @Autowired
    private SeckillOrderMapper orderMapper;
    @Autowired
    private SeckillProductMapper productMapper;
    // 监听秒杀成功消息,可靠创建订单
    @RocketMQMessageListener(topic = "seckill-success-topic", consumerGroup = "seckill-order-group")
    public class SeckillListener implements RocketMQListener<SeckillMessage> {
        @Override
        public void onMessage(SeckillMessage message) {
            Long productId = message.getProductId();
            Long userId = message.getUserId();
            // 防止重复处理(幂等:用唯一索引+insert ignore)
            SeckillOrder order = new SeckillOrder();
            order.setProductId(productId);
            order.setUserId(userId);
            order.setOrderNo(generateOrderNo()); // 雪花算法生成唯一ID
            order.setStatus(0);
            try {
                // 数据库写唯一索引,若插入失败说明已存在订单 → 幂等
                orderMapper.insert(order);
                // 更新MySQL真实库存(可在定时任务中批量刷数据,不必每次更新)
                // productMapper.deduceStock(productId, 1);
                // 发送短信/邮件通知
                sendNotify(userId, productId);
                log.info("[订单创建成功] orderNo={}, userId={}", order.getOrderNo(), userId);
            } catch (DuplicateKeyException e) {
                log.warn("[订单重复] 已存在订单,跳过. userId={}, productId={}", userId, productId);
            }
        }
    }
}

网关限流配置(Spring Cloud Gateway)

spring:
  cloud:
    gateway:
      routes:
        - id: seckill_route
          uri: lb://seckill-service
          predicates:
            - Path=/api/seckill/**
          filters:
            # 令牌桶限流:每秒5个令牌,桶容量10
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 5000   # 每秒放行5000个请求
                redis-rate-limiter.burstCapacity: 10000   # 桶容量(突发流量)
                key-resolver: "#{@userKeyResolver}"       # 以用户ID限流(或IP)

缓存预热与库存预加载(启动时自动执行)

@Component
public class CachePreheatRunner implements CommandLineRunner {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private SeckillProductMapper productMapper;
    @Override
    public void run(String... args) {
        // 预热所有有效的秒杀商品到Redis
        List<SeckillProduct> list = productMapper.selectList(
            new LambdaQueryWrapper<SeckillProduct>()
                .eq(SeckillProduct::getStatus, 1));
        for (SeckillProduct product : list) {
            String stockKey = "seckill:stock:" + product.getId();
            stringRedisTemplate.opsForValue().set(stockKey, product.getStock().toString());
            // 预加载商品详细信息
            stringRedisTemplate.opsForValue().set(
                "seckill:product:" + product.getId(), 
                JSON.toJSONString(product), 
                30, TimeUnit.MINUTES);
        }
        log.info("秒杀商品缓存预热完成,共 {} 件", list.size());
    }
}

高并发下的问题与解决方案(面试高频点)

问题 解决方案
超卖问题 Redis + Lua 原子操作,杜绝并发扣减超额
重复购买 Redis Set记录用户 + 数据库唯一索引双重保障
缓存穿透 布隆过滤器提前拦截不存在的key
缓存击穿 互斥锁更新缓存(分布式锁),热点数据永久不过期
缓存雪崩 过期时间+随机值,多级缓存(Caffeine本地缓存)
接口防刷 网关令牌桶限流 + Redis计数器(单用户每分钟限制5次)
库存存储 Redis预减库存,MySQL用乐观锁兜底(update ... where stock >= 1)
消息幂等 RocketMQ每条消息带唯一ID,数据库唯一索引防重

压测结果与性能优化

JMeter 压测报告

测试规模:10万用户同时并发
服务器:4核8G × 3台
处理结果:
- QPS: 8,200
- RT平均: 78ms
- 秒杀成功率: 99.97%(失败均为库存不足,无技术异常)
- 库存准确率: 100%(1000件库存卖完即止,无超卖)
- 系统稳定性:无宕机,无OOM

优化点总结

  1. 使用 Caffeine 本地缓存热点商品,减少Redis压力
  2. 秒杀接口牺牲一定一致性,先回成功再去异步处理订单
  3. 使用 自定义线程池 处理MQ消息,限制消费速度防止数据库压力过大
  4. 根据业务特点,将商品库存预热到Redis,数据库查询降至最低

部署运维

架构部署图:
用户 → CDN → SLB (阿里云负载均衡) 
             → Nginx × 2
             → Gateway × 2
             → 秒杀服务 × 3
             → RocketMQ (4节点)
             → MySQL (主从+读写分离)
             → Redis Cluster (3主3从)

Docker Compose 关键服务定义

version: '3.8'
services:
  redis:
    image: redis:7.0-alpine
    container_name: redis-seckill
    command: redis-server --appendonly yes --cluster-enabled yes
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
  seckill-service:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - REDIS_HOST=redis
      - ROCKETMQ_NAME_SERVER=rocketmq:9876
    depends_on:
      - redis
      - rocketmq

总结与心得

这个案例完整展现了Java后端开发中高并发处理的核心技能点,在实际开发中,还涉及:

  1. 分布式事务(TCC/SAGA解决订单与库存的最终一致性)
  2. 容灾降级(Sentinel熔断,返回"排队人数过多"友好提示)
  3. 可观测性(全链路日志 + 监控大盘 + 实时告警)

如果你需要我详细展开某一部分(比如分布式锁实现细节幂等性设计消息队列参数调优等),或者需要完整代码仓库结构,欢迎告诉我具体需求。

抱歉,评论功能暂时关闭!