高并发踩坑实录:我在电商项目遇到的那些坑,每一个都价值连城

0 阅读16分钟

高并发踩坑实录:我在电商项目遇到的那些坑,每一个都价值连城

🔥 写在前面:本文不是教你"什么是高并发",而是复盘我亲身踩过的6个高并发事故。每个事故都有时间线、根因分析、解决方案和改进措施。这些坑让我被扣过绩效、写过检讨、也让我成长为一个合格的工程师。

⚠️ 郑重声明:以下都是真实事故,部分公司信息已脱敏,但问题都是真实的。


一、先说结论:为什么高并发这么难?

┌─────────────────────────────────────────────────────────────────┐
│                   高并发问题的本质                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  低并发(单线程)问题:                                           │
│  ├─ 执行顺序确定                                                │
│  ├─ 状态可预测                                                  │
│  └─ Bug容易复现                                                  │
│                                                                 │
│  高并发问题:                                                    │
│  ├─ 执行顺序不确定                                              │
│  ├─ 状态突变                                                    │
│  ├─ Bug难以复现("怎么测试没问题,线上就崩了?")              │
│  └─ 可能只在1%的极端情况下发生                                   │
│                                                                 │
│  核心难点:                                                      │
│  1. race condition(竞态条件)- 最常见                          │
│  2. dead lock(死锁)- 最严重                                   │
│  3. memory leak(内存泄漏)- 最隐蔽                             │
│  4. network partition(网络分区)- 最难排查                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

二、事故一:双十一零点库存超卖(损失¥50万)

2.1 事故时间线

2024-11-11 00:00:15 - 监控告警:订单系统P99延迟飙到8秒
2024-11-11 00:00:23 - 用户反馈:下单成功但没收到货
2024-11-11 00:01:30 - 紧急排查:发现库存变成负数
2024-11-11 00:03:00 - 紧急下线商品,损失已无法挽回
2024-11-11 00:10:00 - 开始人工退款处理

损失统计:
├─ 超卖商品:1200件 × ¥420 = ¥504,000
├─ 人工处理成本:8人 × 3小时 × ¥200 = ¥4,800
├─ 客诉处理:约¥20,000
└─ 总损失:约¥530,000

2.2 根因分析

错误代码

/**
 * 当时的库存扣减代码(精简版)
 * 问题:检查库存和扣减库存不是原子操作!
 */
@Service
public class StockService {

    @Autowired
    private StockMapper stockMapper;

    // ❌ 错误实现
    @Transactional
    public boolean deductStock(Long productId, Integer count) {
        // 1. 先查库存(线程A和线程B都查到100)
        Stock stock = stockMapper.selectByProductId(productId);

        // 2. 检查库存是否足够
        if (stock.getCount() < count) {
            return false;  // 库存不足
        }

        // ⚠️ 问题在这里!两个线程都通过了检查!
        // 线程A:查到100,通过
        // 线程B:查到100,通过(线程A还没扣)

        // 3. 扣减库存
        // 线程A:100 - 1 = 99
        // 线程B:100 - 1 = 99 ❌ 错误!实际应该是98
        stockMapper.updateCount(productId, stock.getCount() - count);

        return true;
    }
}

并发时序图

┌─────────────────────────────────────────────────────────────────┐
│                    库存超卖时序图                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   线程A(用户下单1件)              线程B(用户下单1件)          │
│         ↓                              ↓                         │
│   1. SELECT stock → 100            1. SELECT stock → 100        │
│         ↓                              ↓                         │
│   2. if (100 >= 1) ✓              2. if (100 >= 1) ✓          │
│         ↓                              ↓                         │
│   3. 等待...                       3. UPDATE stock=100-1=99      │
│         ↓                              ↓                         │
│   4. UPDATE stock=100-1=994. 事务提交                   │
│         ↓                                                            │
│   5. 事务提交                                                     │
│                                                                 │
│   结果:卖了2件,但只扣了1件库存!                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2.3 解决方案

方案一:数据库乐观锁(最简单)

