前言:此篇文章系本人学习过程中记录下来的笔记,里面难免会有不少欠缺的地方,诚心期待大家多多给予指教。
基础篇:
进阶篇:
接上期内容:上期完成了缓存双写一致性方面的学习。下面学习HyperLogLog/Geo/Bitmap实际案例,话不多说,直接发车。
聚合统计是指对一组数据进行汇总计算,得到一个或多个反映数据总体特征的统计量。常见的聚合操作包括求和、平均值、最大值、最小值、计数等。
排序统计指的是将数据按照特定的顺序(升序或降序)进行排列,然后基于排列后的结果开展统计分析工作。该过程可让数据的大小关系更加清晰,有助于挖掘数据中的规律和特征。
二值统计主要处理只有两种状态的数据,通常用 0 和 1 来表示。通过统计这两种状态各自出现的数量、比例等信息,来分析数据的特征和规律。
基数统计是对数据集合中不重复元素的数量进行统计,也称为去重计数。它关注的是集合中不同元素的个数,而不考虑元素出现的频率。
独立访客(去重),指在一定统计周期内访问某网站或应用的不同自然人的数量。通常会根据用户的设备信息(如 IP 地址、设备 ID 等)来判断是否为同一访客,同一用户在统计周期内无论访问多少次,都只计为 1 个 UV。
页面浏览量(不去重),是指在一定统计周期内,所有用户访问网站或应用页面的总次数。用户每打开或刷新一次页面,PV 就会增加 1 次。
日活跃用户(去重),指在一天内登录或使用过某网站或应用的独立用户数量。和 UV 类似,但强调的是 “活跃”,即当天有实际操作行为的用户。
月活跃用户(去重),指在一个月内登录或使用过某网站或应用的独立用户数量。
模拟统计某网站首页的UV(统计需要去重),一个用户一天内的多次访问只能算作一次。
public class HyperLogLogDemo {
public static void main(String[] args) throws Exception {
// 用线程池模拟1000000万个IP访问网站
ExecutorService executorService = Executors.newFixedThreadPool(1000);
// 用set集合来记录IP地址,最后对比HyperLogLog和set的数量,来计算误差
HashSet 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;
}
}
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 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 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 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 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)。
小总结:
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 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不可以访问");
}
}
}
}
优:
劣:
通过上述案例学习后,我对 HyperLogLog、Geo 以及 Bitmap 这三种 Redis 数据类型的理解得到了加深。
HyperLogLog 以极小的内存开销实现了对海量数据的基数统计。在案例中,通过它对每日访问用户数量的快速估算,让我清晰认识到在面对如网站 UV 统计、APP 日活计算这类需要在不精确统计的前提下大幅节省内存资源的场景时,HyperLogLog 无疑是最佳选择。
Geo 数据类型提供了强大的地理位置信息处理能力。无论是外卖平台查找附近商家、社交应用定位周边用户,Geo 都能发挥关键作用。
Bitmap 则以其按位存储的特性,在处理布尔类型数据时表现卓越。在案例中,利用 Bitmap 手写布隆过滤器拦截非法IP访问,使我理解了它在处理大量二值状态数据时的优势。在以后的项目中,如有像用户在线状态监控、活动参与情况统计等场景,Bitmap 能够以极低的内存占用和极快的操作速度完成任务,在后续的实际项目中具有极高的参考价值。
ps:努力到底,让持续学习成为贯穿一生的坚守。学习笔记持续更新中。。。。