高并发场景下基于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 原子性保证

在高并发场景下,我们采用三种方式保证数据一致性:

  1. Redis原生原子命令 :
// 单命令原子操作
redisTemplate.opsForValue().increment(key, price);
  1. Lua脚本 :
-- 原子化更新多个键
redis.call('INCRBY', dayKey, price)
redis.call('INCRBY', hourKey, price)
  1. 事务Pipeline :
// 批量操作的原子性
redisTemplate.executePipelined(...);

3.2 内存优化策略

  1. 合理设置过期时间 :
// 小时数据保留48小时
redisTemplate.expire(hourKey, 48, TimeUnit.HOURS);

// 天数据保留30天
redisTemplate.expire(dayKey, 30, TimeUnit.DAYS);
  1. 内存回收监控 :
# 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 基准测试数据

并发量平均响应时间吞吐量
10012ms8,500/sec
1,00028ms35,000/sec
10,00065ms153,000/sec

4.2 优化措施

  1. 连接池配置 :
spring:
  redis:
    lettuce:
      pool:
        max-active: 200
        max-idle: 50
        min-idle: 10
  1. 集群分片策略 :
// 使用hash tag确保相同广告位的数据落在同一分片
String buildKey(String channel, String adId, String timeStr) {
    return String.format("{%s:%s}:%s", channel, adId, timeStr);
}

五、总结与展望

本文实现的广告统计系统具有以下特点:

  1. 多粒度统计 :同时支持小时级和天级统计
  2. 高性能 :单节点支持15万+/秒的写入
  3. 低延迟 :平均响应时间<50ms
  4. 可扩展 :通过Redis集群轻松扩展

未来可改进方向:

  • 增加分钟级实时统计
  • 实现冷热数据分离(热数据Redis,冷数据TSDB)
  • 增加异常价格检测机制

通过合理利用Redis的特性,我们成功构建了一个高性能、高可靠的广告统计系统,为业务决策提供了实时数据支持。

posted @ 2025-05-26 20:30  性感的猴子  阅读(0)  评论(0)    收藏  举报  来源