网站流量统计与分析方案
流量指标体系
在分析网站或应用的活跃度时,有几个核心指标需要掌握:
核心指标定义
mermaid
graph TD
A["流量统计指标"] --> B["PV - 页面浏览量"]
A --> C["UV - 独立访客数"]
A --> D["VV - 访问次数"]
A --> E["IP - 独立IP数"]
B --> B1["每次页面加载计数一次"]
C --> C1["同一访客一天只计一次"]
D --> D1["一次完整访问计数一次"]
E --> E1["不同IP地址计数"]
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style B fill:#E74C3C,color:#fff,rx:10,ry:10
style C fill:#27AE60,color:#fff,rx:10,ry:10
style D fill:#9B59B6,color:#fff,rx:10,ry:10
style E fill:#E67E22,color:#fff,rx:10,ry:10举例说明:
假设用户通过家庭宽带上网,早上访问了电商平台的2个商品页面,下午又访问了3个商品页面:
| 指标 | 计算方式 | 结果 |
|---|---|---|
| PV | 早上2页 + 下午3页 | 5 |
| UV | 同一访客全天只计1次 | 1 |
| VV | 早上1次 + 下午1次访问 | 2 |
| IP | 宽带可能换IP | 1-2 |
指标的业务价值
- PV:反映网站内容的吸引力和用户浏览深度
- UV:反映真实的用户覆盖面
- PV/UV比值:反映用户粘性,比值越高说明单用户浏览越多
- IP:用于判断用户地域分布和识别异常流量
UV统计技术方案
方案一:Set集合精确统计
最直观的实现方式是为每个页面维护一个用户ID集合:
mermaid
graph TD
A["用户访问页面"] --> B["获取用户标识"]
B --> C{"Set中是否存在"}
C -->|是| D["不做处理"]
C -->|否| E["添加到Set"]
F["统计UV"] --> G["计算Set大小"]
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style E fill:#27AE60,color:#fff,rx:10,ry:10
style G fill:#9B59B6,color:#fff,rx:10,ry:10Redis Set实现:
java
@Service
public class UvStatisticsService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 记录用户访问
*/
public void recordVisit(String pageId, String visitorId) {
String key = buildKey(pageId, LocalDate.now());
redisTemplate.opsForSet().add(key, visitorId);
// 设置过期时间,避免数据堆积
redisTemplate.expire(key, 7, TimeUnit.DAYS);
}
/**
* 获取UV数量
*/
public Long getUv(String pageId, LocalDate date) {
String key = buildKey(pageId, date);
return redisTemplate.opsForSet().size(key);
}
private String buildKey(String pageId, LocalDate date) {
return String.format("uv:%s:%s", pageId, date.toString());
}
}适用场景:
- 访问量不大的网站
- 需要精确统计的场景
- 需要知道具体访问用户的场景
局限性:
- 当日访问用户达到百万级时,单个Set占用内存巨大
- 多页面场景下内存成本更高
方案二:HyperLogLog概率统计
HyperLogLog是一种基数估计算法,能够用极小的空间统计海量数据的基数。
mermaid
graph LR
A["HyperLogLog特点"] --> B["空间效率极高"]
A --> C["存在一定误差"]
A --> D["支持合并操作"]
B --> B1["12KB存储2^64个元素"]
C --> C1["标准误差0.81%"]
D --> D1["可跨时段统计"]
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style B fill:#27AE60,color:#fff,rx:10,ry:10
style C fill:#E67E22,color:#fff,rx:10,ry:10
style D fill:#9B59B6,color:#fff,rx:10,ry:10Redis HyperLogLog实现:
java
@Service
public class HllUvStatisticsService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 记录用户访问
*/
public void recordVisit(String pageId, String visitorId) {
String key = buildHllKey(pageId, LocalDate.now());
redisTemplate.opsForHyperLogLog().add(key, visitorId);
}
/**
* 获取单日UV
*/
public Long getDailyUv(String pageId, LocalDate date) {
String key = buildHllKey(pageId, date);
return redisTemplate.opsForHyperLogLog().size(key);
}
/**
* 获取日期范围内的UV(自动去重)
*/
public Long getRangeUv(String pageId, LocalDate startDate, LocalDate endDate) {
List<String> keys = new ArrayList<>();
LocalDate current = startDate;
while (!current.isAfter(endDate)) {
keys.add(buildHllKey(pageId, current));
current = current.plusDays(1);
}
// 合并多天的HyperLogLog
String mergedKey = "uv:merged:" + UUID.randomUUID();
redisTemplate.opsForHyperLogLog().union(
mergedKey,
keys.toArray(new String[0]));
Long result = redisTemplate.opsForHyperLogLog().size(mergedKey);
redisTemplate.delete(mergedKey);
return result;
}
private String buildHllKey(String pageId, LocalDate date) {
return String.format("hll:uv:%s:%s", pageId, date.toString());
}
}方案对比测试
下面通过实际测试对比两种方案:
java
public class UvStatisticsTest {
private static final long USER_COUNT = 100000L;
@Test
void compareAccuracyAndMemory() {
Jedis jedis = new Jedis("localhost", 6379);
String setKey = "test:set:uv";
String hllKey = "test:hll:uv";
// 添加10万用户
for (int i = 0; i < USER_COUNT; i++) {
String userId = "user_" + i;
jedis.sadd(setKey, userId);
jedis.pfadd(hllKey, userId);
}
// 精确度对比
long setCount = jedis.scard(setKey);
long hllCount = jedis.pfcount(hllKey);
System.out.println("Set精确计数: " + setCount);
System.out.println("HLL估算计数: " + hllCount);
System.out.println("误差率: " +
String.format("%.2f%%",
Math.abs(setCount - hllCount) * 100.0 / setCount));
// 内存占用对比(序列化后大小)
// Set: 约965KB
// HLL: 约10KB
}
}测试结果:
- Set精确度:100%
- HLL精确度:约99.27%(误差0.73%)
- Set内存占用:约965KB(10万用户)
- HLL内存占用:约10KB(固定)
PV统计实现
PV统计相对简单,不需要去重:
java
@Service
public class PvStatisticsService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 记录PV(每次访问都计数)
*/
public void recordPv(String pageId) {
String key = buildPvKey(pageId, LocalDate.now());
redisTemplate.opsForValue().increment(key);
}
/**
* 记录PV(带小时维度)
*/
public void recordPvWithHour(String pageId) {
LocalDateTime now = LocalDateTime.now();
String dayKey = buildPvKey(pageId, now.toLocalDate());
String hourKey = buildPvHourKey(pageId, now);
redisTemplate.opsForValue().increment(dayKey);
redisTemplate.opsForValue().increment(hourKey);
}
/**
* 获取指定日期PV
*/
public Long getDailyPv(String pageId, LocalDate date) {
String key = buildPvKey(pageId, date);
String value = redisTemplate.opsForValue().get(key);
return value != null ? Long.parseLong(value) : 0L;
}
/**
* 获取小时级PV分布
*/
public Map<Integer, Long> getHourlyPvDistribution(String pageId, LocalDate date) {
Map<Integer, Long> distribution = new HashMap<>();
for (int hour = 0; hour < 24; hour++) {
String key = buildPvHourKey(pageId,
date.atTime(hour, 0));
String value = redisTemplate.opsForValue().get(key);
distribution.put(hour, value != null ? Long.parseLong(value) : 0L);
}
return distribution;
}
private String buildPvKey(String pageId, LocalDate date) {
return String.format("pv:%s:%s", pageId, date.toString());
}
private String buildPvHourKey(String pageId, LocalDateTime time) {
return String.format("pv:%s:%s:%02d",
pageId,
time.toLocalDate().toString(),
time.getHour());
}
}数据持久化与分析
定时归档策略
Redis中的统计数据需要定期归档到数据库:
mermaid
sequenceDiagram
participant Redis as Redis缓存
participant Task as 定时任务
participant DB as 数据库
participant BI as 分析系统
Task->>Redis: 获取昨日统计数据
Redis-->>Task: 返回PV/UV数据
Task->>DB: 写入统计表
Task->>Redis: 删除已归档数据
BI->>DB: 查询历史数据
DB-->>BI: 返回趋势数据归档任务实现:
java
@Scheduled(cron = "0 30 0 * * ?") // 每天0:30执行
public void archiveYesterdayStats() {
LocalDate yesterday = LocalDate.now().minusDays(1);
// 获取所有页面ID
List<String> pageIds = pageService.getAllPageIds();
for (String pageId : pageIds) {
// 获取UV
Long uv = uvService.getDailyUv(pageId, yesterday);
// 获取PV
Long pv = pvService.getDailyPv(pageId, yesterday);
// 写入数据库
PageStats stats = new PageStats();
stats.setPageId(pageId);
stats.setStatDate(yesterday);
stats.setPv(pv);
stats.setUv(uv);
stats.setCreateTime(LocalDateTime.now());
statsMapper.insert(stats);
}
log.info("完成{}的统计数据归档", yesterday);
}统计表设计
sql
CREATE TABLE page_statistics (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
page_id VARCHAR(64) NOT NULL COMMENT '页面标识',
stat_date DATE NOT NULL COMMENT '统计日期',
pv BIGINT DEFAULT 0 COMMENT '页面浏览量',
uv BIGINT DEFAULT 0 COMMENT '独立访客数',
avg_duration INT DEFAULT 0 COMMENT '平均停留时长(秒)',
bounce_rate DECIMAL(5,2) COMMENT '跳出率',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX uk_page_date(page_id, stat_date),
INDEX idx_stat_date(stat_date)
) COMMENT='页面统计表';高级分析场景
用户行为漏斗分析
mermaid
graph TD
A["首页访问<br/>100,000 UV"] --> B["商品详情页<br/>45,000 UV"]
B --> C["加入购物车<br/>12,000 UV"]
C --> D["提交订单<br/>5,000 UV"]
D --> E["支付成功<br/>4,200 UV"]
A -->|转化率 45%| B
B -->|转化率 26.7%| C
C -->|转化率 41.7%| D
D -->|转化率 84%| E
style A fill:#4A90E2,color:#fff,rx:10,ry:10
style B fill:#3498DB,color:#fff,rx:10,ry:10
style C fill:#9B59B6,color:#fff,rx:10,ry:10
style D fill:#E67E22,color:#fff,rx:10,ry:10
style E fill:#27AE60,color:#fff,rx:10,ry:10来源渠道分析
java
@Service
public class TrafficSourceAnalyzer {
/**
* 记录访问来源
*/
public void recordSource(String visitorId, String source) {
String dateKey = "source:" + LocalDate.now();
redisTemplate.opsForHash().increment(dateKey, source, 1);
}
/**
* 获取来源分布
*/
public Map<String, Long> getSourceDistribution(LocalDate date) {
String key = "source:" + date;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
return entries.entrySet().stream()
.collect(Collectors.toMap(
e -> e.getKey().toString(),
e -> Long.parseLong(e.getValue().toString())
));
}
}企业级方案选型
对于大规模数据分析场景,可以考虑专业的OLAP数据库:
| 方案 | 特点 | 适用场景 |
|---|---|---|
| ClickHouse | 列式存储,查询极快 | 实时分析,Ad-hoc查询 |
| Doris | 兼容MySQL协议 | 多维分析,报表系统 |
| Elasticsearch | 全文检索+聚合分析 | 日志分析,实时监控 |
| Flink + Kafka | 流式计算 | 实时指标计算 |
技术栈选型建议:
- 日活百万以下:Redis + MySQL足够
- 日活百万到千万:考虑ClickHouse或Doris
- 需要实时分析:Flink流式计算 + Kafka
更新: 2025-12-06 17:31:47
原文: https://www.yuque.com/u22210564/zoxfmt/lxzgassuogflideb