如何设计一个能抗住万级并发的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:数据库连接池耗尽,服务不可用
高并发设计原则
高并发接口设计的核心思想是:尽量减少对核心资源的依赖,把非必要操作异步化或前置化。
具体来说:
- 能用缓存就不用数据库
- 能本地缓存就不读 Redis
- 能异步就不同步
- 能批量就不单个处理
分层缓存策略
这是最关键的一步。商品数据的特点是:读多写少,变化不频繁。非常适合缓存。
第一层:本地缓存
热点商品的数据可以直接放在 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 缓存要注意的问题:
- 序列化方式:JSON 序列化比 Java 原生序列化更快、更安全
- Key 设计:要有清晰的命名空间,便于管理和排查
- 过期时间:读多写少的数据可以设长一点
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%
最终效果
分层缓存 + 限流 + 异步化之后:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 单机 QPS | 200 | 3000 |
| TP99 | 3000ms | 35ms |
| 错误率 | 30% | 0.01% |
| 数据库 QPS | 5000 | 50 |
总结
高并发接口设计要点:
- 分层缓存:本地缓存 → Redis → 数据库,按需逐层获取
- 连接池调优:合理配置数据库和 Redis 连接池参数
- 限流保护:计数器或令牌桶算法,防止突发流量打垮系统
- 异步化:非核心操作异步执行,减少主流程延迟
- 降级熔断:下游不可用时,有兜底方案
万级并发不是一台服务器扛住,而是把流量分层消化,每一层都做好防护。
