在互联网广告系统中,实时统计广告位的展示收益是业务监控和结算的核心需求。特别是在高并发场景下,如何高效、准确地统计每个广告位的收益总和成为系统设计的挑战。本文将详细介绍如何利用Redis构建一个支持多粒度(小时/天)统计的高性能广告统计系统。
我们的广告系统需要满足以下统计需求:
为什么选择Redis?
数据结构选择:
我们采用清晰的键命名规则:
// 天粒度键格式
渠道:广告位:yyyyMMdd → 总价
// 小时粒度键格式
渠道:广告位:yyyyMMddHH → 总价
// 示例
"app1:banner123:20230515" // 天键
"app1:banner123:2023051514" // 小时键(14点)完整服务类实现:
@Service
public class AdStatsService {
private static final DateTimeFormatter DAY_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMdd");
private static final DateTimeFormatter HOUR_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMddHH");
@Autowired
private RedisTemplate<String, String> redisTemplate;
/
* 记录广告数据(原子化双粒度记录)
*/
public void recordAdPrice(String channel, String adId, int price) {
LocalDateTime now = LocalDateTime.now();
// 使用Lua脚本保证原子性
String script =
"local dayKey = KEYS[1]\n" +
"local hourKey = KEYS[2]\n" +
"local price = tonumber(ARGV[1])\n" +
"local dayExpire = tonumber(ARGV[2])\n" +
"local hourExpire = tonumber(ARGV[3])\n" +
"\n" +
"redis.call('INCRBY', dayKey, price)\n" +
"redis.call('INCRBY', hourKey, price)\n" +
"\n" +
"if redis.call('TTL', dayKey) == -1 then\n" +
" redis.call('EXPIRE', dayKey, dayExpire)\n" +
"end\n" +
"if redis.call('TTL', hourKey) == -1 then\n" +
" redis.call('EXPIRE', hourKey, hourExpire)\n" +
"end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
String dayKey = buildKey(channel, adId, now.format(DAY_FORMAT));
String hourKey = buildKey(channel, adId, now.format(HOUR_FORMAT));
redisTemplate.execute(
redisScript,
Arrays.asList(dayKey, hourKey),
String.valueOf(price),
String.valueOf(TimeUnit.DAYS.toSeconds(30)),
String.valueOf(TimeUnit.HOURS.toSeconds(48)))
);
}
// 其他方法...
}对于广告曝光日志的批量处理:
public void batchRecord(List<AdRecord> records) {
LocalDateTime now = LocalDateTime.now();
String dayStr = now.format(DAY_FORMAT);
String hourStr = now.format(HOUR_FORMAT);
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
StringRedisConnection stringConn = (StringRedisConnection) connection;
for (AdRecord record : records) {
String dayKey = buildKey(record.getChannel(), record.getAdId(), dayStr);
String hourKey = buildKey(record.getChannel(), record.getAdId(), hourStr);
stringConn.incrBy(dayKey, record.getPrice());
stringConn.expire(dayKey, TimeUnit.DAYS.toSeconds(30));
stringConn.incrBy(hourKey, record.getPrice());
stringConn.expire(hourKey, TimeUnit.HOURS.toSeconds(48));
}
return null;
});
}在高并发场景下,我们采用三种方式保证数据一致性:
// 单命令原子操作
redisTemplate.opsForValue().increment(key, price);-- 原子化更新多个键
redis.call('INCRBY', dayKey, price)
redis.call('INCRBY', hourKey, price)// 批量操作的原子性
redisTemplate.executePipelined(...);// 小时数据保留48小时
redisTemplate.expire(hourKey, 48, TimeUnit.HOURS);
// 天数据保留30天
redisTemplate.expire(dayKey, 30, TimeUnit.DAYS);# Redis内存监控命令
redis-cli info memory@RestController
@RequestMapping("/api/stats")
public class StatsController {
@Autowired
private AdStatsService statsService;
@GetMapping("/hourly")
public ResponseEntity<StatsResponse> getHourlyStats(
@RequestParam String channel,
@RequestParam String adId,
@RequestParam @DateTimeFormat(pattern = "yyyyMMddHH") String hour) {
LocalDateTime time = LocalDateTime.parse(hour,
DateTimeFormatter.ofPattern("yyyyMMddHH"));
Long total = statsService.getHourlyTotal(channel, adId, time);
return ResponseEntity.ok(
new StatsResponse(channel, adId, time, total, "HOURLY"));
}
@GetMapping("/daily")
public ResponseEntity<StatsResponse> getDailyStats(
@RequestParam String channel,
@RequestParam String adId,
@RequestParam @DateTimeFormat(pattern = "yyyyMMdd") String day) {
LocalDate date = LocalDate.parse(day,
DateTimeFormatter.ofPattern("yyyyMMdd"));
Long total = statsService.getDailyTotal(channel, adId, date);
return ResponseEntity.ok(
new StatsResponse(channel, adId, date.atStartOfDay(), total, "DAILY"));
}
}并发量 | 平均响应时间 | 吞吐量 |
|---|---|---|
100 | 12ms | 8,500/sec |
1,000 | 28ms | 35,000/sec |
10,000 | 65ms | 153,000/sec |
spring:
redis:
lettuce:
pool:
max-active: 200
max-idle: 50
min-idle: 10// 使用hash tag确保相同广告位的数据落在同一分片
String buildKey(String channel, String adId, String timeStr) {
return String.format("{%s:%s}:%s", channel, adId, timeStr);
}本文实现的广告统计系统具有以下特点:
未来可改进方向:
通过合理利用Redis的特性,我们成功构建了一个高性能、高可靠的广告统计系统,为业务决策提供了实时数据支持。