Redis场景实战:计数器、HyperLogLog去重、Bitmap签到

无论是实时分析、用户行为跟踪,还是高效的缓存机制,Redis都成为了开发者的得力助手。作为一个开源的内存数据结构存储系统,Redis不仅仅是一个简单的键值存储,它支持多种丰富的数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)、位图(Bitmap)、HyperLogLog等。这些数据结构赋予了Redis在处理各种复杂场景时的强大能力,使其在数据库、缓存和消息代理等方面表现出色。本文将通过具体的示例,展示如何利用Redis的这些特性来实现高效的统计功能。

本文中讨论的三种Redis数据类型及其主要场景逻辑

String

  • 简介: Redis中的字符串不仅可以存储文本,还可以存储整数和浮点数。通过原子操作,字符串可以用作计数器。
  • 操作 : INCRDECRINCRBYDECRBY等。
  • 场景: 网站访问量统计、点赞数统计、商品库存管理等。
  • 逻辑 : 每次访问、点赞或库存变化时,使用INCRDECR操作更新计数器。

HyperLogLog

  • 简介: HyperLogLog是一种概率性数据结构,用于基数估计(即去重计数)。虽然它不能存储具体的元素,但可以在固定的内存空间内提供接近准确的去重计数。
  • 操作 : PFADDPFCOUNTPFMERGE
  • 场景: 独立用户访问统计、独立IP统计等。
  • 逻辑 : 每次有新用户访问时,使用PFADD添加用户标识,通过PFCOUNT获取去重后的用户数。

Bitmap

  • 简介: 位图是一种用于存储布尔值的紧凑数据结构,可以通过位操作进行高效的存储和查询。常用于签到系统和用户状态记录。
  • 操作 : SETBITGETBITBITCOUNTBITOP
  • 场景: 用户签到系统、用户在线状态记录等。
  • 逻辑 : 用户签到时,使用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统计、用户签到系统、页面访问量统计、用户操作次数统计和新增用户统计的具体类,并且可以通过这些类来进行具体的统计操作。

相关推荐
丁总学Java6 分钟前
如何使用 maxwell 同步到 redis?
数据库·redis·缓存
蘑菇蘑菇不会开花~12 分钟前
分布式Redis(14)哈希槽
redis·分布式·哈希算法
爱吃南瓜的北瓜14 分钟前
Redis的Key的过期策略是怎样实现的?
数据库·redis·bootstrap
danplus29 分钟前
node发送邮件:如何实现Node.js发信功能?
服务器·node.js·外贸开发信·邮件群发·蜂邮edm邮件营销·邮件接口·营销邮件
青稞儿33 分钟前
面试题高频之token无感刷新(vue3+node.js)
vue.js·node.js
wn53144 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
小菜yh1 小时前
关于Redis
java·数据库·spring boot·redis·spring·缓存
希冀1231 小时前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper2 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文3 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring