一、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 则在地理空间数据处理方面发挥重要作用,可实现地理位置的存储、查询和分析等操作。通过这些技术的结合运用,我们能够更好地应对各种数据处理需求,提升系统性能和效率。