Redis 高级数据结构:Bitmap、HyperLogLog、GEO 深度解析

发布于:2025-09-06 ⋅ 阅读:(23) ⋅ 点赞:(0)

🔥 Redis 高级数据结构:Bitmap、HyperLogLog、GEO 深度解析

🧠 一、高级数据结构全景图

💡 Redis 高级数据结构价值

位级操作
基数统计
地理位置
业务问题
选择解决方案
Bitmap
HyperLogLog
GEO
用户签到
布隆过滤器
UV统计
去重计数
附近的人
范围查询

为什么需要高级数据结构​​:

  • 🚀 ​​极致性能​​:特殊优化,远超普通实现
  • 💾 ​​内存高效​​:相同数据占用更少内存
  • 🔧 ​​功能专精​​:为解决特定问题而生
  • ⚡ ​​生产验证​​:经过大规模应用验证

🔢 二、Bitmap:位操作的艺术

💡 内部原理与特性

​​Bitmap 本质上是 String 类型​​,但 Redis 提供了专门的位操作命令。每个 bit 位可以存储 0 或 1,极其节省内存。

用户ID
计算偏移量
位操作
结果统计

内存计算示例​​:

  • 1000万用户签到数据 ≈ 10000000 / 8 / 1024 / 1024 ≈ 1.19MB
  • 相同数据用 Set 存储 ≈ 至少 100MB

🚀 常用命令详解

# 设置指定偏移量的位值
SETBIT user:sign:202310 100 1    # 用户ID=100在2023/10/01签到

# 获取位值
GETBIT user:sign:202310 100      # 检查用户是否签到

# 统计位数为1的数量
BITCOUNT user:sign:202310        # 统计当天签到总数

# 位运算操作
BITOP AND destkey key1 key2      # 位与运算
BITOP OR destkey key1 key2       # 位或运算
BITOP XOR destkey key1 key2      # 位异或运算
BITOP NOT destkey key           # 位非运算

# 查找第一个设置或未设置的位
BITPOS user:sign:202310 1        # 第一个签到的用户
BITPOS user:sign:202310 0        # 第一个未签到的用户

🎯 应用场景案例

​​1. 用户签到系统​​:

public class SignService {
    // 用户签到
    public void sign(Long userId) {
        LocalDate today = LocalDate.now();
        String key = "user:sign:" + today.format(DateTimeFormatter.ofPattern("yyyyMM"));
        long offset = userId % 1000000; // 用户ID偏移量
        
        redis.setbit(key, offset, 1);
        
        // 设置过期时间(1个月)
        redis.expire(key, 30 * 24 * 60 * 60);
    }
    
    // 检查签到状态
    public boolean hasSigned(Long userId) {
        LocalDate today = LocalDate.now();
        String key = "user:sign:" + today.format(DateTimeFormatter.ofPattern("yyyyMM"));
        long offset = userId % 1000000;
        
        return redis.getbit(key, offset) == 1;
    }
    
    // 统计当月签到人数
    public long getMonthSignCount() {
        LocalDate today = LocalDate.now();
        String key = "user:sign:" + today.format(DateTimeFormatter.ofPattern("yyyyMM"));
        
        return redis.bitcount(key);
    }
    
    // 获取连续签到天数
    public int getContinuousSignDays(Long userId) {
        List<byte[]> bitFields = new ArrayList<>();
        LocalDate endDate = LocalDate.now();
        LocalDate startDate = endDate.minusDays(30);
        
        // 获取最近30天的签到数据
        while (!startDate.isAfter(endDate)) {
            String key = "user:sign:" + startDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
            bitFields.add(redis.get(key.getBytes()));
            startDate = startDate.plusDays(1);
        }
        
        // 计算连续签到天数(实际实现需要更复杂的位运算)
        return calculateContinuousDays(bitFields, userId);
    }
}

​​2. 布隆过滤器辅助实现​​:

public class SimpleBloomFilter {
    private static final int SIZE = 2 << 24; // 布隆过滤器大小
    private static final int[] SEEDS = new int[]{3, 5, 7, 11, 13, 17, 19, 23}; // 哈希种子
    
