Redis7——进阶篇(四)
前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。
基础篇:
- Redis(一)
- Redis(二)
- Redis(三)
- Redis(四)
- Redis(五)
- Redis(六)
- Redis(七)
- Redis(八)
进阶篇:
- Redis(九)
- Redis(十)
- Redis(十一)
接上期内容:上期完成了缓存双写一致性方面的学习。下面学习HyperLogLog/Geo/Bitmap实际案例,话不多说,直接发车。
一、HyperLogLog类型应用场景
(一)、常用的统计方式
1、聚合统计
1.1、定义
聚合统计是指对一组数据进行汇总计算,得到一个或多个反映数据总体特征的统计量。常见的聚合操作包括求和、平均值、最大值、最小值、计数等。
1.2、应用场景
- 电商领域:统计某一时间段内的销售总额、平均订单金额、商品销售数量等。例如,计算某店铺在一个月内的总销售额,就可以对每天的销售额进行求和操作。
- 金融领域:计算某一投资组合的平均收益率、最大回撤等指标。
2、排序统计
2.1、定义
排序统计指的是将数据按照特定的顺序(升序或降序)进行排列,然后基于排列后的结果开展统计分析工作。该过程可让数据的大小关系更加清晰,有助于挖掘数据中的规律和特征。
2.2、应用场景
- 教育领域:学校会按照学生的考试成绩进行排名。
- 电商平台:对商品按照销量、价格、评分等因素进行排序。
- 体育赛事:根据运动员的比赛成绩进行排名,确定冠亚季军等名次。
3、二值统计
3.1、定义
二值统计主要处理只有两种状态的数据,通常用 0 和 1 来表示。通过统计这两种状态各自出现的数量、比例等信息,来分析数据的特征和规律。
3.2、应用场景
- 医疗检测:在疾病检测中,检测结果通常为阳性(1)或阴性(0)。
- 网络安全:判断用户登录是否成功,成功记为 1,失败记为 0。通过统计登录成功和失败的次数,可以分析系统的安全性,检测是否存在异常登录行为,及时采取防范措施。
- 投票系统:选民对某个提案进行投票,支持记为 1,反对记为 0。统计支持和反对的票数,能够快速得出投票结果,反映选民的意愿。
4、基数统计
4.1、定义
基数统计是对数据集合中不重复元素的数量进行统计,也称为去重计数。它关注的是集合中不同元素的个数,而不考虑元素出现的频率。
4.2、应用场景
- 网站分析:统计网站的独立访客数,即不同用户的访问数量。通过基数统计,网站运营者可以了解网站的用户覆盖范围和流量质量,评估网站的影响力和市场竞争力。
(二)、统计名词解释
1、UV(Unique Visitor)
独立访客(去重),指在一定统计周期内访问某网站或应用的不同自然人的数量。通常会根据用户的设备信息(如 IP 地址、设备 ID 等)来判断是否为同一访客,同一用户在统计周期内无论访问多少次,都只计为 1 个 UV。
2、PV(Page View)
页面浏览量(不去重),是指在一定统计周期内,所有用户访问网站或应用页面的总次数。用户每打开或刷新一次页面,PV 就会增加 1 次。
3、DAU(Daily Active User)
日活跃用户(去重),指在一天内登录或使用过某网站或应用的独立用户数量。和 UV 类似,但强调的是 “活跃”,即当天有实际操作行为的用户。
4、MAU(Monthly Active User)
月活跃用户(去重),指在一个月内登录或使用过某网站或应用的独立用户数量。
(三)、案例需求说明
模拟统计某网站首页的UV(统计需要去重),一个用户一天内的多次访问只能算作一次。
(四)、模拟案例实现
public class HyperLogLogDemo {
public static void main(String[] args) throws Exception {
// 用线程池模拟1000000万个IP访问网站
ExecutorService executorService = Executors.newFixedThreadPool(1000);
// 用set集合来记录IP地址,最后对比HyperLogLog和set的数量,来计算误差
HashSet<String> set = new HashSet<>(1000000);
for (int i = 0; i < 1000000; i++) {
executorService.submit(() -> {
try (Jedis jedis = RedisUtils.getJedis()) {
String ip = generateValidIp();
jedis.pfadd("ip", ip);
set.add(ip);
} catch (Exception e) {
e.printStackTrace();
}
});
}
// 关闭线程池,不再接受新任务
executorService.shutdown();
try {
// 等待所有任务执行完毕,最多等待 1 分钟
if (!executorService.awaitTermination(1, TimeUnit.MINUTES)) {
// 如果超时,强制关闭线程池
executorService.shutdownNow();
}
} catch (InterruptedException e) {
// 如果线程被中断,强制关闭线程池
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
try (Jedis jedis = RedisUtils.getJedis()) {
long hyperLogLogCount = jedis.pfcount("ip");
System.out.println("hyperLogLog统计结果:" + hyperLogLogCount);
System.out.println("HashSet统计结果:" + set.size());
// 计算相对误差
double relativeError = Math.abs((double) (hyperLogLogCount - set.size()) / set.size()) * 100;
System.out.printf("相对误差: %.2f%%\n", relativeError);
}
}
private static String generateValidIp() {
Random r = new Random();
int part1 = r.nextInt(254) + 1;
int part2 = r.nextInt(256);
int part3 = r.nextInt(256);
int part4 = r.nextInt(256);
return part1 + "." + part2 + "." + part3 + "." + part4;
}
}
符合官网文档的描述误差描述,最大误差率:0.81%。
二、GEO 类型数据的应用
(一)、前提说明
GEO 提供了地理位置相关的操作,允许存储和查询地理位置信息。GEO 类型主要使用有序集合(Sorted Set)来实现,存储在有序集合中,同时以成员名称作为键。
(二)、需求说明
假设我们要开发一个简单的旅游系统,需要根据用户的当前位置,查找附近的旅游景点。具体需求如下:
- 能够存储景区的地理位置信息。
- 当用户提供自己的位置时,能够快速查找出距离用户一定半径范围内的景区。
- 能够计算用户与景区之间的距离。
获取位置地标:拾取坐标系统
(三)、模拟案例实现
public class GeoDemo {
private static final String SCENIC_SPOT_KEY = "key";
private static final Jedis jedis;
static {
try {
jedis = RedisUtils.getJedis();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// (1)添加景区的地理位置信息
public static void addScenicSpots() {
Map<String, GeoCoordinate> scenicSpots = new HashMap<>();
scenicSpots.put("天安门", new GeoCoordinate(116.403963, 39.915119));
scenicSpots.put("长城", new GeoCoordinate(116.024067, 40.362639));
scenicSpots.put("故宫", new GeoCoordinate(116.403414, 39.924091));
scenicSpots.put("颐和园", new GeoCoordinate(116.27889, 39.998961));
for (Map.Entry<String, GeoCoordinate> entry : scenicSpots.entrySet()) {
String name = entry.getKey();
GeoCoordinate coordinate = entry.getValue();
jedis.geoadd(SCENIC_SPOT_KEY, coordinate.getLongitude(), coordinate.getLatitude(), name);
}
}
// (2)查找自身附近的景区
public static List<GeoRadiusResponse> findNearbyScenicSpots(double userLongitude, double userLatitude, double radius) {
return jedis.georadius(SCENIC_SPOT_KEY, userLongitude, userLatitude, radius, GeoUnit.KM);
}
// (3)计算自身与景区之间的距离
public static Double calculateDistance(double userLongitude, double userLatitude, String restaurantName) {
// 先把用户位置添加进去以便计算距离
jedis.geoadd(SCENIC_SPOT_KEY, userLongitude, userLatitude, "user_location");
return jedis.geodist(SCENIC_SPOT_KEY, "user_location", restaurantName, GeoUnit.KM);
}
public static void main(String[] args) {
// 添加景区信息
addScenicSpots();
// 用户的当前位置(北京西站)
double userLongitude = 116.328175;
double userLatitude = 39.900772;
List<GeoRadiusResponse> nearbyRestaurants = findNearbyScenicSpots(userLongitude, userLatitude, 10);
System.out.println("根据定位查找附近10KM的景区:");
for (GeoRadiusResponse response : nearbyRestaurants) {
System.out.println(response.getMemberByString());
}
System.out.println("====================================================");
Double distance = calculateDistance(userLongitude, userLatitude, "长城");
System.out.println("北京西站距离长城:" + distance + "km");
// 关闭 Jedis 连接
jedis.close();
}
}
三、布隆过滤器的实际应用
(一)、定义
布隆过滤器(Bloom Filter)是由 Burton Howard Bloom 在 1970 年提出的一种空间效率极高的概率型数据结构。它用于判断一个元素是否存在于一个集合中,其返回结果有两种可能:可能存在或者一定不存在。布隆过滤器实际上是一个很长的二进制向量(可以看成bitmap处理)和一系列随机映射函数。
总结:布隆过滤器 ≈ bitmap + N 个 Hash 函数。
(二)、原理
布隆过滤器的核心原理基于哈希函数和二进制(0或1)。
- 初始化:创建一个bitmap,初始化所有位置都为0。同时创建N个哈希函数。
- 插入元素:当要插入一个元素时,将该元素通过N个哈希函数分别计算出哈希值,然后这些哈希值分别对bitmap的长度取模运算得到对应的位置,最后将对应位置置为1。N个哈希函数是为了解决哈希冲突问题,使其能够均匀分布。
- 查询元素:当查询一个元素是否存在时,同样使用这 N 个哈希函数计算该元素的 N 个哈希值,然后在对bitmap长度取模运算得到对应的位置,最后判断位置在bitmap中是否为1。如果有任何一个位置为 0,则该元素一定不存在;如果所有位置都为 1,则该元素可能存在。
小总结:
- 存在,不一定存在,但不存在,一定不存在。(重点)
- 使用时最好不要让实际元素数量远大于(bitmap)初始化数量,一次给够,避免扩容。
- 当实际元素数量超过初始化数量时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进行。
(三)、功能
- 快速判断元素是否存在:布隆过滤器能够在常数时间 O(N) 内完成元素的查询操作,其中 N 是哈希函数的数量。这种高效性使得它在处理大规模数据时非常实用。
- 节省空间:相比于传统的数据结构(如哈希表),布隆过滤器只需要存储二进制,不需要存储实际的元素本身,因此可以大大节省存储空间。
(四)、使用场景
- 安全防护:对于海量的 IP 地址、恶意域名、恶意请求等信息,可以快速判断是否属于恶意范畴,从而进行拦截。
- 缓存穿透防护:在缓存系统中,当大量请求查询不存在于缓存和数据库中的数据时,会导致缓存穿透问题,增加数据库的压力。布隆过滤器可以在请求到达数据库之前进行过滤,判断请求的数据是否可能存在,从而减少不必要的数据库查询。
- 垃圾邮件过滤:在邮件系统中,可以使用布隆过滤器来判断一个邮件地址是否在垃圾邮件列表中。如果布隆过滤器判断该地址可能是垃圾邮件地址,则可以直接将邮件标记为垃圾邮件。
(五)、模拟实现
public class BloomFilterDemo {
public static final Jedis jedis;
static {
try {
jedis = RedisUtils.getJedis();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 初始化布隆过滤器
*/
public static void bloomFilterInit() throws Exception {
// 白名单ip预加载到布隆过滤器
String ip = "ip:192.9.201.99";
// 1 计算 hashcode,由于可能有负数,直接取绝对值
int hashValue = Math.abs(ip.hashCode());
// 2 通过 hashValue 和 2 的 32 次方取余后(假设bitmap的初始大小为2^32),获得对应的下标坑位
long index = hashValue & ((1L << 32) - 1);
System.out.println(ip + " 对应------坑位 index:" + index);
// 3 设置 redis 里面 bitmap 对应坑位,该有值设置为 1
RedisUtils.getJedis().setbit("whitelistIp", index, true);
}
/**
* 检测过滤非法ip
*/
public static boolean checkWithBloomFilter(String checkItem, String key) {
int hashValue = Math.abs(key.hashCode());
long index = hashValue & ((1L << 32) - 1);
boolean existOK = jedis.getbit(checkItem, index);
System.out.println("----->key:" + key + " 对应坑位index:" + index + " 是否存在:" + existOK);
return existOK;
}
public static void main(String[] args) throws Exception {
// 初始化布隆过滤器
bloomFilterInit();
// 假设有这些ip地址向服务器发起请求,来查询数据
List<String> ipList = new ArrayList<>();
ipList.add("192.9.201.99");
ipList.add("192.9.202.19");
ipList.add("192.168.202.75");
for (String ip : ipList) {
boolean result = checkWithBloomFilter("whitelistIp", "ip:" + ip);
if (result) {
System.out.println("正常IP可以访问");
// TODO:查缓存 → 查数据库..... → 返回结果
} else {
System.out.println("非法IP不可以访问");
}
}
}
}
(六)、优劣势
优:
- 空间效率高:布隆过滤器不需要存储实际的元素,因此在处理大规模数据时可以节省大量的存储空间。
- 查询速度快:布隆过滤器的查询操作时间复杂度为 O(N),其中 N 是哈希函数的数量,通常 N 是一个较小的常数,因此查询速度非常快。
- 实现简单:布隆过滤器的实现相对简单,只需要使用bitmap和哈希函数即可。
劣:
- 存在误判概率:布隆过滤器只能判断元素可能存在或者一定不存在,存在一定的误判率。当判断元素可能存在时,实际上该元素可能并不存在。
- 不能删除元素:由于布隆过滤器的多个元素可能会映射到同一个二进制位上,因此无法直接删除某个元素。
四、总结
通过上述案例学习后,我对 HyperLogLog、Geo 以及 Bitmap 这三种 Redis 数据类型的理解得到了加深。
HyperLogLog 以极小的内存开销实现了对海量数据的基数统计。在案例中,通过它对每日访问用户数量的快速估算,让我清晰认识到在面对如网站 UV 统计、APP 日活计算这类需要在不精确统计的前提下大幅节省内存资源的场景时,HyperLogLog 无疑是最佳选择。
Geo 数据类型提供了强大的地理位置信息处理能力。无论是外卖平台查找附近商家、社交应用定位周边用户,Geo 都能发挥关键作用。
Bitmap 则以其按位存储的特性,在处理布尔类型数据时表现卓越。在案例中,利用 Bitmap 手写布隆过滤器拦截非法IP访问,使我理解了它在处理大量二值状态数据时的优势。在以后的项目中,如有像用户在线状态监控、活动参与情况统计等场景,Bitmap 能够以极低的内存占用和极快的操作速度完成任务,在后续的实际项目中具有极高的参考价值。
ps:努力到底,让持续学习成为贯穿一生的坚守。学习笔记持续更新中。。。。