亿万用户在线,一张bitmap统计全解密

背景

实现统计网站日活和在线人数的方式有很多种,其中一种常见的方法是使用位图(bitmap)进行统计。

位图是一种数据结构,用来表示一组二进制位的集合。在这种情况下,每个二进制位表示一个用户的在线状态,比如0表示离线,1表示在线。位图可以用一个整数数组来表示,数组中的每个元素可以存储多个位的值。

具体实现中,将每个用户映射到一个唯一的数字标识符,并使用这个标识符作为位图的索引。当用户登录或注销时,根据用户的标识符在位图中相应位置上置1或置0。

统计日活和在线人数时,只需遍历位图,计算其中值为1的位的个数即可。这是一种高效的统计方式,因为整个位图只需要占用很小的内存空间,就可以轻松统计数百万用户的在线状态。

bitmap介绍

在 Redis 中,Bitmaps 是利用比特位来存储大量的布尔值数据的一种数据结构。每个比特位只能存储 0 或 1,而且 Bitmaps 是以数组的形式进行存储,数组的每个单元对应一个比特位。

在 Redis 的 Bitmaps 中,我们可以使用一些操作来操作 Bitmaps。例如,可以设置、获取或清除指定偏移量处的比特位的值,还可以对多个 Bitmaps 进行逻辑运算,如与、或、非等。

使用 Bitmaps 可以极大地节省内存空间,尤其对于需要存储大量的布尔值数据,且每个布尔值只需要占用一个比特位的情况下。这使得 Bitmaps 成为了一种高效的数据结构,特别适用于处理大规模数据集合的情况。

位图不是实际的数据类型,而是在 String 类型上定义的一组面向位的操作,将其视为位向量。由于字符串是二进制安全 blob,其最大长度为 512 MB,因此它们适合设置最多 2^32 个不同位。

基本使用

SETBIT

语法

go 复制代码
SETBIT key offset value

当使用 SETBIT 命令设置一个键的某个位(bit)时,需要注意以下几点:

  1. 设置最后一个可能的位(偏移量等于2^32 -1)可能导致内存分配阻塞:如果你尝试设置最后一个可用的位,Redis需要分配大量内存,并且可能导致服务器阻塞一段时间。
  2. 设置较小的位数需要更少的时间:较小的位数设置比较快,例如设置位号2^30 - 1(128MB分配)需要约80毫秒,设置位号2^28 - 1(32MB分配)需要约30毫秒,设置位号2^26 - 1(8MB分配)需要约8毫秒。
  3. 后续对同一键的 SETBIT 调用不会再次分配内存:一旦完成第一次内存分配,后续对同一键的 SETBIT 调用将不会产生额外的内存开销,因此不会再次阻塞服务器。
  4. 潜在的性能影响:在设置大量位(特别是最后一个位)时,SETBIT 命令可能会对系统性能产生显著的影响。在大数据量或设备资源有限的情况下,需要谨慎使用 SETBIT 命令,并在合适的时间点进行操作。
go 复制代码
//设置
setbit mykey 7 1
//清空
setbit mykey 7 0

GETBIT

返回 key 处存储的字符串值中偏移处的位值

当偏移量超出字符串长度时,字符串被假定为具有 0 位的连续空间。当 key 不存在时,它被假定为空字符串,因此 offset 总是超出范围,并且 value 也被假定为 0 位的连续空间

语法

go 复制代码
GETBIT key offset

示例

go 复制代码
getbit mykey 7

BITCOUNT 位计数

计算字符串中设置位的数量(总体计数)

语法

go 复制代码
BITCOUNT key [start end]

示例

go 复制代码
#获取mykey内值为 1 的个数
BITCOUNT mykey
# 获取指定范围内值为 1 的个数,start 和 end 以字节为单位
BITCOUNT mykey 0 1

BITOP

在多个键(包含字符串值)之间执行按位运算并将结果存储在目标键中

语法:

go 复制代码
#AND 与运算 &
#OR 或运算 |
# XOR 异或 ^
#NOT 取反 ~
BITOP <AND | OR | XOR | NOT> destkey key [key ...]

场景

ip访问

