redis bitmap简介

redis中的bitmap是一串连续的2进制数字,每一位所在的位置为偏移,在bitmap上可以执行AND,OR,XOR,NOT等位操作。

bitmap中保存每个位置的数据仅使用了一个bit,而对应的数据的存储根据大小在bitmap中相对应offset。

一个简单的例子:日活跃用户

​ 为了统计今日登录的用户数,我们建立了一个bitmap,每一位标识一个用户ID。当某个用户访问我们的网页或执行了某个操作,就在bitmap中把标识此用户的位置为1。在Redis中获取此bitmap的key值是通过用户执行操作的类型和时间戳获得的。之后可以通过各天的bitmap进行位操作来统计相应的数据。

在这里,bitmap在大数据量的情况下,占用大小较为稳定,增长不会随着存储数据量线性增长。也不会只存储一个 2^32-1数字就占满 512G(存在着内存压缩)。

bitmap vs hash

在数据量没有那么大的情况下,使用bitmap可能比使用hashmap还要占用空间。因此,做了如下的实验来验证各个数据存储量的情况下,bitmap与hash占用的空间大小。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func testBit(c redis.Conn, times int) int {	//times为测试存储量
	for i := 0; i < times; i = i + 1000 {
		for j := 0; j < 1000; j++ {		//redis pipeline
			c.Send("setbit", "bitset", rand.Intn(maxUsers), 1)	//使数据尽量分散,减小压缩带来的占用降低
		}
		c.Flush()
	}

	res, _ := redis.String(c.Do("debug", "object", "bitset"))
	return getLength(res)
}

func testHash(c redis.Conn, times int) int {
	for i := 0; i < times; i += 1000 {
		args := make([]interface{}, 0)
		args = append(args, "hash")
		for j := 0; j < 1000; j++ {	//redis mset
			args = append(args, i+j+times)
			args = append(args, 1)
		}
		c.Do("HSET", args...)
	}
	res, _ := redis.String(c.Do("debug", "object", "hash"))
	return getLength(res)
}

func getLength(s string) int {			//获取debug命令中占用空间量
	strs := strings.Fields(s)
	length := strings.Split(strs[4], ":")[1]
	l, _ := strconv.Atoi(length)
	return l
}

实际测试代码,测试少量数据情况下【100~100000】及大量数据情况下的【100000,10000000】情况下的实际空间大小,并使用gonum/plot绘制出效果图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const maxUsers = 200000000		//假设公司有两亿用户

func main() {
	c, err := redis.Dial("tcp", ":6379")
	if err != nil {
		fmt.Println("err:", err)
		return
	}
	defer c.Close()
	p, _ := plot.New()
	p.Title.Text = "bitmap vs hash"
	p.X.Label.Text = "key nums"
	p.Y.Label.Text = "memory used"

	points := plotter.XYs{}
	points1 := plotter.XYs{}

	timess := []int{100, 1000, 2000, 5000, 10000, 30000, 45000, 60000, 100000}
	afterTest(c)
	for _, times := range timess {
		points = append(points, plotter.XY{float64(times), float64(testBit(c, times))})
		points1 = append(points1, plotter.XY{float64(times), float64(testHash(c, times))})
		afterTest(c)
	}
	plotutil.AddLinePoints(p, []interface{}{points, points1}...)
	p.Save(4*vg.Inch, 4*vg.Inch, "small.png")

	points3 := plotter.XYs{}
	points4 := plotter.XYs{}

	timess = []int{100000, 300000, 1000000, 2000000, 3000000, 10000000}
	afterTest(c)
	for _, times := range timess {
		points3 = append(points3, plotter.XY{float64(times), float64(testBit(c, times))})
		points4 = append(points4, plotter.XY{float64(times), float64(testHash(c, times))})
		afterTest(c)
	}
	plotutil.AddLinePoints(p, []interface{}{points3, points4}...)
	p.Save(4*vg.Inch, 4*vg.Inch, "large.png")
}

func afterTest(c redis.Conn) {
	c.Do("del", "bitset")
	c.Do("del", "hash")
}

实验结果如下:

小规模数据

在假定两亿用户的情况下,小规模数据测试情况下,可以发现6W左右数据存储的情况下,hash 和 bitmap使用的空间都为400~500KB,而用户小于6W的情况下使用hash稍有优势。而数据规模更大的时候,bitmap的存储优势就发挥了出来。

大规模数据

大规模场景下可以看出hash的空间占用为线性增长,而bitmap在大规模数据存储的情况下空间占用量的增长则稳定的多。