节衣缩食 —— 位图

2019/04/19 22:42
阅读数 51

在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。

为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。

位图数据结构

Redis 的位数组是自动扩展,如果设置了某个偏移位置超出了现有的内容范围,就会自动将位数组进行零扩充。

Bit实例

下面实现一个签到的例子:
考虑到每月初需要重置连续签到次数,最简单的方式是按用户每月存一条签到数据(也可以每年存一条数据)。Key的格式为u:sign:uid:yyyyMM,Value则采用长度为4个字节(32位)的位图(最大月份只有31天)。位图的每一位代表一天的签到,1表示已签,0表示未签。例如u:sign:1000:201902表示ID=1000的用户在2019年2月的签到记录。

        DateTime y2 = DateUtil.parse("2019-02-01");
        String key = buildSignKey(1000, y2.toJdkDate());

        // 偏移量是从0开始,所以要把17减1
        jedis.setbit(key, 16, true); // 用户2月17号签到
        jedis.setbit(key, 27, true); // 用户2月28号签到
        Assert.assertTrue(jedis.getbit(key, 16)); // 检查2月17号是否签到
        Assert.assertEquals(2,jedis.bitcount(key).longValue()); // 统计2月份的签到次数
        // u8,一个8位的无符号整数,i16是一个16位的有符号整数
        List<Long> list = jedis.bitfield(key, "GET", "u28", "0");// 获取2月份前28天的签到数据
        Map<String, Boolean> signMap = new HashMap(DateUtil.dayOfMonth(y2.toJdkDate()));
        if (list != null && list.size() > 0) {
            // 由低位到高位,为0表示未签,为1表示已签
            long v = list.get(0) == null ? 0 : list.get(0);
            for (int i = 28; i > 0; i--) {
                final DateTime dateTime = DateUtil.offsetDay(y2.toJdkDate(), i-1);
                signMap.put(DateUtil.format(dateTime, "yyyy-MM-dd"), v >> 1 << 1 != v);
                v >>= 1;
            }
        }
        Console.log(JSONUtil.toJsonPrettyStr(signMap));
        Assert.assertEquals(16, jedis.bitpos(key, true).longValue()); // 获取当月首次签到日期

打印信息如下:

{
    "2019-02-09": false,
    "2019-02-08": false,
    "2019-02-07": false,
    "2019-02-28": true,
    "2019-02-06": false,
    "2019-02-27": false,
    "2019-02-05": false,
    "2019-02-26": false,
    "2019-02-04": false,
    "2019-02-25": false,
    "2019-02-03": false,
    "2019-02-24": false,
    "2019-02-02": false,
    "2019-02-23": false,
    "2019-02-01": false,
    "2019-02-22": false,
    "2019-02-21": false,
    "2019-02-20": false,
    "2019-02-19": false,
    "2019-02-18": false,
    "2019-02-17": true,
    "2019-02-16": false,
    "2019-02-15": false,
    "2019-02-14": false,
    "2019-02-13": false,
    "2019-02-12": false,
    "2019-02-11": false,
    "2019-02-10": false
}

本文基于《Redis深度历险:核心原理和应用实践》一文的JAVA实践。更多文章请参考:高性能缓存中间件Redis应用实战(JAVA)

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部