详解Redis三种特殊类型数据结构(Bitmap、HyperLogLog、GEO)

1.概述

上文讲解了Redis五种基础数据类型的使用及场景,本文将分析Redis的3中特殊数据类型(Bitmap、HyperLogLog、GEO),这三种类型在特定场景下能有效提升数据处理效率、存储效率等。

2.数据类型详解

2.1 Bitmap

Bitmap是一个由位(bit)组成的图(map)。在计算机科学中,位一般只有两种状态:0或1,通常用来表示布尔值的真(true)或假(false)。Redis中的BitMap是基于String类型实现的,一个字符串的每个字节(8位)可以表示8个不同位,从而实现了位数组的功能。

2.1.1 Bitmap常用指令

命令 说明
SETBIT key offset value 设置指定offset位置的值
GETBIT key offset 获取指定offset位置的值
BITCOUNT key start end 统计指定范围内值为 1 的元素个数
BITPOS key bit start end 返回第一个被设置为bit值的位的位置
BITOP operation destkey key1 key2 ... 设置指定offset位置的值

2.1.2 Bitmap指令实测

bash 复制代码
> SETBIT sign 5 1
0
> SETBIT sign 3 1
0
> GETBIT sign 0
0
> GETBIT sign 3
1
> BITCOUNT sign 0 5
2
> BITPOS sign 1 0 5
3
> GETBIT sign 3
1
> SETBIT sign1 3 1
0
> SETBIT sign1 4 1
0
> BITOP AND sign2 sign sign1
1
> GETBIT sign2 3
1
> GETBIT sign2 4
0
> GETBIT sign2 5
0
> BITOP OR sign3 sign sign1
1
> GETBIT sign3 4
1

2.1.3 Bitmap使用场景

1.‌活跃用户统计

例如,可以用来记录网站的访问次数、用户登录次数等。

使用场景:使用日期作为 key,然后用户 id 为 offset,如果当日活跃过就设置为1。 ‌
2.用户行为统计

例如,文章评论、点赞等行为统计。

使用场景:用文章id作为key,用户id为offset,如果当日评论、点赞过就设置为1。 ‌
3.实现布隆过滤器

布隆过滤器是一种空间效率高的概率性数据结构,用于判断元素是否存在于集合中。它在大数据、缓存穿透防护、垃圾邮件过滤等场景中广泛应用。布隆过滤器可能存在误判,但它能以极小的内存代价完成高效的查询。

2.2 HyperLogLog(基数统计)

2.2.1 HyperLogLog常用指令

Redis在2.8.9版本引入了HyperLogLog 结构,HyperLogLog做数据统计的优势在于:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且占用内存很小。

HyperLogLog 是一种有名的基数计数概率算法,并非是redis独有,redis只是基于该算法提供了一些通用API,并且对 HyperLogLog 的存储进行了优化,在计数比较小时,它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间。

基数计数概率算法为了节省内存并不会直接存储元数据,而是通过一定的概率统计方法预估基数值(集合中包含元素的个数)。因此, HyperLogLog 的计数结果并不是一个精确值,存在一定的误差(标准误差为 0.81% )

命令 说明
PFADD key element1 element2 ... 添加一个或多个元素到 HyperLogLog 中
PFCOUNT key1 key2 获取一个或者多个 HyperLogLog 的唯一计数
PFMERGE destkey sourcekey1 sourcekey2 ... 将多个 HyperLogLog 合并到 destkey 中,destkey 会结合多个源,算出对应的唯一计数

2.2.2 HyperLogLog指令实测

bash 复制代码
> PFADD chars a b c d e
1
> PFADD chars f g
1
> PFCOUNT chars
7
> PFADD nums 1 2 3
1
> PFCOUNT chars nums
10
> PFMERGE destination chars nums
OK
> PFCOUNT destination
10

2.2.3 HyperLogLog使用场景

1.‌活跃用户统计

例如,计算网站的日活、7日活、月活数据等。