当使用位图(Bitmap)来实现某个系统的IP访问检测时,需要将IP地址转换为整数形式进行处理。在IPv4中,IP地址由四个字节组成,每个字节有8位,总共32位。因此,将IP地址转换为整数,可以将每个字节看作是256进制,然后将它们转换为十进制数。

下面是更新过的示例代码,包含了将整数转换为IP地址的过程:

go 复制代码
package main

import (
	"fmt"
	"github.com/go-redis/redis/v8"
)

func main() {
	// 初始化Redis客户端
	client := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // Redis服务器的地址和端口
		Password: "",               // Redis服务器的密码
		DB:       0,                // 使用的数据库编号
	})

	// 模拟记录IP访问行为
	client.SetBit("ip_access", 3232235786, 1) // 3232235786 对应的IP地址是192.168.1.10
	client.SetBit("ip_access", 3232235787, 1) // 192.168.1.11
	client.SetBit("ip_access", 3232235788, 1) // 192.168.1.12
	// 更多的IP访问记录...

	// 生成报表
	report := "| IP Address | Access |\n|------------|--------|\n"
	for i := 0; i < 1000; i++ {
		bit, err := client.GetBit("ip_access", int64(i)).Result()
		if err == nil && bit == 1 {
			ip := convertIntToIP(i)
			report += fmt.Sprintf("| %s | Yes |\n", ip)
		}
	}

	// 输出报表
	fmt.Println(report)
}

// 辅助函数:将整数转换为IP地址字符串
func convertIntToIP(ipInt int) string {
	// 将整数分解为四个字节
	byte1 := ipInt >> 24 & 0xFF
	byte2 := ipInt >> 16 & 0xFF
	byte3 := ipInt >> 8 & 0xFF
	byte4 := ipInt & 0xFF
	// 构造IP地址字符串
	ip := fmt.Sprintf("%d.%d.%d.%d", byte1, byte2, byte3, byte4)
	return ip
}

在这个示例中,我们新增了一个辅助函数convertIntToIP,它将整数形式的IP地址转换为点分十进制形式的IP地址。通过这样的方法,我们可以更高效地处理和存储IP地址的访问数据。

在线用户统计

在线用户统计可以通过使用一个简单的键来实现,其中用户ID作为偏移量(offset),该键表示用户的在线状态。当用户在线时,对应的偏移量设置为1;当用户离线时,设置为0。

以下是在线用户统计的操作步骤:

用户登录时,将其ID设置为已登录状态:

go 复制代码
SETBIT login_status 10086 1

检查用户是否登录,如果返回值为1,则表示用户已登录:

go 复制代码
GETBIT login_status 10086

用户登出时,将对应的偏移量设置为0,表示用户已离线:

go 复制代码
SETBIT login_status 10086 0

根据提供的用户数量估算,假设当前站点有5000万用户,那么一天的数据大约为50000000/8/1024/1024=6MB,可以看出内存占用非常低。

代码实现

go 复制代码
package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
)

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr:     "10.1.250.157:6379",
        Password: "google00",
        DB:       0,
    })

    // 设置用户登录在线
    rdb.SetBit(ctx, "login_status", 10086, 1)

    // 查询用户登录状态
    loginStatus, _ := rdb.GetBit(ctx, "login_status", 10086).Result()
    if loginStatus == 1 {
        fmt.Println("10086 用户在线")
    } else {
        fmt.Println("10086 用户离线")
    }

    // 用户退出登录, 值设置为0
    rdb.SetBit(ctx, "login_status", 10086, 0)
}

用户签到

在签到统计中,每个用户每天的签到可以用一个比特位来表示,一年的签到只需要365个比特位。一个月中最多只有31天,所以只需要31个比特位即可。

以下是在 Redis 中如何进行用户签到的相关操作,以编号为10086的用户在2024年1月份的打卡情况为例:

记录用户在2024年1月16日打卡:

go 复制代码
SETBIT uid:sign:10086:202401 15 1

判断编号为10086的用户在2024年1月16日是否打卡:

go 复制代码
GETBIT uid:sign:10086:202401 15

统计该用户在2024年1月份的打卡次数,使用BITCOUNT指令。BITCOUNT指令用于统计给定的比特位数组中值为1的比特位的数量:

