🏆 一文 掌握 Redis的数据处理和分析 <Bitmap、HyperLogLog、GEO> 实战

一、Bitmap

1.1什么是Bitmap

Redis的 位图(bitmap) 是由0和1状态表现的二进制位的bit数组,主要适合在一些场景下进行空间的节省,并有意义的记录数据。例如一些大量的bool类型的存取,一个用户365天的签到记录,签到了是1,没签到是0,如果用普通的key/value进行存储,当用户量很大的时候,需要的存储空间是很大的。如果使用位图进行存储,一年365天,用365个bit就可以存储,365个bit换算成46个字节(一个稍长的字符串),如此就节省了很多的存储空间。

1.2 实战应用

场景:用于统计状态:是否登录、打卡上班签到统计

1.2.1 用mysql统计

签到一天就新增一条记录,亿级用户量下也会造用过多的内存

优化方案

一个月最多31天,int类型是32位,有来该位置就为1没有就是0

这样一行数据就记录一个月

1.2.2 用bitmap统计

在签到功能实现时,一个用户每天的签到只需要1个bit来实现

一个月只需要不超过31个bit,一年也不超过365个bit为

1.3 bitmap类型签到+结合布隆过滤器

1.3.1 什么是布隆过滤器

布隆过滤器是一种空间效率很高的数据结构,用于判断一个元素是否可能存在于一个集合中。

它的基本原理是通过多个哈希函数将元素映射到一个位数组中,通过检查这些位置上的标记来判断元素是否存在。

布隆过滤器:

不保存数据信息,只是在内存中做一个是否存在的标记flag

高效地插入和查询,占用空间少,返回的结果是不确定性+不够完美。

一个元素如果判断结果:存在时,元素不一定存在

但是判断结果为不存在时,则一定不存在。

1.3.2 布隆过滤器实战

场景模拟 :解决redis的缓存穿透问题

1.3.2.1 什么是缓存穿透

缓存穿透是指查询一个不存在的数据,由于缓存中没有,导致每次都要去数据库中查询,而数据库中也不存在,这样每次查询都会失败,而且查询压力会直接打到数据库上。这在并发量大的时候,会给数据库带来巨大的压力。

1.3.2.2 代码实战

1.导入pom

java 复制代码
<!-- Google Guava for BloomFilter -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>30.1-jre</version> 
</dependency>

<!-- Jedis for Redis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version> 
</dependency>

2.编写测试类

Java 复制代码
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import redis.clients.jedis.Jedis;

import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Set;

public class CachePenetrationPrevention {

    private static final int EXPECTED_INSERTIONS = 1000000; // 预估插入的元素数量
    private static final double FALSE_POSITIVE_PROBABILITY = 0.01; // 布隆过滤器误报率
    private static final String BLOOM_FILTER_KEY = "bloom_filter";
    private static final String BITMAP_PREFIX = "bitmap_";

    private BloomFilter<String> bloomFilter;
    private Jedis jedis;

    public CachePenetrationPrevention(Jedis jedis) {
        this.jedis = jedis;
        initializeBloomFilter();
    }

    private void initializeBloomFilter() {
        // 从Redis加载已有的bitmap数据
        Set<String> bitmapKeys = jedis.keys(BITMAP_PREFIX + "*");
        Set<String> values = new HashSet<>();
        for (String key : bitmapKeys) {
            byte[] bitmapBytes = jedis.get(key.getBytes(StandardCharsets.UTF_8));
            if (bitmapBytes != null) {
                // 将bitmap转换为HashSet以获取其中的所有元素
                BitSet bitSet = BitSet.valueOf(bitmapBytes);
                for (int i = 0; i < bitSet.size(); i++) {
                    if (bitSet.get(i)) {
                        values.add(String.valueOf(i));
                    }
                }
            }
        }
        // 创建布隆过滤器并添加已知不存在的值
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), EXPECTED_INSERTIONS, FALSE_POSITIVE_PROBABILITY);
        bloomFilter.putAll(values);
        // 将布隆过滤器数据存回Redis
        jedis.set(BLOOM_FILTER_KEY.getBytes(StandardCharsets.UTF_8), bloomFilter.toByteArray());
    }

    public boolean mightContain(String value) {
        return bloomFilter.mightContain(value);
    }

    public void put(String key, String value) {
        // 将数据存入Redis
        jedis.set(key, value);

        // 更新布隆过滤器
        bloomFilter.put(key);

        // 如果数据不存在,则更新bitmap
        if (value == null) {
            String bitmapKey = BITMAP_PREFIX + key;
            BitSet bitSet = new BitSet();
            bitSet.set(key.hashCode());
            jedis.set(bitmapKey.getBytes(StandardCharsets.UTF_8), bitSet.toByteArray());
        }

        // 更新Redis中的布隆过滤器数据
        jedis.set(BLOOM_FILTER_KEY.getBytes(StandardCharsets.UTF_8), bloomFilter.toByteArray());
    }

    public String get(String key) {
        // 使用布隆过滤器检查数据是否可能存在
        if (!mightContain(key)) {
            return null;
        }

        // 从Redis缓存中获取数据
        String value = jedis.get(key);
        if (value != null) {
            return value;
        }

        // 如果缓存中没有数据,则查询数据库
        value = queryDataFromDatabase(key);

        // 将查询结果存入缓存,并更新布隆过滤器和bitmap
        put(key, value);

        return value;
    }

    private String queryDataFromDatabase(String key) {
        // 这里应该是实际的数据库查询逻辑
        // 假设这里是从数据库中查询数据
        // 为简化示例,我们假设所有查询都返回null(即数据库中不存在数据)
        return null;
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost");
        CachePenetrationPrevention cachePenetrationPrevention = new CachePenetrationPrevention(jedis);
        }

上述代码的实现步骤为:

1.初始化布隆过滤器与Redis连接

需要初始化布隆过滤器,并设置其预期的插入元素数量和误报率。同时,建立与Redis的连接。

2.从Redis加载布隆过滤器和bitmap

如果Redis中已经存在布隆过滤器和bitmap的数据,我们需要将其加载到本地对象中。布隆过滤器可以以某种序列化形式存储,而bitmap则可以通过Redis的GETBIT或GETRANGE命令来获取。

3.实现数据添加功能

当有新数据需要缓存时,我们需要将数据添加到Redis中,并在布隆过滤器和bitmap中做相应的更新。

4.实现数据获取功能

当接收到一个查询请求时,首先使用布隆过滤器检查该数据是否可能存在于缓存中。如果布隆过滤器认为数据不存在,则直接返回null。如果布隆过滤器认为数据可能存在,则进一步查询Redis缓存。如果Redis中没有数据,则去数据库中查询。

5.实现数据库查询模拟

6.保存布隆过滤器和bitmap到Redis

在适当的时候(例如应用程序关闭前),需要将布隆过滤器和bitmap的数据保存到Redis中,以便在下次启动时可以加载它们。

二、Hyperloglog

2.1 什么是HyperLogLog

HyperLogLog 是一种用于进行近似去重计数的算法 它可以在只使用相对较少的内存空间的情况下,较为准确地估计一个集合中不同元素的数量。

2.2常见名词解释

用于统计网站或者应用中的流量统计

常见名词 解释
UV "Unique Visitor":即独立访客数、它表示在一定统计周期内访问某个网站或应用的不同用户数量、这些用户是唯一的,即每个用户只计算一次。
PV "Page View":页面浏览量、它表示网站被访问的总页面数、用户每打开一个网页就被计为 1 个 PV
DAU "Daily Active User":指日活跃用户数量、它反映了应用或网站在一天内的活跃用户情况。
MAU Monthly Active User":月活跃用户数、这是衡量一个产品或服务在一个月内的活跃用户规模的指标

2.3 实战应用

场景 :统计电商网站首页每天的UV

2.3.1 用mysql统计

用一张表来记录,字段为用户IP和用户ID ,亿级访问量下占太多内存

2.3.2 用redis的hash统计

hash的底层数据结构可以去除重复的数据,这样咋一看符合场景的需求,但是在亿级访问量的情况下,还是会出现内存占用太多的问题,因为一个ip地址大约为15byte, 在亿级的数量情况下也要占用2GB以上,一个月估计60G,不久Redis就会内存不足。

2.3.3 用HyperLogLog统计

每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数

代码实战

1.导入pom

java 复制代码
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version> 
</dependency>

2.编写测试类

java 复制代码
import com.google.common.hash.HashCode;
import com.google.common.hash.Hashing;
import com.google.common.math.IntMath;
import com.google.common.primitives.Ints;

public class UVStatistics {

    private static final int PRECISION = 10; // 设置精度,这会影响估计的误差率
    private static final HyperLogLogPlus plus = HyperLogLogPlus.Builder.withExpectedSize(1000000, PRECISION).build();

    public static void main(String[] args) {
        // 假设这些是网站首页的访客ID
        String[] visitorIds = {
                "visitor1",
                "visitor2",
                "visitor1",
                "visitor3",
                "visitor4",
                "visitor2",
                // ... 更多访客ID
        };

        // 将每个访客ID转换为哈希码,然后添加到HyperLogLog中
        for (String visitorId : visitorIds) {
            HashCode hashCode = Hashing.sha256().hashString(visitorId, Charsets.UTF_8);
            plus.offer(hashCode.asBytes());
        }

        // 估计唯一访客数量
        long estimatedUniqueVisitors = plus.cardinality();
        System.out.println("Estimated unique visitors: " + estimatedUniqueVisitors);
    }
}

首先为HyperLogLogPlus实例设置了一个预期的大小和精度。然后,我们遍历所有访客ID,将每个ID转换为一个哈希码,并将哈希码添加到HyperLogLogPlus实例中。最后,我们调用cardinality()方法来获取估计的唯一访客数量。

HyperLogLogPlus是Guava库提供的一个更精确的变种,它允许指定精度。精度越高,所需的内存就越多,但估计的准确性也会提高。

实际应用中,需要从网站日志或其他数据源中读取访客ID,而不是硬编码它们。同时,考虑到性能和内存使用,你可能需要定期合并或重置HyperLogLog实例,或者使用分布式版本的HyperLogLog算法来处理大规模数据。

三、GEO

3.1 什么是GEO

GEO 是 Redis 在 3.2 版本中新增的一个功能模块,主要用于存储地理位置信息,并对存储的信息进行操作,用于实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能。

3.2 实战应用

场景: 推送附近的一些商品:酒店、加油站、超市等

实现关键点: georadius 以给定的经纬度为中心, 找出某一半径内的元素

代码实战

1.导入pom

java 复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version> 
</dependency>

2.编写测试类

java 复制代码
import redis.clients.jedis.GeoCoordinate;
import redis.clients.jedis.GeoRadiusResponse;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.GeoRadiusParam;

import java.util.List;

public class HotelPushExample {

    public static void main(String[] args) {
        // 创建Jedis实例连接到Redis服务器
        Jedis jedis = new Jedis("localhost");

        // 添加酒店位置数据
        jedis.geoadd("hotels", new GeoCoordinate(113.898323,22.587032), "Baocube International Hotel");
        jedis.geoadd("hotels", new GeoCoordinate(113.894797,22.585719), "Vienna Hotels");

        // 设置查询参数
        double longitude = 116.4;
        double latitude = 39.9;
        double distance = 100; // 100公里
        GeoRadiusParam param = new GeoRadiusParam()
                .withUnit("km")
                .withSort(GeoRadiusParam.Sort.ASC)
                .withDistance(distance)
                .withCoord(longitude, latitude);

        // 查询附近酒店
        GeoRadiusResponse<String> response = jedis.georadius("hotels", longitude, latitude, distance, "km", param);

        // 处理查询结果
        List<GeoRadiusResponse.GeoRadiusEntry<String>> entries = response.getRaw();
        for (GeoRadiusResponse.GeoRadiusEntry<String> entry : entries) {
            String hotelName = entry.getName();
            double distanceFromUser = entry.getDistance();

            // 在这里可以将酒店名称和距离信息推送给用户
            System.out.println("Hotel: " + hotelName + ", Distance: " + distanceFromUser + " km");
        }

        // 关闭Jedis连接
        jedis.close();
    }
}

1.使用geoadd命令添加了两个酒店的位置数据到GeoSet中。

2.设置了查询参数,包括经度、纬度、距离单位、排序方式等,并使用georadius命令查询了附近100公里内的酒店。

3.我们遍历了查询结果,并打印出了每个酒店的名称和距离用户的位置。

四、总结

本文主要介绍了Bitmap、HyperLogLog、GEO 的意思以及实战应用,在redis的十大类型下篇详细介绍了这些的常用命令:Redis的十大数据类型下篇

Bitmap 可用于高效存储和处理大量的布尔型数据;HyperLogLog 能以较小的空间成本估算集合的基数;GEO 则在地理空间数据处理方面发挥重要作用,可实现地理位置的存储、查询和分析等操作。通过这些技术的结合运用,我们能够更好地应对各种数据处理需求,提升系统性能和效率。

相关推荐
爱上语文26 分钟前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people30 分钟前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
小安运维日记1 小时前
Linux云计算 |【第四阶段】NOSQL-DAY1
linux·运维·redis·sql·云计算·nosql
罗政6 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
Karoku0666 小时前
【网站架构部署与优化】web服务与http协议
linux·运维·服务器·数据库·http·架构
码农郁郁久居人下6 小时前
Redis的配置与优化
数据库·redis·缓存
拾光师7 小时前
spring获取当前request
java·后端·spring
Hsu_kk8 小时前
Redis 主从复制配置教程
数据库·redis·缓存
DieSnowK8 小时前
[Redis][环境配置]详细讲解
数据库·redis·分布式·缓存·环境配置·新手向·详细讲解
Lill_bin9 小时前
深入理解ElasticSearch集群:架构、高可用性与数据一致性
大数据·分布式·elasticsearch·搜索引擎·zookeeper·架构·全文检索