    public boolean mightContain(String value) {
        for (int seed : SEEDS) {
            int hash = hash(value, seed) % SIZE;
            if (redis.getbit("bloom:filter", hash) == 0) {
                return false;
            }
        }
        return true;
    }
    
    public void add(String value) {
        for (int seed : SEEDS) {
            int hash = hash(value, seed) % SIZE;
            redis.setbit("bloom:filter", hash, 1);
        }
    }
    
    private int hash(String value, int seed) {
        int result = 0;
        for (int i = 0; i < value.length(); i++) {
            result = seed * result + value.charAt(i);
        }
        return (result & 0x7FFFFFFF);
    }
}

📊 三、HyperLogLog:基数估算的魔法

💡 内部原理与特性

HyperLogLog 使用概率算法来估算基数,​​标准误差为 0.81%​​,但内存占用极低。

输入元素
哈希函数
计算前导零
概率估算
基数结果

内存优势​​:

  • 统计1亿个不重复元素 ≈ 12KB内存
  • 传统Set存储1亿元素 ≈ 至少500MB

🚀 常用命令详解

# 添加元素
PFADD daily:uv:20231001 "user1" "user2" "user3"

# 统计基数
PFCOUNT daily:uv:20231001        # 统计当天UV

# 合并多个HyperLogLog
PFMERGE weekly:uv daily:uv:20231001 daily:uv:20231002
PFCOUNT weekly:uv                # 统计周UV

🎯 应用场景案例

​​1. 网站UV统计​​:

public class UVStatisticsService {
    // 记录每日UV
    public void recordUV(String userId) {
        String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String key = "uv:daily:" + today;
        
        redis.pfadd(key, userId);
        // 设置过期时间(2天)
        redis.expire(key, 2 * 24 * 60 * 60);
    }
    
    // 获取当日UV
    public long getTodayUV() {
        String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String key = "uv:daily:" + today;
        
        return redis.pfcount(key);
    }
    
    // 获取多日合并UV
    public long getRangeUV(LocalDate start, LocalDate end) {
        List<String> keys = new ArrayList<>();
        LocalDate current = start;
        
        while (!current.isAfter(end)) {
            keys.add("uv:daily:" + current.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
            current = current.plusDays(1);
        }
        
        String tempKey = "uv:range:temp:" + System.currentTimeMillis();
        redis.pfmerge(tempKey, keys.toArray(new String[0]));
        long count = redis.pfcount(tempKey);
        redis.del(tempKey);
        
        return count;
    }
}

​​2. 实时数据去重统计​​:

public class RealTimeStatistics {
    // 实时统计独立用户数
    public void trackUserAction(String action, String userId) {
        String key = "action:" + action + ":" + 
                    LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
        
        redis.pfadd(key, userId);
        redis.expire(key, 2 * 60 * 60); // 过期时间2小时
    }
    
    // 获取小时级统计
    public long getHourlyActionCount(String action, LocalDateTime time) {
        String key = "action:" + action + ":" + 
                    time.format(DateTimeFormatter.ofPattern("yyyyMMddHH"));
        
        return redis.pfcount(key);
    }
}

🌐 四、GEO:地理位置服务

💡 内部原理与特性

GEO 基于 ZSet 实现,使用 ​​Geohash​​ 算法将二维坐标编码为一维字符串。

经纬度坐标
Geohash编码
ZSet存储
范围查询
距离计算

精度与特性​​:

  • 有效精度:约±0.5米(取决于Geohash精度)
  • 支持半径查询、距离计算
  • 底层使用ZSet,支持所有ZSet命令

🚀 常用命令详解

# 添加地理位置
GEOADD cities:location 116.405285 39.904989 "北京"
GEOADD cities:location 121.472644 31.231706 "上海"

# 获取地理位置
GEOPOS cities:location "北京"

# 计算距离
GEODIST cities:location "北京" "上海" km

# 半径查询
GEORADIUS cities:location 116.405285 39.904989 100 km WITHDIST

# 获取Geohash值
GEOHASH cities:location "北京"

🎯 应用场景案例

​​1. 附近的人功能​​:

public class NearbyService {
    // 更新用户位置
    public void updateUserLocation(Long userId, double longitude, double latitude) {
        String key = "user:location";
        redis.geoadd(key, longitude, latitude, "user:" + userId);
    }
    
