如何设计一个能抗住万级并发的RESTful接口

如何设计一个能抗住万级并发的RESTful接口

做过电商大促的人都知道,零点下单高峰时,一个接口能打死一台服务器。不是服务器性能差,而是接口设计没考虑高并发场景。

拿商品查询接口举例,高峰期 QPS 能到 5000+,如果每个请求都查库,数据库直接被打爆。

问题场景

商品详情接口的原始实现:

@GetMapping("/product/{id}")
public ProductVO getProduct(@PathVariable Long id) {
    // 每个请求都查数据库
    Product product = productDao.findById(id);
    return convertToVO(product);
}

单机压测结果:

  • 并发 100:TP99 120ms,正常
  • 并发 500:TP99 800ms,开始超时
  • 并发 1000:TP99 3000ms,大量超时
  • 并发 2000:数据库连接池耗尽,服务不可用

高并发设计原则

高并发接口设计的核心思想是:尽量减少对核心资源的依赖,把非必要操作异步化或前置化

具体来说:

  1. 能用缓存就不用数据库
  2. 能本地缓存就不读 Redis
  3. 能异步就不同步
  4. 能批量就不单个处理

分层缓存策略

这是最关键的一步。商品数据的特点是:读多写少,变化不频繁。非常适合缓存。

第一层:本地缓存

热点商品的数据可以直接放在 JVM 内存里,避免每次请求都访问 Redis:

@Service
public class ProductService {

    // 使用 Caffeine 作为本地缓存
    private final Cache<Long, ProductVO> localCache = Caffeine.newBuilder()
        .maximumSize(10000)           // 最多缓存1万个商品
        .expireAfterWrite(1, TimeUnit.MINUTES)  // 1分钟后过期
        .recordStats()                // 记录命中率
        .build();

    @Autowired
    private ProductDao productDao;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String REDIS_KEY_PREFIX = "product:";

    public ProductVO getProduct(Long productId) {
        // 1. 先查本地缓存
        ProductVO product = localCache.getIfPresent(productId);
        if (product != null) {
            return product;
        }

        // 2. 本地缓存没命中,查Redis
        product = getFromRedis(productId);
        if (product != null) {
            localCache.put(productId, product);
            return product;
        }

        // 3. Redis也没命中,查数据库
        product = getFromDatabase(productId);
        if (product != null) {
            // 回填Redis
            redisTemplate.opsForValue().set(
                REDIS_KEY_PREFIX + productId,
                product,
                10, TimeUnit.MINUTES
            );
            localCache.put(productId, product);
        }

        return product;
    }
}

第二层:Redis 缓存

本地缓存容量有限,且各节点独立。对于通用数据,Redis 是更好的选择。

Redis 缓存要注意的问题:

  1. 序列化方式:JSON 序列化比 Java 原生序列化更快、更安全
  2. Key 设计:要有清晰的命名空间,便于管理和排查
  3. 过期时间:读多写少的数据可以设长一点
private ProductVO getFromRedis(Long productId) {
    String key = REDIS_KEY_PREFIX + productId;
    String cached = redisTemplate.opsForValue().get(key);
    if (cached != null) {
        return JSON.parseObject(cached, ProductVO.class);
    }
    return null;
}

第三层:数据库

数据库是最后的防线,能不到就不到。

对于商品表,查询优化:

-- 确保 id 有索引
ALTER TABLE products ADD INDEX idx_id (id);

-- 对于分页查询,确保排序字段有索引
ALTER TABLE products ADD INDEX idx_category_time (category_id, create_time DESC);

连接池优化

高并发场景下,连接池配置至关重要。

数据库连接池

HikariCP 配置:

spring:
  datasource:
    hikari:
      minimum-idle: 10        # 最小空闲连接
      maximum-pool-size: 50   # 最大连接数
      connection-timeout: 3000   # 获取连接超时3秒
      idle-timeout: 600000    # 空闲超时10分钟
      max-lifetime: 1800000   # 连接最大生命周期30分钟
      pool-name: ProductHikariPool

Redis 连接池

spring:
  redis:
    lettuce:
      pool:
        max-active: 50      # 最大连接数
        max-idle: 20        # 最大空闲连接
        min-idle: 10        # 最小空闲连接
        max-wait: 3000      # 获取连接等待时间

请求限流

系统扛不住时,需要限流保护。常用算法:

计数器限流

最简单的限流,用 AtomicInteger:

@Component
public class RateLimiter {

