共计 2204 个字符,预计需要花费 6 分钟才能阅读完成。
背景痛点
在广告投放系统中,s 参数(如用户标签、竞价策略等)是决定广告展示逻辑的核心参数。传统的同步验证方式在高并发场景下会暴露三个典型问题:

- 响应延迟 :每次请求都需要实时计算和验证 s 参数,当 QPS 超过 5 万时,数据库查询成为瓶颈
- 数据不一致 :多服务器节点间缺乏参数同步机制,导致同一用户在不同节点获得不同广告策略
- 系统脆弱性 :某个参数计算服务异常会导致整个广告请求失败,影响系统可用性
技术选型
我们对比了三种主流方案:
- 同步验证 :
- 优点:实现简单,强一致性
-
缺点:数据库压力大,响应时间随流量线性增长
-
异步队列 :
- 优点:削峰填谷,系统解耦
-
缺点:实时性差(延迟通常在 200-500ms),需要维护消息队列
-
分布式缓存 + 异步更新 :
- 优点:毫秒级响应,支持 10 万 + QPS
- 缺点:存在短暂数据不一致窗口
最终选择方案三,因其在广告业务中:
- 允许短暂(<1s)的数据不一致
- 90% 的 s 参数更新频率低于 1 次 / 分钟
- 通过 TTL 机制保证最终一致性
核心实现
架构设计
graph LR
A[API Gateway] --> B[Cache Layer]
B -->|Cache Hit| C[Return Ads]
B -->|Cache Miss| D[Async Worker]
D --> E[Parameter Service]
D --> F[Update Cache]
Java 代码示例(Spring Boot)
// 缓存服务核心逻辑
@Slf4j
@Service
public class SParamCacheService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 缓存 TTL 设置为 5 分钟
private static final long CACHE_TTL = TimeUnit.MINUTES.toSeconds(5);
/**
* 获取带缓存的 s 参数
* @param userId 用户 ID
* @return 参数对象(可能为旧数据)*/
public SParam getWithCache(String userId) {
String cacheKey = "sparam:" + userId;
try {
// 先查缓存
SParam cached = (SParam) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {return cached;}
// 触发异步更新(非阻塞)CompletableFuture.runAsync(() -> refreshParam(userId));
// 返回兜底数据
return getFallbackParam(userId);
} catch (Exception e) {log.error("Cache query failed", e);
return getFallbackParam(userId);
}
}
// 异步刷新缓存
@Async
public void refreshParam(String userId) {
String lockKey = "lock:sparam:" + userId;
try {
// 获取分布式锁(防止缓存击穿)if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)) {SParam freshParam = paramService.calculateParam(userId);
redisTemplate.opsForValue().set(
"sparam:" + userId,
freshParam,
CACHE_TTL,
TimeUnit.SECONDS
);
}
} finally {redisTemplate.delete(lockKey);
}
}
}
性能优化
通过 JMeter 压测对比(8 核 16G 服务器集群):
| 方案 | 平均响应时间 | 99 分位延迟 | 最大 QPS |
|---|---|---|---|
| 直接查数据库 | 78ms | 210ms | 12,000 |
| 纯缓存 | 3ms | 8ms | 85,000 |
| 缓存 + 异步更新 | 5ms | 15ms | 72,000 |
优化策略:
- 缓存预热 :
- 每日凌晨加载活跃用户的 s 参数
-
热点用户(VIP)永久缓存
-
批量处理 :
# Python 批量更新示例 def batch_refresh(user_ids): params = param_service.batch_calculate(user_ids) # 批量计算 pipeline = redis.pipeline() for user_id, param in zip(user_ids, params): pipeline.set(f"sparam:{user_id}", param, ex=300) pipeline.execute()
避坑指南
- 缓存雪崩 :
-
解决方案:
- 差异化 TTL(基础值±随机偏移)
- 二级缓存(本地缓存 +Redis)
-
数据一致性 :
-
解决方案:
- 版本号机制(每次更新递增版本)
- 数据库 binlog 监听(用于关键参数)
-
热点 Key 问题 :
- 现象:某明星广告导致特定 s 参数访问量暴增
- 解决方案:
- 本地缓存
- 请求合并(如 1 秒内相同请求只计算一次)
总结与思考
当前方案在千万级日活的广告系统中稳定运行,但仍有改进空间:
- 实时性提升 :对重要参数可采用推模式(WebSocket 推送更新)
- 成本优化 :冷用户参数转存到更廉价的存储介质
- 智能化 :基于历史访问模式动态调整缓存策略
建议根据业务特点做针对性调整,例如:
- 游戏广告需要更高实时性
- 电商广告可以接受更长缓存时间
- 品牌广告对一致性要求较低
正文完