    // 查找附近的人
    public List<UserDistance> findNearbyUsers(Long userId, double radius) {
        // 先获取当前用户位置
        List<GeoCoordinate> position = redis.geopos("user:location", "user:" + userId);
        if (position == null || position.isEmpty()) {
            return Collections.emptyList();
        }
        
        GeoCoordinate coord = position.get(0);
        
        // 查询附近用户
        List<GeoRadiusResponse> responses = redis.georadius(
            "user:location", 
            coord.getLongitude(), 
            coord.getLatitude(), 
            radius, 
            GeoUnit.KM,
            GeoRadiusParam.geoRadiusParam().withDist()
        );
        
        // 转换为用户列表
        return responses.stream()
            .map(response -> new UserDistance(
                response.getMemberByString(),
                response.getDistance()
            ))
            .collect(Collectors.toList());
    }
    
    // 计算两个用户距离
    public Double getDistance(Long user1, Long user2) {
        return redis.geodist(
            "user:location", 
            "user:" + user1, 
            "user:" + user2, 
            GeoUnit.KM
        );
    }
}

​​2. 地理位置搜索​​:

public class LocationSearchService {
    // 添加地点
    public void addPlace(Place place) {
        redis.geoadd(
            "places:location",
            place.getLongitude(),
            place.getLatitude(),
            place.getId()
        );
        
        // 同时存储地点详细信息
        redis.hset("place:info:" + place.getId(), toMap(place));
    }
    
    // 半径搜索
    public List<Place> searchNearby(double lng, double lat, double radius) {
        List<GeoRadiusResponse> responses = redis.georadius(
            "places:location",
            lng,
            lat,
            radius,
            GeoUnit.KM,
            GeoRadiusParam.geoRadiusParam().withDist()
        );
        
        // 批量获取地点详情
        List<Place> places = new ArrayList<>();
        for (GeoRadiusResponse response : responses) {
            String placeId = response.getMemberByString();
            Map<String, String> info = redis.hgetAll("place:info:" + placeId);
            Place place = toPlace(info);
            place.setDistance(response.getDistance());
            places.add(place);
        }
        
        return places;
    }
}

💡 五、总结与选型指南

📊 高级数据结构对比

特性 Bitmap HyperLogLog GEO
底层实现 String 特殊结构 ZSet
内存效率 极高 极高
精度 精确 近似(0.81%误差) 精确
适用场景 二值状态统计 基数估算 地理位置
典型应用 签到、布隆过滤器 UV统计、去重计数 附近的人、LBS

🎯 选型决策指南

二值状态
基数估算
地理位置
业务需求
数据类型
Bitmap
HyperLogLog
GEO
用户签到
特征标记
UV统计
去重计数
附近的人
范围搜索

🔧 生产环境建议

  1. ​​Bitmap最佳实践​​:
    • 合理设计偏移量映射规则
    • 定期归档历史数据
    • 注意大Key问题(单个Bitmap不宜过大)
  2. HyperLogLog最佳实践​​: ​​
    • 理解并接受误差范围
    • 不适合需要精确计数的场景
    • 合并多个HLL时误差可能累积
  3. GEO最佳实践​​:
    • 合理设置Geohash精度
    • 结合传统数据库存储详细信息
    • 注意半径查询的性能影响

🚀 性能优化技巧

​​批量操作​​:使用管道(pipeline)提升批量操作性能
​​内存优化​​:定期清理过期数据,控制单个Key大小
​​架构设计​​:根据业务特点选择合适的数据结构
​​监控告警​​:设置内存使用监控和告警阈值


网站公告

今日签到

点亮在社区的每一天
去签到