使用场景:将关键字+时间作为key(DAYLIVE+20251217),将活跃用户userId作为element,计算某一天的日活,只需要执行 DAYLIVE+20251217即可。每个月的第一天,执行 PFMERGE 将上一个月的所有数据合并成一个 HyperLogLog(MONTHLIVE_202512),再执行 PFCOUNT MONTHLIVE_202512,就得到了 12 月的月活数据。
2.统计注册 IP 数、统计在线用户、统计用户每天搜索不同词条的个数这些场景利用HyperLogLog均能实现,原理类似

2.3 GEO

Redis 的 Geospatial 基于 Sorted Set 实现提供了一种有效的方式来存储地理空间信息,例如地理位置坐标(经度和纬度)以及与之相关的数据。通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。

2.3.1 常用指令

命令 说明
GEOADD key longitude latitude member ... 将一个或多个成员的地理位置(经度和纬度)添加到指定的有序集合中
GEOPOS key member1 member2 ... 返回指定元素的经纬度信息
GEODIST key member1 member2 M/KM/FT/MI 返回两个给定元素之间的距离,M/KM/FT/MI: 指定半径的单位,可以是米(m)、千米(km)、英里(mi)、或英尺(ft)
GEORADIUS key longitude latitude radius M/KM/FT/MI 获取给定的经纬度为中心, 返回与中心的距离不超过给定最大距离的所有位置元素,支持 ASC(由近到远)、DESC(由远到近)、Count(数量) 等参数
GEORADIUSBYMEMBER key member radius distance 找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定

2.3.2 指令实测

bash 复制代码
> GEOADD location 116.33 39.89 user1 116.34 39.90 user2 116.35 39.88 user3 119.35 41.22 user4
3
> GEOPOS location user1
116.3299986720085144
39.89000061669732844
> GEODIST location user1 user2 km
1.4018
> GEODIST location user1 user4 km
294.9606
> GEORADIUS location 116.33 39.87 3 km
user3
user1
> GEORADIUS location 116.33 39.87 5 km
user3
user1
user2
> GEORADIUSBYMEMBER location user1 3 km
user3
user1
user2
> GEORADIUSBYMEMBER location user1 2 km
user1
user2

2.3.2 使用场景

1.‌需要管理地理位置的场景

例如,寻找附近的人。

使用场景:通过GEORADIUS获取当前用户指定距离范围内的人,如QQ、微信附近的人。

3.代码实现