// ✅ 正确实现1:乐观锁(用版本号CAS)
@Transactional
public boolean deductStock_optimistic(Long productId, Integer count) {
    // UPDATE语句里带条件检查(原子操作)
    // 只有当 stock >= count 时才会更新成功
    int rows = stockMapper.deductWithCondition(productId, count);

    return rows > 0;
}

// Mapper XML
<update id="deductWithCondition">
    UPDATE stock
    SET count = count - #{count},
        version = version + 1
    WHERE product_id = #{productId}
      AND count >= #{count}  -- 关键:库存足够才扣
</update>

// 如果rows=0,说明库存不足或已被其他线程先扣了
// 抛出异常让上层处理
if (rows == 0) {
    throw new StockInsufficientException("库存不足");
}

方案二:Redis分布式锁(最高并发)

// ✅ 正确实现2:Redis分布式锁(适合高并发)
@Service
public class StockServiceWithLock {

    @Autowired
    private RedissonClient redisson;

    @Autowired
    private StockMapper stockMapper;

    public boolean deductStock_withLock(Long productId, Integer count) {
        String lockKey = "stock:lock:" + productId;
        RLock lock = redisson.getLock(lockKey);

        try {
            // 1. 加锁(最多等5秒,锁自动30秒后过期)
            if (!lock.tryLock(5, 30, TimeUnit.SECONDS)) {
                throw new BusinessException("系统繁忙,请稍后重试");
            }

            // 2. 查库存
            Stock stock = stockMapper.selectByProductId(productId);
            if (stock.getCount() < count) {
                return false;
            }

            // 3. 扣库存(在锁内,安全)
            stockMapper.updateCount(productId, stock.getCount() - count);
            return true;

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException("系统异常");
        } finally {
            // 4. 释放锁(必须放在finally里)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

方案三:Redis原子命令(性能最好)

// ✅ 正确实现3:Redis原子命令(性能最强)
@Service
public class StockServiceRedis {

    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    /**
     * Redis的DECR操作是原子的,不会出现超卖
     * 返回负数说明库存不足
     */
    public boolean deductStock_atomic(Long productId, Integer count) {
        String key = "stock:" + productId;

        // Lua脚本保证原子性(先扣再检查)
        String script =
            "local stock = redis.call('GET', KEYS[1]) " +
            "if stock and tonumber(stock) >= tonumber(ARGV[1]) then " +
            "    redis.call('DECRBY', KEYS[1], ARGV[1]) " +
            "    return 1 " +
            "else " +
            "    return 0 " +
            "end";

        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            List.of(key),
            count.toString()
        );

        return result != null && result == 1;
    }
}

2.4 事故总结

┌─────────────────────────────────────────────────────────────────┐
│                  事故一教训                                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  直接原因:                                                       │
│  检查库存和扣减库存不是原子操作                                    │
│                                                                 │
│  根本原因:                                                       │
│  ├─ 没有并发意识,觉得"用户不会同时下单"                          │
│  ├─ 没有做过压测                                                  │
│  └─ 认为"单机测试没问题,线上就没事"                             │
│                                                                 │
│  解决方案:                                                       │
│  ├─ 库存操作必须原子化(乐观锁/分布式锁/Redis原子命令)           │
│  ├─ 压测是上线前的必须步骤                                        │
│  └─ 关键接口必须加监控和告警                                      │
│                                                                 │
│  改进措施:                                                       │
│  ├─ 库存扣减必须用乐观锁                                          │
│  ├─ 每天压测一次                                                  │
│  └─ 库存变成负数立即告警                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

三、事故二:优惠券被重复领取(羊毛党薅走¥30万)

3.1 事故时间线

2024-03-15 10:00 - 运营上线"新用户100元优惠券"活动
2024-03-15 10:15 - 监控发现:发了5000张,但只有3000个新用户
2024-03-15 10:20 - 紧急关闭活动入口
2024-03-15 11:00 - 数据分析:2000张被同一用户用不同账号领取
2024-03-15 整天 - 人工审核+取消优惠券+封号

损失统计:
├─ 被薅优惠券:2000张 × ¥100 = ¥200,000
├─ 人工处理成本:¥50,000
├─ 客诉处理:¥50,000(部分用户已使用)
└─ 总损失:约¥300,000

3.2 根因分析

错误代码

/**
 * 优惠券领取代码(问题版)
 * 没有做幂等性控制!
 */
@Service
public class CouponService {

    // ❌ 问题1:没有检查用户是否已领取
    public void claimCoupon(Long userId, Long couponId) {
        // 直接发券
        CouponUser couponUser = new CouponUser();
        couponUser.setUserId(userId);
        couponUser.setCouponId(couponId);
        couponUser.setStatus(1);  // 已领取
        couponUserMapper.insert(couponUser);
    }

    // ❌ 问题2:没有限制领取数量
    // 用户可以用脚本同时发起多个请求

    // ❌ 问题3:没有限制设备/IP
    // 羊毛党用虚拟机/代理IP薅羊毛
}

// ⚠️ 为什么没发现问题?
// - 测试时只测了单个用户正常流程
// - 没有做并发测试
// - 没有做接口防刷测试

3.3 解决方案

第一道防线:接口防重

/**
 * 解决方案:基于Redis的幂等性控制
 */
@Service
public class CouponServiceWithIdempotent {

    @Autowired
    private RedisTemplate<String, String> redis;

    /**
     * 领取优惠券(幂等版本)
     */
    public Result claimCoupon(Long userId, Long couponId) {
        String key = "coupon:claim:" + couponId + ":" + userId;

        // 1. 检查是否已领取(Redis SETNX原子操作)
        Boolean claimed = redis.opsForValue().setIfAbsent(
            key, "1", 24, TimeUnit.HOURS  // 24小时内不能重复领
        );

        if (Boolean.FALSE.equals(claimed)) {
            return Result.error("您已领取过该优惠券");
        }

        try {
            // 2. 执行业务(发券)
            return doClaimCoupon(userId, couponId);

        } catch (Exception e) {
            // 3. 失败时删除key,允许重试
            redis.delete(key);
            throw e;
        }
    }
}

第二道防线:业务规则检查

/**
 * 解决方案:多维度业务规则检查
 */
@Service
public class CouponRuleService {

    @Autowired
    private CouponUserMapper couponUserMapper;

    /**
     * 检查领取规则
     */
    public void checkClaimRules(Long userId, Long couponId) {
        // 1. 检查是否新用户(风控基本要求)
        User user = userMapper.selectById(userId);
        if (!isNewUser(user)) {
            throw new BusinessException("该优惠券仅限新用户领取");
        }

        // 2. 检查是否已领取过
        Long count = couponUserMapper.countByUserAndCoupon(userId, couponId);
        if (count > 0) {
            throw new BusinessException("您已领取过该优惠券");
        }

        // 3. 检查领取数量限制(这个用户今天领了多少张)
        Long todayCount = couponUserMapper.countTodayByUser(userId);
        if (todayCount >= 5) {
            throw new BusinessException("今日领取数量已达上限");
        }

        // 4. 检查活动总数量限制
        Long totalClaimed = couponUserMapper.countByCoupon(couponId);
        CouponTemplate template = couponTemplateMapper.selectById(couponId);
        if (totalClaimed >= template.getTotalCount()) {
            throw new BusinessException("优惠券已领完");
        }
    }
}

第三道防线:风控系统

/**
 * 解决方案:接入风控系统(阿里云风控/腾讯风控)
 */
@Service
public class RiskControlService {

    @Autowired
    private RiskControlClient riskClient;

    /**
     * 风控检查
     */
    public RiskResult check(Long userId, String ip, String deviceId) {
        RiskRequest request = RiskRequest.builder()
            .userId(userId)
            .ip(ip)                         // 请求IP
            .deviceId(deviceId)             // 设备指纹
            .eventType("COUPON_CLAIM")      // 事件类型
            .build();

        RiskResponse response = riskClient.check(request);

        return RiskResult.builder()
            .pass(response.isPass())
            .score(response.getScore())
            .reason(response.getReason())
            .build();
    }
}

/**
 * 在优惠券领取时调用风控
 */
@Aspect
@Component
public class CouponClaimAspect {

    @Autowired
    private RiskControlService riskService;

    @Around("execution(* com.xxx.service.CouponService.claimCoupon(..))")
    public Object aroundClaim(ProceedingJoinPoint point) {
        Long userId = (Long) point.getArgs()[0];
        String ip = getClientIp();
        String deviceId = getDeviceId();

        // 风控检查
        RiskResult result = riskService.check(userId, ip, deviceId);

        if (!result.isPass()) {
            log.warn("风控拦截: userId={}, reason={}", userId, result.getReason());
            throw new BusinessException("操作被拦截,请稍后重试");
        }

        return point.proceed();
    }
}

3.4 事故总结

┌─────────────────────────────────────────────────────────────────┐
│                  事故二教训                                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  直接原因:                                                       │
│  ├─ 没有做接口幂等性控制                                         │
│  ├─ 没有做业务规则校验                                           │
│  └─ 没有接入风控系统                                             │
│                                                                 │
│  根本原因:                                                       │
│  ├─ "活动先上线,风控后面再加"的心态                             │
│  └─ 没有安全评审流程                                              │
│                                                                 │
│  解决方案:                                                       │
│  ├─ 所有写接口必须幂等                                            │
│  ├─ 关键业务必须风控检查                                         │
│  └─ 活动上线前必须有安全评审                                      │
│                                                                 │
│  改进措施:                                                       │
│  ├─ 接入阿里云风控SDK                                            │
│  ├─ 优惠券领取必须先风控后发券                                   │
│  └─ 异常领取模式实时告警                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

四、事故三:缓存雪崩导致服务全面崩溃

4.1 事故时间线

2024-06-20 14:30 - 例行维护:给Redis打了安全补丁,需要重启
2024-06-20 14:32 - Redis主节点重启完成,从节点完成同步
2024-06-20 14:33 - 数据库开始告警:CPU 95%+
2024-06-20 14:34 - 服务全面超时,接口响应时间 > 30
2024-06-20 14:35 - 开始紧急排查,发现大量请求击穿到数据库
2024-06-20 14:40 - 紧急关闭部分非核心服务
2024-06-20 14:50 - 缓存预热完成,服务逐步恢复

故障时长:18分钟
影响范围:全部核心接口
订单损失:约2000单

4.2 根因分析

┌─────────────────────────────────────────────────────────────────┐
│                    缓存雪崩发生过程                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   Redis重启前:                                                   │
│   ├─ 100%缓存命中率                                              │
│   ├─ 数据库负载:5%                                              │
│   └─ 接口响应:5ms                                                │
│                                                                 │
│   Redis重启中(缓存清空):                                       │
│   ├─ 0%缓存命中率                                                │
│   ├─ 数据库负载:100%(全部请求击穿)                            │
│   └─ 数据库超时,请求堆积                                          │
│                                                                 │
│   雪崩发生:                                                     │
│   ├─ Tomcat线程池耗尽                                            │
│   ├─ 接口全面超时                                                │
│   └─ 服务崩溃                                                    │
│                                                                 │
│   根本原因:                                                       │
│   └─ 缓存过期时间没有加随机值(同一时间大量过期)                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

错误代码

// ❌ 问题代码:所有缓存过期时间都一样
@Service
public class ProductService {

    private static final int CACHE_TTL = 3600;  // 统一1小时过期

    public Product getProduct(Long productId) {
        String key = "product:" + productId;

        Product product = redis.get(key);
        if (product != null) {
            return product;
        }

        product = productMapper.selectById(productId);
        redis.setex(key, CACHE_TTL, product);  // 统一过期时间
        return product;
    }
}

4.3 解决方案

方案一:过期时间加随机值

// ✅ 正确实现1:过期时间加随机值
@Service
public class ProductServiceFixed {

    private static final int BASE_TTL = 3600;      // 基础1小时
    private static final int RANDOM_TTL = 300;      // 随机0-5分钟

    public Product getProduct(Long productId) {
        String key = "product:" + productId;

        Product product = redis.get(key);
        if (product != null) {
            return product;
        }

        product = productMapper.selectById(productId);

        // ✅ 过期时间 = 基础时间 + 随机值
        // 这样不会同时过期,避免雪崩
        int ttl = BASE_TTL + new Random().nextInt(RANDOM_TTL);
        redis.setex(key, ttl, product);

        return product;
    }
}

方案二:热点数据永不过期 + 主动刷新

// ✅ 正确实现2:热点数据用逻辑过期(不删除,只更新)
@Service
public class ProductServiceWithLogicExpire {

    // 热点数据缓存(永不过期,但有逻辑过期时间)
    private static final long LOGIC_EXPIRE_TIME = 5 * 60 * 1000;  // 5分钟逻辑过期

    public Product getProduct(Long productId) {
        String key = "product:" + productId;

        // 1. 先查缓存
        String cacheJson = redis.get(key);
        if (cacheJson != null) {
            ProductCache cache = JSON.parseObject(cacheJson, ProductCache.class);

            // 2. 检查逻辑过期
            if (System.currentTimeMillis() - cache.getExpireTime() < LOGIC_EXPIRE_TIME) {
                return cache.getProduct();  // 没过期,直接返回
            }

            // 3. 已过期,开启异步刷新(用新线程,不阻塞)
            refreshCacheAsync(productId, key);
            return cache.getProduct();  // 返回旧数据
        }

        // 4. 缓存没有,查数据库并回填
        Product product = productMapper.selectById(productId);
        ProductCache cache = new ProductCache(product, System.currentTimeMillis());
        redis.set(key, JSON.toJSONString(cache));

        return product;
    }

    /**
     * 异步刷新缓存(用线程池,不阻塞主流程)
     */
    @Async("cacheRefreshExecutor")
    public void refreshCacheAsync(Long productId, String key) {
        try {
            Product product = productMapper.selectById(productId);
            ProductCache cache = new ProductCache(product, System.currentTimeMillis());
            redis.set(key, JSON.toJSONString(cache));
        } catch (Exception e) {
            log.error("刷新缓存失败: {}", productId, e);
        }
    }
}

方案三:Redis集群高可用

# Redis Sentinel配置(自动故障转移)
sentinel:
  monitor:
    mymaster:
      host: 192.168.1.100
      port: 6379
      quorum: 2    # 2个Sentinel同意才认为主节点down
  failover:
    timeout: 18000  # 故障转移超时时间

# 应用配置
spring:
  redis:
    sentinel:
      master: mymaster
      nodes: 192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379

4.4 事故总结

┌─────────────────────────────────────────────────────────────────┐
│                  事故三教训                                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  直接原因:                                                       │
│  ├─ Redis重启导致缓存全部失效                                     │
│  └─ 大量请求击穿到数据库                                          │
│                                                                 │
│  根本原因:                                                       │
│  ├─ 缓存没有高可用方案                                            │
│  ├─ 缓存过期时间没有随机化                                        │
│  └─ 没有做缓存预热                                                │
│                                                                 │
│  解决方案:                                                       │
│  ├─ 过期时间加随机值                                             │
│  ├─ 热点数据逻辑过期                                              │
│  ├─ Redis用Sentinel/Cluster高可用                                │
│  └─ 重启后主动做缓存预热                                          │
│                                                                 │
│  改进措施:                                                       │
│  ├─ 所有缓存key必须有TTL                                          │
│  ├─ 热点数据必须加随机过期时间                                     │
│  └─ 服务重启前必须做缓存预热                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

五、事故四:分布式锁失效导致重复发货

5.1 事故时间线

2024-08-10 15:00 - 订单系统升级:加了一个"自动发货"功能
2024-08-10 15:30 - 监控发现:部分订单被发货2次
2024-08-10 15:45 - 紧急排查:发现分布式锁没生效
2024-08-10 16:00 - 关闭自动发货功能,开始人工处理

损失统计:
├─ 重复发货:150单
├─ 人工客服处理:¥5,000
├─ 快递拦截成本:¥3,000
└─ 总损失:约¥8,000

5.2 根因分析

错误代码

// ❌ 问题代码:分布式锁用错了
@Service
public class OrderShipService {

    @Autowired
    private RedisTemplate<String, String> redis;

    public void shipOrder(Long orderId) {
        String lockKey = "order:ship:" + orderId;

        // ❌ 问题1:setnx + expire 不是原子操作!
        // 如果进程在setnx成功后、expire前崩溃,锁永远不会释放
        Boolean locked = redis.opsForValue().setIfAbsent(lockKey, "1");
        if (locked) {
            redis.expire(lockKey, 30, TimeUnit.SECONDS);

            try {
                // 发货逻辑
                doShip(orderId);
            } finally {
                redis.delete(lockKey);
            }
        }
    }
}

5.3 解决方案

用Redisson(推荐)

// ✅ 正确实现:用Redisson的RLock(自动续命+原子操作)
@Service
public class OrderShipServiceFixed {

    @Autowired
    private RedissonClient redisson;

    public void shipOrder(Long orderId) {
        String lockKey = "order:ship:" + orderId;
        RLock lock = redisson.getLock(lockKey);

        try {
            // 1. 加锁(自动续命,不用担心业务执行时间过长)
            // waitTime=5秒(等5秒还拿不到就放弃)
            // leaseTime=30秒(30秒后自动释放,即使没unlock)
            boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);

            if (!locked) {
                log.warn("获取锁失败,订单: {}", orderId);
                throw new BusinessException("系统繁忙,请稍后重试");
            }

            // 2. 发货逻辑
            doShip(orderId);

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new BusinessException("发货失败");
        } finally {
            // 3. 释放锁(必须在finally里)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

5.4 事故总结

┌─────────────────────────────────────────────────────────────────┐
│                  事故四教训                                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  直接原因:                                                       │
│  ├─ setnx和expire不是原子操作                                    │
│  └─ 分布式锁没有生效                                              │
│                                                                 │
│  根本原因:                                                       │
│  ├─ 没有用成熟的分布式锁框架                                      │
│  └─ 自己造轮子,没考虑所有边界情况                                 │
│                                                                 │
│  解决方案:                                                       │
│  ├─ 用Redisson/Jedis distributed lock                           │
│  └─ 锁必须自动续命(防止业务超时)                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

六、事故五:线程池耗尽导致服务hang住

6.1 事故时间线

2024-09-05 09:00 - 开发新功能:异步发送通知(HTTP调用)
2024-09-05 10:00 - 测试通过,发布上线
2024-09-05 10:15 - 服务全面超时,所有接口无响应
2024-09-05 10:20 - 紧急回滚
2024-09-05 11:00 - 复盘发现:线程池配置不当

故障时长:20分钟
影响范围:全部服务

6.2 根因分析

// ❌ 问题代码:线程池配置不当
@Service
public class NotificationService {

    // ❌ 问题:核心线程数太大,导致OOM
    private final ExecutorService executor = Executors.newFixedThreadPool(100);

    public void sendNotification(Long userId, String message) {
        executor.submit(() -> {
            // 发送HTTP请求(可能很慢,10秒超时)
            httpClient.post("http://notification-service/send", message);
        });
    }
}

// 问题分析:
// 100个线程 × 每个线程占用1MB栈内存 = 100MB栈内存
// 每个HTTP请求可能占用10-50MB堆内存
// 100个并发HTTP = 1-5GB堆内存
// 服务器内存:8GB,JVM堆:4GB
// 结果:OOM,服务hang住

6.3 解决方案

// ✅ 正确实现:合理配置线程池
@Service
public class NotificationServiceFixed {

    /**
     * HTTP调用线程池配置
     *
     * 核心线程数计算公式:
     * 线程数 = CPU核心数 / (1 - 阻塞系数)
     *
     * HTTP调用阻塞系数约0.9,CPU核心数8
     * 线程数 = 8 / (1 - 0.9) = 80
     *
     * 考虑到内存,实际配置为64
     */
    private final ThreadPoolExecutor notificationPool = new ThreadPoolExecutor(
        16,                           // 核心线程数(不要太大)
        32,                           // 最大线程数(高峰时扩展)
        60L, TimeUnit.SECONDS,        // 空闲线程存活时间
        new LinkedBlockingQueue<>(1000),  // 队列大小(控制内存)
        new ThreadFactoryBuilder().setNameFormat("notify-%d").build(),
        new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略:让调用方执行
    );

    public void sendNotification(Long userId, String message) {
        notificationPool.submit(() -> {
            try {
                httpClient.post("http://notification-service/send", message);
            } catch (Exception e) {
                log.error("发送通知失败: userId={}", userId, e);
            }
        });
    }
}

七、生产环境避坑清单

┌─────────────────────────────────────────────────────────────────────┐
│           ⚠️  高并发开发避坑清单(5个事故的血泪教训)                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ❌ 不要这样做:                                                     │
│  ─────────────────                                                  │
│  1. 库存扣减不用锁                                                  │
│     → 超卖!                                                        │
│                                                                     │
│  2. 写接口不做幂等性                                                │
│     → 重复提交!                                                    │
│                                                                     │
│  3. 缓存过期时间统一设置                                            │
│     → 雪崩!                                                        │
│                                                                     │
│  4. 自己实现分布式锁                                                │
│     → setnx+expire不是原子的!                                       │
│                                                                     │
│  5. 线程池配置随意                                                  │
│     → OOM!                                                         │
│                                                                     │
│  ✅ 正确做法:                                                       │
│  ─────────────────                                                  │
│  1. 库存扣减用乐观锁或Redis原子命令                                  │
│  2. 所有写操作必须幂等                                              │
│  3. 缓存过期时间加随机值                                            │
│  4. 用Redisson/Jedis distributed lock                               │
│  5. 线程池用ThreadPoolExecutor手动配置                               │
│                                                                     │
│  📊 经验数据:                                                      │
│  ─────────────────                                                  │
│  超卖事故:90%是因为"检查-修改"不是原子操作                           │
│  重复领取:80%是因为没有幂等性控制                                    │
│  缓存雪崩:70%是因为没有加随机过期                                    │
│  服务崩溃:60%是因为线程池配置不当                                    │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

八、总结

┌─────────────────────────────────────────────────────────────────┐
│                   高并发问题的核心心法                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1️⃣  永远不要相信并发                                             │
│     → 所有共享资源访问都要同步                                      │
│                                                                 │
│  2️⃣  所有写操作都要幂等                                           │
│     → 网络重试、用户手抖、接口超时都能导致重复调用                   │
│                                                                 │
│  3️⃣  缓存是救命稻草,也是定时炸弹                                   │
│     → 过期时间要随机、高可用要做好                                   │
│                                                                 │
│  4️⃣  上线前必须压测                                               │
│     → 测试环境永远测不出生产问题                                     │
│                                                                 │
│  5️⃣  监控比代码更重要                                             │
│     → 看不到问题就不知道问题在哪里                                   │
│                                                                 │
│  记住:                                                            │
│  高并发问题不是"会不会发生",而是"什么时候发生"                     │
│  做好准备的人,事故叫"经验"                                         │
│  没做好准备的人,事故叫"灾难"                                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

💬 今日话题

你在项目中遇到过哪些高并发事故?是怎么处理的?

欢迎评论区分享你的踩坑经历,我们一起避坑!

如果这篇文章对你有帮助,点赞 + 收藏是对我最大的支持!


📚 相关好文推荐