无论是实时分析、用户行为跟踪,还是高效的缓存机制,Redis都成为了开发者的得力助手。作为一个开源的内存数据结构存储系统,Redis不仅仅是一个简单的键值存储,它支持多种丰富的数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)、位图(Bitmap)、HyperLogLog等。这些数据结构赋予了Redis在处理各种复杂场景时的强大能力,使其在数据库、缓存和消息代理等方面表现出色。本文将通过具体的示例,展示如何利用Redis的这些特性来实现高效的统计功能。
本文中讨论的三种Redis数据类型及其主要场景逻辑
String
- 简介: Redis中的字符串不仅可以存储文本,还可以存储整数和浮点数。通过原子操作,字符串可以用作计数器。
- 操作 :
INCR
,DECR
,INCRBY
,DECRBY
等。
- 场景: 网站访问量统计、点赞数统计、商品库存管理等。
- 逻辑 : 每次访问、点赞或库存变化时,使用
INCR
或DECR
操作更新计数器。
HyperLogLog
- 简介: HyperLogLog是一种概率性数据结构,用于基数估计(即去重计数)。虽然它不能存储具体的元素,但可以在固定的内存空间内提供接近准确的去重计数。
- 操作 :
PFADD
,PFCOUNT
,PFMERGE
。
- 场景: 独立用户访问统计、独立IP统计等。
- 逻辑 : 每次有新用户访问时,使用
PFADD
添加用户标识,通过PFCOUNT
获取去重后的用户数。
Bitmap
- 简介: 位图是一种用于存储布尔值的紧凑数据结构,可以通过位操作进行高效的存储和查询。常用于签到系统和用户状态记录。
- 操作 :
SETBIT
,GETBIT
,BITCOUNT
,BITOP
。
- 场景: 用户签到系统、用户在线状态记录等。
- 逻辑 : 用户签到时,使用
SETBIT
设置对应日期的位,查询签到状态时使用GETBIT
,统计签到天数时使用BITCOUNT
。
场景枚举
以下是一些适合使用Redis进行统计的常见场景。为了代码的清晰和维护性,每个场景可以封装在单独的类中。
独立IP统计
场景编号 |
场景名称 |
描述 |
应用场景 |
1 |
最近一小时的独立IP统计 |
统计最近一小时内访问网站的独立IP数量。 |
分析当前流量情况,检测潜在流量异常。 |
2 |
最近5分钟的独立IP统计 |
统计最近5分钟内访问网站的独立IP数量。 |
分析当前流量情况,检测突发流量。 |
3 |
按小时统计独立IP |
统计每小时访问网站的独立IP数量。 |
分析每小时的流量来源,检测潜在流量异常。 |
4 |
按天统计独立IP |
统计每天访问网站的独立IP数量。 |
分析每日流量来源,检测潜在流量异常。 |
5 |
按月统计独立IP |
统计每月访问网站的独立IP数量。 |
长期流量趋势分析,制定月度营销策略。 |
独立用户统计
场景编号 |
场景名称 |
描述 |
应用场景 |
6 |
最近一小时的独立用户统计 |
统计最近一小时内访问网站的独立用户数量。 |
分析当前用户活跃情况,评估实时运营效果。 |
7 |
最近5分钟的独立用户统计 |
统计最近5分钟内访问网站的独立用户数量。 |
分析当前用户活跃情况,检测突发用户行为。 |
8 |
按小时统计独立用户 |
统计每小时访问网站的独立用户数量。 |
分析每小时的用户活跃情况,评估小时级别的运营效果。 |
9 |
按天统计独立用户 |
统计每天访问网站的独立用户数量。 |
分析每日活跃用户,评估日常运营效果。 |
10 |
按月统计独立用户 |
统计每月访问网站的独立用户数量。 |
长期用户增长分析,制定月度用户增长策略。 |
活跃用户统计
场景编号 |
场景名称 |
描述 |
应用场景 |
11 |
最近一小时的活跃用户统计 |
统计最近一小时内的活跃用户数量(即最近一小时内有过操作的用户)。 |
分析当前用户活跃情况,评估实时运营效果。 |
12 |
最近5分钟的活跃用户统计 |
统计最近5分钟内的活跃用户数量(即最近5分钟内有过操作的用户)。 |
分析当前用户活跃情况,检测突发用户行为。 |
13 |
按小时统计活跃用户 |
统计每小时的活跃用户数量(即该小时内有过操作的用户)。 |
分析小时级别的用户活跃情况,评估运营效果。 |
14 |
按天统计活跃用户 |
统计每天的活跃用户数量(即当天有过操作的用户)。 |
分析日活跃用户(DAU),评估日常运营效果。 |
15 |
按月统计活跃用户 |
统计每月的活跃用户数量(即当月有过操作的用户)。 |
分析月活跃用户(MAU),评估长期用户活跃度。 |
新增用户统计
场景编号 |
场景名称 |
描述 |
应用场景 |
16 |
最近一小时的新增用户统计 |
统计最近一小时内新增的用户数量。 |
分析用户增长情况,评估实时推广效果。 |
17 |
最近5分钟的新增用户统计 |
统计最近5分钟内新增的用户数量。 |
分析用户增长情况,检测突发用户注册行为。 |
18 |
按小时统计新增用户 |
统计每小时新增的用户数量。 |
分析用户增长情况,评估小时级别的推广效果。 |
19 |
按天统计新增用户 |
统计每天新增的用户数量。 |
分析用户增长情况,评估日常推广效果。 |
20 |
按月统计新增用户 |
统计每月新增的用户数量。 |
长期用户增长分析,制定月度用户增长策略。 |
用户操作次数统计
场景编号 |
场景名称 |
描述 |
应用场景 |
21 |
最近一小时的用户操作次数统计 |
统计最近一小时内用户的操作次数(如点击、浏览)。 |
分析当前用户行为,优化用户体验。 |
22 |
最近5分钟的用户操作次数统计 |
统计最近5分钟内用户的操作次数(如点击、浏览)。 |
分析当前用户行为,检测突发用户操作行为。 |
23 |
按小时统计用户操作次数 |
统计每小时用户的操作次数(如点击、浏览)。 |
分析用户行为,优化用户体验。 |
24 |
按天统计用户操作次数 |
统计每天用户的操作次数(如点击、浏览)。 |
分析用户行为,优化用户体验。 |
25 |
按月统计用户操作次数 |
统计每月用户的操作次数(如点击、浏览)。 |
长期用户行为分析,制定月度运营策略。 |
用户签到系统
场景编号 |
场景名称 |
描述 |
应用场景 |
26 |
用户签到系统 |
记录用户每日签到情况。 |
用户激励机制,如连续签到奖励。 |
27 |
用户签到奖励系统 |
根据用户签到情况发放奖励。 |
用户激励机制,增加用户粘性。 |
28 |
用户签到统计 |
统计用户的签到次数和连续签到天数。 |
评估用户活跃度,制定签到奖励策略。 |
用户在线状态
场景编号 |
场景名称 |
描述 |
应用场景 |
29 |
用户在线状态 |
记录和查询用户的在线状态。 |
即时通讯系统中的在线状态显示。 |
页面访问量统计
场景编号 |
场景名称 |
描述 |
应用场景 |
30 |
按小时统计页面访问量 |
统计每小时各个页面的访问量。 |
分析页面受欢迎程度,优化页面内容。 |
31 |
按天统计页面访问量 |
统计每天各个页面的访问量。 |
分析页面受欢迎程度,优化页面内容。 |
32 |
按月统计页面访问量 |
统计每月各个页面的访问量。 |
长期页面流量分析,优化页面结构。 |
通过使用Redis的不同数据结构(如String、HyperLogLog、Bitmap),我们可以高效地实现各种统计需求。每个统计功能都可以封装在单独的类中,使代码结构清晰、易于维护和扩展。这种设计方式非常适合实际应用中的各种统计需求。
具体实现
我们可以创建一个通用的基础统计类,然后通过继承和扩展这个基础类来实现不同的统计场景。这样可以更好地实现时间单位的选择和链式调用。
以下是一个通用的基础统计类和具体场景类的实现示例:
工具函数
ts
复制代码
// tools/time.ts
/**
* 获取今天的日期字符串,格式为 YYYY-MM-DD 或其他指定分隔符。
* @param date 可选参数,要获取日期的 Date 对象,默认为当前日期。
* @param connector 可选参数,日期中的分隔符,默认为横杠 ("-")。
* @returns 返回今天的日期字符串,例如 "2023-10-23" 或其他指定格式。
*/
export function getTodayString(
date: Date = new Date(),
connector: string = "-"
): string {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return [year, month, day].join(connector);
}
/**
* 获取当前小时的字符串,格式为 YYYY-MM-DD-HH。
* @param date 可选参数,要获取小时的 Date 对象,默认为当前日期。
* @returns 返回当前小时的字符串,例如 "2023-10-23-14"。
*/
export function getCurrentHourString(date: Date = new Date()): string {
const todayString = getTodayString(date);
const hour = date.getHours().toString().padStart(2, "0");
return `${todayString}-${hour}`;
}
/**
* 获取当前月份的字符串,格式为 YYYY-MM。
* @param date 可选参数,要获取月份的 Date 对象,默认为当前日期。
* @returns 返回当前月份的字符串,例如 "2023-10"。
*/
export function getCurrentMonthString(date: Date = new Date()): string {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
return `${year}-${month}`;
}
/**
* 获取当前年份的字符串,格式为 YYYY。
* @param date 可选参数,要获取年份的 Date 对象,默认为当前日期。
* @returns 返回当前年份的字符串,例如 "2023"。
*/
export function getCurrentYearString(date: Date = new Date()): string {
return date.getFullYear().toString();
}
通用基础统计类
typescript
复制代码
import { Redis as RedisClient } from "ioredis";
import { CacheOption } from "./TSRedisCacheKit/type";
import {
getTodayString,
getCurrentHourString,
getCurrentMonthString,
getCurrentYearString,
} from "../tools/time";
// 定义时间单位类型
type TimeUnit =
| "second"
| "minute"
| "hour"
| "day"
| "week"
| "month"
| "year";
export abstract class BaseStatistic {
protected option: CacheOption;
protected redis: RedisClient;
protected prefix: string;
protected timeUnit: TimeUnit = "day"; // 默认时间单位为 'day'
constructor(prefix: string, option: CacheOption, redisClient: RedisClient) {
this.option = option;
this.redis = redisClient;
this.prefix = prefix;
}
protected createKey(suffix: string): string {
const timeSuffix = this.getTimeSuffix();
return `${this.prefix}-${this.option.appName}-${this.option.funcName}-${timeSuffix}-${suffix}`;
}
setTimeUnit(unit: TimeUnit) {
this.timeUnit = unit;
return this;
}
private getTimeSuffix(): string {
const date = new Date();
switch (this.timeUnit) {
case "second":
return `${getTodayString(
date
)}-${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
case "minute":
return `${getTodayString(
date
)}-${date.getHours()}-${date.getMinutes()}`;
case "hour":
return getCurrentHourString(date);
case "day":
return getTodayString(date);
case "week":
// 计算当前周的第一天
const firstDayOfWeek = new Date(
date.setDate(date.getDate() - date.getDay())
);
return getTodayString(firstDayOfWeek);
case "month":
return getCurrentMonthString(date);
case "year":
return getCurrentYearString(date);
default:
return getTodayString(date);
}
}
abstract add(value: string): Promise<void>;
abstract getCount(): Promise<number>;
}
独立IP统计类
typescript
复制代码
// ioredis/busines/count.ts
import { Redis as RedisClient } from "ioredis";
import { BaseStatistic } from "../BaseStatistic";
import { CacheOption } from "../TSRedisCacheKit/type";
class UniqueIPStatistic extends BaseStatistic {
constructor(prefix: string, option: CacheOption, redisClient: RedisClient) {
super(prefix, option, redisClient);
}
async add(ip: string): Promise<void> {
const key = this.createKey("unique-ip");
await this.redis.pfadd(key, ip);
}
async getCount(): Promise<number> {
const key = this.createKey("unique-ip");
return await this.redis.pfcount(key);
}
}
用户签到系统类
typescript
复制代码
// ioredis/busines/count.ts
import { Redis as RedisClient } from "ioredis";
import { BaseStatistic } from "../BaseStatistic";
import { CacheOption } from "../TSRedisCacheKit/type";
class UserSignInStatistic extends BaseStatistic {
constructor(prefix: string, option: CacheOption, redisClient: RedisClient) {
super(prefix, option, redisClient);
}
async add(userId: string): Promise<void> {
const key = this.createKey(userId);
const today = new Date().getDate();
await this.redis.setbit(key, today, 1);
}
async getCount(): Promise<number> {
const key = this.createKey("user-sign-in");
return await this.redis.bitcount(key);
}
async signIn(userId: string): Promise<void> {
await this.add(userId);
}
async getSignInStatus(userId: string): Promise<boolean> {
const key = this.createKey(userId);
const today = new Date().getDate();
return (await this.redis.getbit(key, today)) === 1;
}
async getSignInCount(userId: string): Promise<number> {
const key = this.createKey(userId);
return await this.redis.bitcount(key);
}
}
页面访问量统计类
typescript
复制代码
// ioredis/busines/count.ts
import { Redis as RedisClient } from "ioredis";
import { BaseStatistic } from "../BaseStatistic";
import { CacheOption } from "../TSRedisCacheKit/type";
class PageViewStatistic extends BaseStatistic {
constructor(prefix: string, option: CacheOption, redisClient: RedisClient) {
super(prefix, option, redisClient);
}
async add(page: string): Promise<void> {
const key = this.createKey("page-view");
await this.redis.incr(key);
}
async getCount(): Promise<number> {
const key = this.createKey("page-view");
const count = await this.redis.get(key);
return count ? parseInt(count) : 0;
}
}
用户操作次数统计类
typescript
复制代码
// ioredis/busines/count.ts
import { Redis as RedisClient } from "ioredis";
import { BaseStatistic } from "../BaseStatistic";
import { CacheOption } from "../TSRedisCacheKit/type";
class UserActionCountStatistic extends BaseStatistic {
constructor(prefix: string, option: CacheOption, redisClient: RedisClient) {
super(prefix, option, redisClient);
}
async add(action: string): Promise<void> {
const key = this.createKey("user-action");
await this.redis.incr(key);
}
async getCount(): Promise<number> {
const key = this.createKey("user-action");
const count = await this.redis.get(key);
return count ? parseInt(count) : 0;
}
}
新增用户统计类
typescript
复制代码
// ioredis/busines/count.ts
import { Redis as RedisClient } from "ioredis";
import { BaseStatistic } from "../BaseStatistic";
import { CacheOption } from "../TSRedisCacheKit/type";
class NewUserStatistic extends BaseStatistic {
constructor(prefix: string, option: CacheOption, redisClient: RedisClient) {
super(prefix, option, redisClient);
}
async add(userId: string): Promise<void> {
const key = this.createKey("new-user");
await this.redis.pfadd(key, userId);
}
async getCount(): Promise<number> {
const key = this.createKey("new-user");
return await this.redis.pfcount(key);
}
}
使用示例
typescript
复制代码
// ioredis/test/count.ts
import Redis from "ioredis";
import { CacheOption } from "../TSRedisCacheKit/type";
import {
UniqueIPStatistic,
UserSignInStatistic,
PageViewStatistic,
UserActionCountStatistic,
NewUserStatistic,
} from "../busines/count";
const redisClient = new Redis();
const cacheOption: CacheOption = { appName: "myApp", funcName: "myFunc" };
const uniqueIPStat = new UniqueIPStatistic("stat", cacheOption, redisClient);
const userSignInStat = new UserSignInStatistic(
"stat",
cacheOption,
redisClient
);
const pageViewStat = new PageViewStatistic("stat", cacheOption, redisClient);
const userActionCountStat = new UserActionCountStatistic(
"stat",
cacheOption,
redisClient
);
const newUserStat = new NewUserStatistic("stat", cacheOption, redisClient);
(async () => {
await uniqueIPStat.add("192.168.1.1");
console.log(await uniqueIPStat.getCount());
await userSignInStat.signIn("user1");
console.log(await userSignInStat.getSignInStatus("user1"));
console.log(await userSignInStat.getSignInCount("user1"));
await pageViewStat.add("home");
console.log(await pageViewStat.getCount());
await userActionCountStat.add("click");
console.log(await userActionCountStat.getCount());
await newUserStat.add("user1");
console.log(await newUserStat.getCount());
})();
这样,我们就实现了独立IP统计、用户签到系统、页面访问量统计、用户操作次数统计和新增用户统计的具体类,并且可以通过这些类来进行具体的统计操作。