java 复制代码
package com.eckey.lab.service.util;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    private static final Logger LOG = LoggerFactory.getLogger(RedisUtil.class);

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource(name = "strRedisTemplate")
    private RedisTemplate<String, String> stringRedisTemplate;

	 /**
	     * 创建布隆过滤器
	     *
	     * @param size          位数组大小
	     * @param hashFunctions 哈希函数数量
	     * @param value         元素值
	     */
    private List<Long> getHashPositions(String value, int hashFunctions, int size) {
        List<Long> positions = new ArrayList<>(hashFunctions);
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(value.getBytes(StandardCharsets.UTF_8));

            // 使用同一个MD5值生成多个哈希位置
            for (int i = 0; i < hashFunctions; i++) {
                long hashValue = 0;
                for (int j = i * 4; j < i * 4 + 4; j++) {
                    hashValue <<= 8;
                    int index = j % bytes.length;
                    hashValue |= (bytes[index] & 0xFF);
                }
                positions.add(Math.abs(hashValue % size));
            }
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5 algorithm not found", e);
        }
        return positions;
    }

    public void bloomFilterAdd(String key, String value, int hashFunctions, int size) {
        for (long position : getHashPositions(value, hashFunctions, size)) {
            stringRedisTemplate.opsForValue().setBit(key, position, true);
        }
    }

    public boolean bloomFilterContains(String key, String value, int hashFunctions, int size) {
        for (long position : getHashPositions(value, hashFunctions, size)) {
            if (Boolean.FALSE.equals(stringRedisTemplate.opsForValue().getBit(key, position))) {
                return false;
            }
        }
        return true;
    }

    public void hyperAdd(String key, String value) {
        stringRedisTemplate.opsForHyperLogLog().add(key, value);
    }

    public void hyperAdd(String key, String... values) {
        stringRedisTemplate.opsForHyperLogLog().add(key, values);
    }

    public Long hyperSize(String key) {
        return stringRedisTemplate.opsForHyperLogLog().size(key);
    }

    public void hyperDel(String key) {
        stringRedisTemplate.opsForHyperLogLog().delete(key);
    }

    public Long hyperUnion(String destKey, String srcKey1, String srcKey2) {
        return stringRedisTemplate.opsForHyperLogLog().union(destKey, srcKey1, srcKey2);
    }

    public Long hyperUnion(String destKey, String... srcKeys) {
        return stringRedisTemplate.opsForHyperLogLog().union(destKey, srcKeys);
    }
  public Long geoAdd(String key, double lat, double lon, String member) {
        return stringRedisTemplate.opsForGeo().add(key, new Point(lat, lon), member);
    }

    public List<Point> geoGet(String key, String member) {
        return stringRedisTemplate.opsForGeo().position(key, member);
    }

    public Distance geoDistance(String key, String member1, String member2, Metric metric) {
        return stringRedisTemplate.opsForGeo().distance(key, member1, member2, metric);
    }

    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadius(String key, double longitude, double latitude, double radius, RedisGeoCommands.DistanceUnit unit) {
        Point point = new Point(longitude, latitude);
        Circle circle = new Circle(point, new Distance(radius, unit));
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending();
        return stringRedisTemplate.opsForGeo().radius(key, circle, args);
    }

    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoNearByMember(String key, String member, double radius, RedisGeoCommands.DistanceUnit unit) {
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending();
        return stringRedisTemplate.opsForGeo().radius(key, member, new Distance(radius, unit), args);
    }

}

4.小结

1.Bitmap其实就是一个存储二进制数字(0 和 1)的数组,通过一个bit位可以表示某个元素对应的值或者状态,key 就是对应元素本身 。一个字节(byte)占8个bit,因此Bitmap能极大节省存储空间。

2.HyperLogLog数据结构在Redis中占用固定空间,因此不适合存储大量数据。同时它只能提供近似值,对于精确度要求较高的场景不太适用。Bitmap适合存储大量数据,但对于少量数据而言不够高效。

3.Geospatial index(地理空间索引)主要用于存储地理位置信息,适用于根据距离进行查询、统计等一系列场景。

5.参考文献

1.https://juejin.cn/post/6844903785744056333

2.https://javaguide.cn/database/redis/redis-data-structures-02.html

3.https://www.cnblogs.com/lykbk/p/15871615.html

4.https://hogwartsrico.github.io/2020/06/08/BloomFilter-HyperLogLog-BitMap/index.html

相关推荐
ShiXZ2138 小时前
Redis docker-compose 部署指南
redis·docker·eureka
YDS82911 小时前
大营销平台 —— 活动SKU库存扣减业务及其一致性处理
java·spring boot·redis·rabbitmq·ddd
爬也要爬着前进1 天前
redis主从搭建
数据库·redis·缓存
用户095367515831 天前
在系统的学习 redis 前的疑惑
redis·go
吴声子夜歌1 天前
Redis 3.x——集群故障转移
java·数据库·redis·集群
明豆1 天前
Redis 分布式缓存生产实战
redis·分布式·缓存
海兰1 天前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis
weixin_727535621 天前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf
William Dawson1 天前
【华为云 MRS Redis 安全集群 Kerberos 认证接入实战(Spring Boot \+ Jedis)】
redis·安全·华为云
zhouhui0011 天前
AI帮我写了个Spring Boot校验,线上漏掉了这组边界条件
java·spring boot·redis·ai编程