• Redis 复习计划 - String内存开销问题以及基本/扩展数据类型的使用


    一. String 类型内存开销问题

    RedisString类型保存值的时候,有两个选择:

    • 保存整数:采用int编码,将其保存为一个8字节的类型整数。
    • 保存字符:使用SDS结构保存。

    可见,Redis它本身并不傻,如果在String类型中,存储的值是一个整数,它会自动进行int编码。而保存的数据中若包含了字符,即采用简单动态字符串SDS结构体来保存。

    1.1 SDS 结构

    SDS,即Simple Dynamic String。低版本的Redis SDS结构如图(Redis3.2版本以下):
    在这里插入图片描述
    总共有三个部分:

    • len:表示buf的已用长度。占4个字节。
    • alloc:表示buf的时机分配长度,占4个字节。
    • buf:字节数组,保存真实的数据。Redis会在这个数组的末尾自动加一个"\0",代表结束标识。额外占用1个字节的开销。

    1.2 RedisObject 结构

    对于RedisString类型而言,还涉及到RedisObject结构。该结构体主要用于记录一些元数据记录(最后一次的访问时间,被引用的次数等等)包含了8个字节大小的元数据和8个字节大小的指针,共16字节。同时该结构体还指向实际的数据。如图:
    在这里插入图片描述
    意思就是,每当往Redis中插入一个String类型的键值对后,就会构建出对于的SDS结构(若是字符类型),以及一个额外的数据结构RedisObject(存储相关的元数据),并与之绑定。

    1.3 String 类型的内存布局优化

    到这里,我们可以知道,Redis中对于String类型的键值对存储,这几个部分可能是“多余”的:

    1. SDS中的len以及alloc
    2. RedisObject中的元数据以及指针ptr

    为了节省内存空间,实际上RedisLong类型的整数以及SDS的布局做了对应的优化:

    • 倘若保存的是Long类型整数:RedisObject中的指针就是整数本身,无需额外的指针去指向实际数据。
    • 倘若保存的是字符串数据:当字符串<=44字节的时候, RedisObject中的元数据以及指针ptrSDS是一块连续的内存区域。即embstr编码。目的:避免内存碎片。 否则,当字符串>44字节的时候,将会给SDS分配独立的空间,并用上图所示的方式,指针ptr去指向SDS结构,此时称之为raw编码。

    三种编码方式,用图所示如下:
    在这里插入图片描述

    我这里准备了一个Redis服务器,首先看下它的占用内存是多少:

    public static void main(String[] args) {
        Jedis jedis = new Jedis("xxx", 6379);
        //授权
        jedis.auth("xxx");
        System.out.println(jedis.info());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    结果打印出来如下:看used_memory,值为918704
    在这里插入图片描述
    倘若我此时插入一个key-Value

    jedis.set("20", "30");
    
    • 1

    再看下结果:看used_memory,值为918736
    在这里插入图片描述

    内存一共多了32B,分析如下:

    1. KeyValue都是整数。因此Redis会对其采用int编码。
    2. int编码情况下,使用RedisObject结构保存。其中元数据占8个字节,指针部分则由8字节的整数来代替。一共16字节。
    3. 因此Key+Value总共消耗的内存为32字节。

    除此之外,我们还知道,Redis用了一张全局的Hash表来保存所有的键值对。哈希表中的每一项是一个哈希桶,哈希桶中又包含了多个dictEntry的结构体,结构图如下:在这里插入图片描述
    dictEntry一共占用了24个字节大小。但是同时,Redis中有一个内存分配库jemalloc,当我们插入一个键值对的时候,会根据申请的字节数N,找一个比N大的最小二次幂作为分配的空间作为dictEntry的大小那么此时dictEntry的大小就是固定的32字节。

    也就是说,在假设Redis中没有任何数据的时候,执行set 20 20时,一共会占用64内存大小。但实际上,真实的数据却只有16字节。KeyValue各对应一个RedisObject,其中的指针(由于是int编码,因此转为整数本身)就是我们要的真实数据。

    同时我们还应该注意到:我们对于String类型的数据,每插入一条,就会对应的在全局哈希表中生成一个dictEntry结构体,占用32字节的大小。倘若有1亿条数据插入,就会生成1亿个dictEntry结构体。同时哈希桶还得不断地扩容,保证大小为2的N次幂。

    1.4 压缩列表的优势

    假设:在Redis中存储大量的Key-Value映射,比如set 用户Id 会员Id,然后用户Id和会员Id都是唯一,并且数据量很大,从下述Id处开始添加10000条数据。

    set 11010001 12010001
    伪代码就是
    for(int i=0; i <10000;i++){
    	set (11010001+ i) (12010001+i)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    首先来说下倘若使用String类型来存储的劣势:

    1. 每插入一条数据,对于生成一个32B大小的dictEntry
    2. RedisObject来存储这样的整形数据,虽然有int编码,但是还是有多余的元数据信息,占用8B。

    那么这里可以采用压缩列表来保存。压缩列表的数据结构如图:
    在这里插入图片描述

    • zlbytes:列表长度。
    • zltail:列表尾的偏移量。
    • zllen:列表中的 entry 个数。
    • zlend:表示列表结束。

    entry中的各个属性:

    • prev_len,表示前一个 entry 的长度。要么1字节(上一个entry的长度<254B)要么5字节。
    • len:表示自身长度,4 个字节。
    • encoding:表示编码方式,1 个字节。
    • content:保存实际数据。

    因此对于本文的案例来说,存储用户Id的时候,由于其字节大小不会超过254B,因此prev_len的大小为1B。那么每个entry的大小就是:1+4+1+8(Long整形)=14个字节,然后根据内存分配器的原则,取最靠近的二次幂数16,即每个entry大小为16字节。

    而我们向同一个压缩列表中添加数据的时候,只会改变压缩列表内entry的个数,而全局哈希表中,对于这个压缩列表生成的dictEntry对象个数却不会增加,这是和String类型存储的一个重要区别。

    测试如下,采用String类型添加500条数据:

    public static void main(String[] args) {
        Jedis jedis = new Jedis("xxx", 6379);
        //授权
        jedis.auth("xxx");
    
        String before = getMemorySize(jedis);
        System.out.println("Before Size: " + before);
    
        LongAdder key = new LongAdder();
        LongAdder value = new LongAdder();
        key.add(11010001);
        value.add(12010001);
        for (int i = 0; i < 500; i++) {
            key.add(1);
            value.add(1);
            jedis.set(key.toString(), value.toString());
        }
    
        String after = getMemorySize(jedis);
        System.out.println("After Size: " + after);
        System.out.println(Integer.parseInt(after) - Integer.parseInt(before));
    }
    static String getMemorySize(Jedis jedis) {
        String[] split = jedis.info().split("\r\n");
        String msg = "";
        for (String s : split) {
            if (s.contains("used_memory")) {
                msg = s;
                break;
            }
        }
        String[] res = msg.split(":");
        return res[1];
    }
    
    • 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

    结果如下:
    在这里插入图片描述
    倘若改成压缩列表:用户Id为11010001,我们取前五位作为压缩列表的键,然后后三位作为其key,会员Id作为value。代码:

    public static void main(String[] args) {
        Jedis jedis = new Jedis("xxx", 6379);
        //授权
        jedis.auth("xxx");
    
        String before = getMemorySize(jedis);
        System.out.println("Before Size: " + before);
    
        LongAdder key = new LongAdder();
        LongAdder value = new LongAdder();
        key.add(11010001);
        value.add(12010001);
        for (int i = 0; i < 500; i++) {
            key.add(1);
            value.add(1);
            // 压缩列表的key
            String hashKey = key.toString().substring(0, 5);
            // 集合内部每个entry的value
            String listValue = value.toString();
            // 集合内部每个entry的key
            String listKey = key.toString().substring(5, 7);
            jedis.hset(hashKey, listKey, listValue);
        }
    
        String after = getMemorySize(jedis);
        System.out.println("After Size: " + after);
        System.out.println(Integer.parseInt(after) - Integer.parseInt(before));
    }
    
    static String getMemorySize(Jedis jedis) {
        String[] split = jedis.info().split("\r\n");
        String msg = "";
        for (String s : split) {
            if (s.contains("used_memory")) {
                msg = s;
                break;
            }
        }
        String[] res = msg.split(":");
        return res[1];
    }
    
    • 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

    结果如下:

    在这里插入图片描述
    可见,压缩列表的使用,在这种场景下,比单纯的使用String类型,在内存消耗上要节省的多的多。

    不过有一点需要注意的是,RedisHash类型的底层数据结构有两种:压缩列表和哈希表。倘若数据超过一定的阈值,就会改用哈希表来存储,此时数据结构就并不像压缩列表那样紧凑了。相关的阈值涉及到两个:

    • hash-max-ziplist-entries:表示用压缩列表保存时哈希集合中的最大元素个数。
    • hash-max-ziplist-value:表示用压缩列表保存时哈希集合中单个元素的最大长度。

    我们取的是用户Id的后三位作为压缩列表的key,也就是说这个压缩列表中的数据个数不超过1000个。为了能充分使用压缩列表的精简内存布局,我们一般要控制保存在 Hash 集合中的元素个数。因此我们可以将 hash-max-ziplist-entries的值设置为1000。 这样Hash集合就可以使用压缩列表来节省空间了。

    到这里为止讲了什么内容?

    1. 在面对这种有一定规则(比如单调递增的Id),并且在Redis中存储的情况下,压缩列表比单纯的使用String类型一条条存储,在内存开销上,要少的多。
    2. 还讲了String类型在存储的时候,具体的内存消耗在哪些地方了。
    3. Redis高低版本中,关于SDS的结构以及其他数据结构可能会有所不同,因此在计算插入一个键值对的时候,计算内存大小前后可能会有所差异。

    二. Redis 基本操作和扩展集合的使用

    Redis中有5个基本数据类型:String、List、Hash、Set、Sorted Set

    2.1 基于 Redis 和 Java 的基本操作

    首先是Javapom依赖:

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.1.1</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接下来就给出Redis中关于基本数据类型的几种常见的命令操作。

    2.1.1 String

    Redis相关操作:

    # 指定key对应的值
    SET key value
    # 获取指定 key 的值
    GET key
    # 返回 key 中字符串值的子字符,即字符串的截取,包括start和end所在的位置
    GETRANGE key start end
    # 获取多个给定 key 的值
    MGET key1 [key2..]
    # 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)
    SETEX key seconds value
    # 只有在 key 不存在时设置 key 的值
    SETNX key value
    # 返回 key 所储存的字符串值的长度
    STRLEN key
    # 批量设置k-v
    MSET key value [key value ...]
    # 将 key 中储存的数字值增一
    INCR key
    # 将 key 所储存的值加上给定的增量值(increment)
    INCRBY key increment
    # 将 key 中储存的数字值减一
    DECR key
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    Java相关操作:

    // 向redis中添加数据
    jedis.set("k1", "v1");
    jedis.set("k2", "k2");
    jedis.set("k3", "k3");
    jedis.set("k4", "k4");
    jedis.set("k5", "k5");
    
    // 查看某个键对应值的数据类型
    String type = jedis.type("k1");
    // 获取redis中全部的key
    Set<String> keys = jedis.keys("*");
    // 删除redis中的一个键值对
    Long del = jedis.del("name");
    // 判断是否存在指定的key
    Boolean k1 = jedis.exists("k1");
    // 判断指定的key的过期时间
    Long k11 = jedis.ttl("k1");
    // 向redis中添加多个key-value
    String mSet = jedis.mset("test1", "111", "test2", "222", "test3", "333");
    // 获取redis中的多个key-value
    List<String> mGet = jedis.mget("test1", "test2", "test3");
    // 给redis中指定的key对应的值加一,可以加第二个参数,指定加/减多少数值,否则默认1
    Long incr = jedis.incr("presence");
    // 给redis中指定的key对应的值减一
    Long decr = jedis.decr("presence");
    // 清空redis中的数据
    String s = jedis.flushDB();
    
    • 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

    2.1.1 List

    Redis相关操作:

    # 通过索引获取列表中的元素
    LINDEX key index
    # 获取列表长度
    LLEN key
    # 移出并获取列表的第一个元素
    LPOP key
    # 将一个或多个值插入到列表头部
    LPUSH key value1 [value2]
    # 获取列表指定范围内的元素
    LRANGE key start stop
    # 移除列表元素
    LREM key count value
    # 通过索引设置列表元素的值
    LSET key index value
    # 移除列表的最后一个元素,返回值为移除的元素
    RPOP key
    # 在列表尾部中添加一个或多个值
    RPUSH key value1 [value2]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Java相关操作:

    // 向redis中从左边添加一个集合数据
    System.out.println("添加元素a,b,c,d");
    Long lPush = jedis.lpush("list1", "a", "b", "c", "d");
    // 查看list1集合中元素的个数
    System.out.println("list1集合中元素的个数:" + jedis.llen("list1"));
    // 从左边获取redis中的list1集合数据
    List<String> list1 = jedis.lrange("list1", 0, -1);
    System.out.println("获取集合数据:" + list1);
    // 从list1集合的左边弹出一个元素
    String lPop = jedis.lpop("list1");
    System.out.println("list1集合从左边弹出一个元素:" + lPop);
    // 从list1集合的右边弹出一个元素
    String rPop = jedis.rpop("list1");
    System.out.println("list1集合从右边弹出一个元素:" + rPop);
    // 查看list1集合中元素的个数
    System.out.println("list1集合中元素的个数:" + jedis.llen("list1"));
    // 向redis中从右边添加一个集合数据
    Long rPush = jedis.rpush("list2", "a", "b", "c", "d");
    // 查看list2集合中元素的个数
    Long list2 = jedis.llen("list2");
    // 设置下标为1的元素值为666
    jedis.lset("list2", 1, "666");
    List<String> list3 = jedis.lrange("list2", 0, -1);
    System.out.println("获取集合数据:" + list3);
    System.out.println("***********清空redis数据***********");
    //清空redis中的数据
    String s = jedis.flushDB();
    System.out.println(s);
    
    • 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

    结果如下:
    在这里插入图片描述

    2.1.1 Hash

    Redis相关操作:

    # 删除一个或多个哈希表字段
    HDEL key field1 [field2]
    # 查看哈希表 key 中,指定的字段是否存在
    HEXISTS key field
    # 获取存储在哈希表中指定字段的值
    HGET key field
    # 获取在哈希表中指定 key 的所有字段以及和
    HGETALL key
    # 获取所有哈希表中的字段
    HKEYS key
    # 获取哈希表中字段的数量
    HLEN key
    # 批量获取所有给定字段的值
    HMGET key field1 [field2]
    # 批量设置
    HMSET key field1 value1 [field2 value2 ]
    # 将哈希表 key 中的字段 field 的值设为 value
    HSET key field value
    # 获取哈希表中所有值
    HVALS key
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    Java相关操作:

    System.out.println("***********向redis中添加hash类型数据***********");
    // 向redis中添加hash类型数据(添加一个数据)
    Long hSet = jedis.hset("student1", "name", "李四");
    System.out.println("添加一个数据: name-李四");
    // 获取添加的一个数据
    String name = jedis.hget("student1", "name");
    System.out.println("获取name指定的数据: " + name);
    
    // 向redis中添加hash类型数据(添加多个数据)
    Map<String, String> map = new HashMap<>();
    map.put("name", "张三");
    map.put("sex", "男");
    map.put("address", "上海");
    String msg = jedis.hmset("student2", map);
    System.out.println("添加多个数据: " + msg);
    // 获取多个数据
    List<String> hmGet = jedis.hmget("student2", "name", "sex", "address");
    System.out.println("获取多个数据: " + hmGet);
    Long len = jedis.hlen("student2");
    System.out.println("哈希表中字段数量:" + len);
    
    System.out.println("***********清空redis数据***********");
    // 清空redis中的数据
    String s = jedis.flushDB();
    System.out.println(s);
    
    • 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

    结果如下:
    在这里插入图片描述

    2.1.1 Set

    Redis相关操作:

    # 向集合添加一个或多个成员
    SADD key member1 [member2]
    # 获取集合的成员数
    SCARD key
    # 返回第一个集合与其他集合之间的差异
    SDIFF key1 [key2]
    # 返回给定所有集合的交集
    SINTER key1 [key2]
    # 判断 member 元素是否是集合 key 的成员
    SISMEMBER key member
    # 返回集合中的所有成员
    SMEMBERS key
    # 移除集合中一个或多个成员
    SREM key member1 [member2]
    # 返回所有给定集合的并集
    SUNION key1 [key2]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Java相关操作:

    // 向redis中添加Set集合数据
    Long sAdd = jedis.sadd("set1", "a", "b", "c", "d", "d", "e", "f");
    // 获取redis中的set1集合数据
    Set<String> set1 = jedis.smembers("set1");
    System.out.println("获取Set集合数据:" + set1);
    // 删除redis中set1集合中指定的数据
    Long sRem = jedis.srem("set1", "a", "b");
    System.out.println("删除Set集合数据a,b");
    System.out.println("获取Set集合数据:" + jedis.smembers("set1"));
    
    System.out.println("***********Set交并差运算***********");
    System.out.println("set1:  a,b,c,d,e,f");
    System.out.println("set2:  a,b,g,h,i");
    Long sadd1 = jedis.sadd("set1", "a", "b", "c", "d", "e", "f");
    Long sadd2 = jedis.sadd("set2", "a", "b", "g", "h", "i");
    //获取交集
    Set<String> sinter = jedis.sinter("set1", "set2");
    System.out.println("交集:" + sinter);
    //获取并集
    Set<String> sunion = jedis.sunion("set1", "set2");
    System.out.println("并集:" + sunion);
    //获取差集
    Set<String> sdiff = jedis.sdiff("set1", "set2");
    System.out.println("差集:" + sdiff);
    System.out.println("***********清空redis数据***********");
    //清空redis中的数据
    String s = jedis.flushDB();
    System.out.println(s);
    
    • 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

    结果如下:
    在这里插入图片描述

    2.1.1 Sorted Set

    Redis相关操作:

    # 向有序集合添加一个或多个成员
    ZADD key score1 member1 [score2 member2]
    # 获取有序集合的成员数
    ZCARD key
    # 计算在有序集合中指定区间分数的成员数
    ZCOUNT key min max
    # 有序集合中对指定成员的分数加上增量
    ZINCRBY key increment member
    # 移除有序集合中的一个或多个成员
    ZREM key member [member ...]
    # 移除有序集合中给定的字典区间的所有成员
    ZREMRANGEBYLEX key min max
    # 返回有序集中指定区间内的成员,通过索引,分数从高到低
    ZREVRANGE key start stop [WITHSCORES]
    # 返回有序集中,成员的分数值
    ZSCORE key member
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    Java相关操作:由于主要还是用于排序,我感觉用的还是比较少的。就给个简单的用例吧。

    jedis.zadd("city", 10d, "上海");
    jedis.zadd("city", 30d, "温州");
    jedis.zadd("city", 20d, "北京");
    Set<String> city = jedis.zrange("city", 0, -1);
    System.out.println("排序:" + city);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结果如下:
    在这里插入图片描述

    2.2 扩展集合的使用

    此外Redis还有3种扩展数据类型,一般用于海量数据的统计以及特殊场景。有:BitmapHyperLogLogGEO

    2.2.1 Bitmap 二值统计

    首先来说下什么是二值统计:集合元素的取值只有0和1两种。那么典型的运用场景就是打卡签到了:

    1. 打卡了–>1。
    2. 未打卡–>0。

    首先来大概讲一下BitmapBitmap本身利用String类型作为底层的数据结构。可以保存二进制的字节数组,因此将字节数组的每个bit位代表一个元素的二值状态。Bitmap就相当于一个bit数组。

    Redis操作:

    SETBIT user.01.202208 0 1 ;
    SETBIT user.01.202208 2 1 ;
    SETBIT user.01.202208 7 1 ;
    # 统计
    BITCOUNT user.01.202208
    
    • 1
    • 2
    • 3
    • 4
    • 5

    那么代码怎么去编写呢?

    public static void main(String[] args) {
        Jedis jedis = new Jedis("xxx", 6379);
        //授权
        jedis.auth("xxx");
        String key = "user.01.202208";
        jedis.setbit(key, 0, true);
        jedis.setbit(key, 2, true);
        jedis.setbit(key, 3, true);
        jedis.setbit(key, 7, true);
        System.out.println(jedis.getbit(key,0));
        System.out.println(jedis.getbit(key,3));
        System.out.println(jedis.getbit(key,5));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    结果如下:
    在这里插入图片描述
    简单来说就是:

    1. Bitmap记录的是:用户编号为01,在2022年8月份的打卡记录。
    2. 用户在1号,3号,4号18号打了卡。因此jedis.getbit(key,5)的时候,是没打卡的,返回false。当然,你可以从1开始设置。只不过Bitmap的偏移量从0开始算。不过不影响。
    3. 那么这个月的打卡记录只要统计这个月中值为1的个数就可以了。如果在Redis中,可以命令操作:bitcount user.01.202208

    2.2.2 HyperLogLog 基数统计

    先来给个应用场景:基数统计。即统计一个集合中不重复的元素。

    如果是Set的使用,Redis命令:

    sadd key value;
    sadd key value2;
    # 统计命令
    scard key
    
    • 1
    • 2
    • 3
    • 4

    如果是Java

    jedis.sadd("mySet","123");
    jedis.sadd("mySet","1234");
    jedis.sadd("mySet","1235");
    jedis.sadd("mySet","123");
    Set<String> mySet = jedis.smembers("mySet");
    System.out.println(mySet.size());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果是Hash的使用,Redis命令:

    hset test user1 1;
    hset test user2 2;
    # 统计命令
    hlen test 
    
    • 1
    • 2
    • 3
    • 4

    Java命令:

    edis.hset("myhash","username","lcg");
    jedis.hset("myhash","username","lcg1");
    Map<String, String> myhash = jedis.hgetAll("myhash");
    System.out.println(myhash.size());
    
    • 1
    • 2
    • 3
    • 4

    但是这两种情况,在数据量非常大的情况下,SetHash类型都会消耗很大的内存空间。因此这里可以使用HyperLogLog来替代,HyperLogLog专门用来处理这种海量数据的基数操作。

    HyperLogLog其优势在于当集合元素数量非常多时,它计算基数所需的空间总是固定的,而且还很小。在 Redis 中,每个 HyperLogLog 只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数。

    例如,统计某个页面中,访问的用户有哪些:

    pfadd page user1 user2 user3 user4 user5;
    # 统计
    pfcount page;
    
    • 1
    • 2
    • 3

    HyperLogLog有一点需要值得注意,HyperLogLog虽然快,但是牺牲了一定的统计准确度:HyperLogLog 的统计规则是基于概率完成的,所以统计结果有一定误差,标准误算率是 0.81%。

    2.2.3 GEO 经纬度计算

    GEO主要涉及到经纬度的一个计算,例如车辆的定位信息,假设车辆 ID 是 666,经纬度位置是(120,40),我们可以用一个 GEO 集合保存所有车辆的经纬度,集合 keylocation

    GEOADD location 120 40 666
    
    • 1

    那么当我们需要统计这个坐标附近的车辆信息时候,我们就可以使用以下命令:

    # 在经纬度120°,40°附近10km范围内,寻找最近的10辆车辆。
    GEORADIUS location 120 40 10 km ASC COUNT 10
    
    • 1
    • 2

    其他的关于GEORedis操作如下:

    # 添加地理位置的坐标
    geoadd key 经度 纬度 menber
    # 获取地理位置的坐标
    geopos key member [memberN....]
    # 计算两个位置之间的距离
    geodist key member1 member2 [m|km|ft|mi]
    # 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
    georadius location 经度 纬度 举例 [m|km|ft|mi] [ASC|DESC] COUNT 10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Java相关操作:

    Jedis jedis = new Jedis("124.220.208.165", 6379);
    //授权
    jedis.auth("Ljj000..");
    // 添加北京坐标信息
    Long beijing = jedis.geoadd("china:city", 116.46, 39.92, "beijing");
    System.out.println("添加北京坐标信息:" + beijing);
    // 添加上海坐标信息
    Long shanghai = jedis.geoadd("china:city", 121.48, 31.22, "shanghai");
    System.out.println("添加上海坐标信息:" + shanghai);
    // 添加杭州坐标信息
    Long hangzhou = jedis.geoadd("china:city", 120.19, 30.26, "hangzhou");
    System.out.println("添加杭州坐标信息:" + hangzhou);
    // 获取北京的坐标信息
    List<GeoCoordinate> geoCoordinate = jedis.geopos("china:city", "beijing");
    System.out.println("获取北京的坐标信息:" + geoCoordinate);
    // 获取多个坐标信息
    List<GeoCoordinate> geoCoordinates = jedis.geopos("china:city", "beijing", "shanghai");
    System.out.println("获取多个坐标信息:" + geoCoordinates);
    // 获取北京到上海的直线距离
    Double distance = jedis.geodist("china:city", "beijing", "shanghai", GeoUnit.KM);
    System.out.println("获取北京到上海的直线距离(单位:KM):" + distance);
    // 获取距离指定点位距离的城市
    List<GeoRadiusResponse> citys = jedis.georadiusByMember("china:city", "beijing", 1500, GeoUnit.KM);
    System.out.println("获取距离指定点位距离的城市:");
    for (GeoRadiusResponse city:citys) {
        System.out.print(city.getMemberByString() + "\t");
    }
    System.out.println();
    // 获取指定经纬度多少距离以内的元素
    List<GeoRadiusResponse> geo1 = jedis.georadius("china:city", 116.46, 39.92, 1200, GeoUnit.KM);
    System.out.println("获取指定经纬度多少距离以内的元素: ");
    for (GeoRadiusResponse city:geo1) {
        System.out.print(city.getMemberByString() + "\t");
    }
    System.out.println();
    System.out.println("***********清空redis中的数据***********");
    //清空redis中的数据
    String s = jedis.flushDB();
    
    • 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

    结果如下:
    在这里插入图片描述

  • 相关阅读:
    基于微信小程序的火车购票系统源码
    【小型网站测试】使用python脚本来控制docker容器的编排
    python如何读写excel
    x64dbg 自动化控制插件
    Session的基本使用 [JavaWeb][Servlet]
    阿里Java面试题剖析:如何保证缓存与数据库的双写一致性?
    电脑文件夹加密怎么操作?保护数据4个方法分享!
    2023年MBA/MPA/MEM联考笔试答题抓分点
    蓝月亮,蓝禾,奇安信,三七互娱,顺丰,康冠科技,金证科技24春招内推
    GitHub私有派生仓库(fork仓库) | 派生仓库改为私有
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126157840