    private final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
    private final int maxRequests;
    private final Duration window;

    public RateLimiter(int maxRequests, Duration window) {
        this.maxRequests = maxRequests;
        this.window = window;
    }

    public boolean tryAcquire(String key) {
        AtomicInteger counter = counters.computeIfAbsent(key,
            k -> new AtomicInteger(0));

        int current = counter.incrementAndGet();
        if (current > maxRequests) {
            return false;
        }

        // 窗口过期后重置
        if (current == 1) {
            ((Supplier<?>) () -> {
                counter.set(0);
                return null;
            }).get();
            // 实际应该用定时器,这里简化
        }

        return true;
    }
}

令牌桶限流

更平滑的限流,用 Guava 的 RateLimiter:

@Service
public class ProductService {

    // 每秒产生100个令牌
    private final RateLimiter rateLimiter = RateLimiter.create(100);

    public ProductVO getProduct(Long productId) {
        // 获取令牌,没有则等待
        rateLimiter.acquire();

        return doGetProduct(productId);
    }
}

Sentinel 限流

生产环境推荐用 Sentinel,功能更完善:

@GetMapping("/product/{id}")
@SentinelResource(value = "getProduct",
    blockHandler = "getProductBlocked",
    fallback = "getProductFallback")
public ProductVO getProduct(@PathVariable Long id) {
    return productService.getProduct(id);
}

public ProductVO getProductBlocked(Long id, BlockException ex) {
    return ProductVO.builder()
        .id(id)
        .name("系统繁忙,请稍后再试")
        .build();
}

public ProductVO getProductFallback(Long id) {
    // 降级逻辑:返回默认数据或友好提示
    return ProductVO.defaultValue(id);
}

预热与拉取

大促前,提前把热点数据加载到缓存里:

@Component
public class ProductCacheWarmup {

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductDao productDao;

    @PostConstruct
    public void warmup() {
        // 活动开始前1分钟,预加载热点商品
        List<Long> hotProductIds = getHotProductIds();

        for (Long productId : hotProductIds) {
            productService.getProduct(productId);
        }

        log.info("商品缓存预热完成,共加载{}个商品", hotProductIds.size());
    }

    private List<Long> getHotProductIds() {
        // 从配置中心或数据库获取热点商品ID
        // 这里简化处理
        return productDao.findHotProductIds(1000);
    }
}

异步化

有些非核心操作可以异步化,不阻塞主流程:

@Service
public class ProductService {

    @Autowired
    private ProductViewLogDao viewLogDao;

    public ProductVO getProduct(Long productId) {
        ProductVO product = loadProduct(productId);

        // 记录浏览量,异步处理
        CompletableFuture.runAsync(() -> {
            viewLogDao.incrementViewCount(productId);
        });

        return product;
    }
}

降级与熔断

依赖的下游服务不可用时,需要降级:

@Service
public class ProductService {

    @Autowired
    private PriceService priceService;

    @Autowired
    private Sentinel sentinel;

    public ProductVO getProduct(Long productId) {
        ProductVO product = loadProduct(productId);

        // 价格信息降级:如果服务不可用,返回"价格获取中"
        try {
            BigDecimal price = sentinel.execute(() ->
                priceService.getPrice(productId)
            );
            product.setPrice(price);
        } catch (BlockException e) {
            product.setPriceText("价格获取中");
            product.setPriceStatus("UNAVAILABLE");
        }

        return product;
    }
}

压测验证

优化完成后,用 JMeter 压测验证:

# JMeter 命令行压测
./jmeter -n -t product-api-test.jmx -l result.jtl -e -o report

关键指标:

  • TP99 < 50ms
  • 错误率 < 0.1%
  • 系统 CPU < 70%
  • 数据库连接池使用率 < 80%

最终效果

分层缓存 + 限流 + 异步化之后:

指标优化前优化后
单机 QPS2003000
TP993000ms35ms
错误率30%0.01%
数据库 QPS500050

总结

高并发接口设计要点:

  1. 分层缓存:本地缓存 → Redis → 数据库,按需逐层获取
  2. 连接池调优:合理配置数据库和 Redis 连接池参数
  3. 限流保护:计数器或令牌桶算法,防止突发流量打垮系统
  4. 异步化:非核心操作异步执行,减少主流程延迟
  5. 降级熔断:下游不可用时,有兜底方案

万级并发不是一台服务器扛住,而是把流量分层消化,每一层都做好防护。

最后更新 4/20/2026, 4:48:48 AM