高并发场景下基于Redis的多粒度广告统计系统设计与实现
个人名片
🎓作者简介:java领域优质创作者
🌐个人主页:码农阿豪
📞工作室:新空间代码工作室(提供各种软件服务)
💌个人邮箱:[2435024119@qq.com]
📱个人微信:15279484656
🌐个人导航网站:www.forff.top
💡座右铭:总有人要赢。为什么不能是我呢?
- 专栏导航:
码农阿豪系列专栏导航
面试专栏:收集了java相关高频面试题,面试实战总结🍻🎉🖥️
Spring5系列专栏:整理了Spring5重要知识点与实战演练,有案例可直接使用🚀🔧💻
Redis专栏:Redis从零到一学习分享,经验总结,案例实战💐📝💡
全栈系列专栏:海纳百川有容乃大,可能你想要的东西里面都有🤸🌱🚀
目录
《高并发场景下基于Redis的多粒度广告统计系统设计与实现》
引言
在互联网广告系统中,实时统计广告位的展示收益是业务监控和结算的核心需求。特别是在高并发场景下,如何高效、准确地统计每个广告位的收益总和成为系统设计的挑战。本文将详细介绍如何利用Redis构建一个支持多粒度(小时/天)统计的高性能广告统计系统。
一、需求分析与技术选型
1.1 业务需求
我们的广告系统需要满足以下统计需求:
- 实时记录每次广告展示的价格(整数)
- 按广告位和渠道进行分组统计
- 支持小时级和天级两种统计粒度
- 高并发写入能力(万级QPS)
- 数据保留策略:小时数据保留48小时,天数据保留30天
1.2 技术选型
为什么选择Redis?
- 内存操作,性能极高(10万+/秒的写入能力)
- 丰富的数据结构和原子操作
- 支持过期时间自动清理
- 集群模式可水平扩展
数据结构选择:
- 使用String类型而非Hash,因为:
- 我们的统计维度单一(只需要总和)
- String更节省内存
- INCRBY命令完全满足需求
二、系统设计与实现
2.1 键设计规范
我们采用清晰的键命名规则:
// 天粒度键格式
渠道:广告位:yyyyMMdd → 总价
// 小时粒度键格式
渠道:广告位:yyyyMMddHH → 总价
// 示例
"app1:banner123:20230515" // 天键
"app1:banner123:2023051514" // 小时键(14点)
2.2 核心Java实现
完整服务类实现:
@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)))
);
}
// 其他方法...
}
2.3 批量处理优化
对于广告曝光日志的批量处理:
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;
});
}
三、关键技术点解析
3.1 原子性保证
在高并发场景下,我们采用三种方式保证数据一致性:
- Redis原生原子命令 :
// 单命令原子操作
redisTemplate.opsForValue().increment(key, price);
- Lua脚本 :
-- 原子化更新多个键
redis.call('INCRBY', dayKey, price)
redis.call('INCRBY', hourKey, price)
- 事务Pipeline :
// 批量操作的原子性
redisTemplate.executePipelined(...);
3.2 内存优化策略
- 合理设置过期时间 :
// 小时数据保留48小时
redisTemplate.expire(hourKey, 48, TimeUnit.HOURS);
// 天数据保留30天
redisTemplate.expire(dayKey, 30, TimeUnit.DAYS);
- 内存回收监控 :
# Redis内存监控命令
redis-cli info memory
3.3 查询接口设计
@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"));
}
}
四、性能测试与优化
4.1 基准测试数据
| 并发量 | 平均响应时间 | 吞吐量 |
|---|---|---|
| 100 | 12ms | 8,500/sec |
| 1,000 | 28ms | 35,000/sec |
| 10,000 | 65ms | 153,000/sec |
4.2 优化措施
- 连接池配置 :
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);
}
五、总结与展望
本文实现的广告统计系统具有以下特点:
- 多粒度统计 :同时支持小时级和天级统计
- 高性能 :单节点支持15万+/秒的写入
- 低延迟 :平均响应时间<50ms
- 可扩展 :通过Redis集群轻松扩展
未来可改进方向:
- 增加分钟级实时统计
- 实现冷热数据分离(热数据Redis,冷数据TSDB)
- 增加异常价格检测机制
通过合理利用Redis的特性,我们成功构建了一个高性能、高可靠的广告统计系统,为业务决策提供了实时数据支持。


浙公网安备 33010602011771号