likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 7

背景

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

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

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

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

bitmap介绍

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

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

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

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

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

基本使用

SETBIT

语法

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 命令,并在合适的时间点进行操作。
//设置
setbit mykey 7 1
//清空
setbit mykey 7 0

GETBIT

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

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

语法

GETBIT key offset

示例

getbit mykey 7

BITCOUNT 位计数

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

语法

BITCOUNT key [start end]

示例

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

BITOP

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

语法:

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

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

场景

ip访问

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

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

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设置为已登录状态:

SETBIT login_status 10086 1

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

GETBIT login_status 10086

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

SETBIT login_status 10086 0

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

代码实现

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日打卡:

SETBIT uid:sign:10086:202401 15 1

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

GETBIT uid:sign:10086:202401 15

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

BITCOUNT uid:sign:10086:202401
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

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

bitop and dest1 login:20240115 login:20240116
bitcount dest1

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

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

bitop or dest2 login:20240115 login:20240116 login:20240117

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

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

转载自:https://juejin.cn/post/7329694104118362139
评论
请登录