go 复制代码
BITCOUNT uid:sign:10086:202401
go 复制代码
package main

import (
	"fmt"
	"github.com/go-redis/redis/v8"
	"log"
)

func main() {
	// 建立 Redis 客户端连接
	client := redis.NewClient(&redis.Options{
		Addr:     "10.1.250.157:6379",
		Password: "google00", // 设置密码
	})

	// 关闭连接
	defer client.Close()

	// 记录用户在 2024 年 1 月 16 号打卡
	err := client.SetBit(ctx, "uid:sign:10086:202401", 10086, 1).Err()
	if err != nil {
		log.Fatal(err)
	}

	// 判断用户在 2024 年 1 月 16 是否打卡
	loginStatus, err := client.GetBit(ctx, "uid:sign:10086:202401", 10086).Result()
	if err != nil {
		log.Fatal(err)
	}
	if loginStatus == 1 {
		fmt.Println("10086用户 2024 年 1 月 16 打卡")
	} else {
		fmt.Println("10086用户 2024 年 1 月 16 未打卡")
	}

	// 统计1月份打卡次数
	num, err := client.BitCount(ctx, "uid:sign:10086:202401", nil).Result()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("用户10086在1月份打卡:%d次\n", num)
}

统计活跃用户(用户登陆情况)

使用日期作为键,然后用户ID作为偏移量,可以记录每天活跃的用户情况。具体什么样的行为定义为活跃用户可以根据需求进行自定义。

假设有用户ID为「1,2,3,4,5,6」的用户,并记录以下日期的登录情况:

  1. 用户2024年1月15号的登录情况:

    • 用户1登录:setbit login:20240115 1 1
    • 用户2登录:setbit login:20240115 2 1
    • 用户3登录:setbit login:20240115 3 1
    • 用户4登录:setbit login:20240115 4 1
    • 用户5登录:setbit login:20240115 5 1
  2. 用户2024年1月16号的登录情况:

    • 用户1登录:setbit login:20240116 1 1
    • 用户2登录:setbit login:20240116 2 1
    • 用户3登录:setbit login:20240116 3 1
    • 用户4登录:setbit login:20240116 4 1
  3. 用户2024年1月17号的登录情况:

    • 用户1登录:setbit login:20240117 1 1
    • 用户2登录:setbit login:20240117 2 1
    • 用户4登录:setbit login:20240117 4 1
    • 用户6登录:setbit login:20240117 6 1

接下来,可以使用以下指令来统计连续两天活跃的用户总数:

go 复制代码
bitop and dest1 login:20240115 login:20240116
bitcount dest1

以上指令将会在dest1中记录连续两天活跃的用户ID,然后通过bitcount指令统计活跃用户的数量。

如果要统计从2024年1月15日至2024年1月17日期间活跃过的用户,可以使用以下指令:

go 复制代码
bitop or dest2 login:20240115 login:20240116 login:20240117

以上指令将会在dest2中记录在这三天内活跃过的用户ID。

根据以上步骤,您可以实现活跃用户的统计和记录。

相关推荐
小码哥_常6 小时前
别再被误导!try...catch性能大揭秘
后端
苍何8 小时前
30分钟用 Agent 搓出一家跨境网店,疯了
后端
ssshooter8 小时前
Tauri 2 iOS 开发避坑指南:文件保存、Dialog 和 Documents 目录的那些坑
前端·后端·ios
追逐时光者8 小时前
一个基于 .NET Core + Vue3 构建的开源全栈平台 Admin 系统
后端·.net
程序员飞哥9 小时前
90后大龄程序员失业4个月终于上岸了
后端·面试·程序员
彭于晏Yan10 小时前
Redisson分布式锁
spring boot·redis·分布式
GetcharZp10 小时前
Git 命令行太痛苦?这款 75k Star 的神级工具,让你告别“合并冲突”恐惧症!
后端
Victor35611 小时前
MongoDB(69)如何进行增量备份?
后端
Victor35611 小时前
MongoDB(70)如何使用副本集进行备份?
后端
千寻girling12 小时前
面试官 : “ 说一下 Python 中的常用的 字符串和数组 的 方法有哪些 ? ”
人工智能·后端·python