news 2026/9/11 12:13:59

Redis高级数据结构实战:GEO、BitMap与HyperLogLog在项目中的应用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis高级数据结构实战:GEO、BitMap与HyperLogLog在项目中的应用

做黑马点评项目的时候,我最大的感触不是缓存、分布式锁这些老生常谈的东西,而是 Redis 里面那三个平时不太用、但一到具体业务场景就非常出彩的数据结构:GEO、BitMap、HyperLogLog。这个项目里正好有三块需求把它们三个全部串起来了——附近门店搜索、用户签到统计、店铺访问 UV 统计。如果你只学过 String 和 Hash,这几个需求实现起来会非常别扭,但换到对应结构后,代码量能少一个量级,性能还能提升不少。这篇笔记我就把这三个结构的实战用法、命令细节、Spring Data Redis 的调用方式、以及我踩过的坑全部整理出来,给正在做黑马点评或者其他类似项目的同学一个可以直接抄作业的参考。

先说一个总体感受:这三个结构不是替代关系,而是各自解决一类特定问题。GEO 解决“根据经纬度找附近”,BitMap 解决“大量布尔型状态的低成本存储”,HyperLogLog 解决“海量数据去重计数”。搞懂它们各自适合什么场景,比背命令重要得多。下面我会按照项目里的实际需求展开讲。

1. 整体设计思路:为什么这三个场景必须用对应的结构

1.1 黑马点评里的三类需求拆解

黑马点评项目(或者说类似的生活服务类项目)里,有三个特别有代表性的功能点:

第一个是“附近门店”。用户在首页能看到离自己最近的商家,或者按距离展示“距离 xx 米”。如果只用 MySQL,那要先把全城的门店经纬度查出来,再逐一计算距离并排序。数据量小还能忍,门店数据一多,这个计算就是灾难,而且没法用普通索引高效筛选。更麻烦的是,如果还要支持按类型、按评分再做过滤,SQL 会非常复杂。

第二个是“签到统计”。用户每天可以在 App 里签到,还要能看到当月签到了几天、连续签到了几天。如果建一张签到表,每个用户每天一条记录,一年就是 365 条,1000 万用户就是 36 亿条。上面还要做连续签到天数的计算,得按用户和日期做窗口函数,SQL 写起来又臭又长,索引也扛不住。

第三个是“店铺访问 UV 统计”。商家需要知道自己的店铺页面一天有多少独立访客,注意是 UV 不是 PV。PV 可以直接用计数器,但 UV 必须去重。如果用集合(Set)把每个访问用户的 ID 都存进去,一个百万访问量的页面,内存会被撑爆;如果用 MySQL 的 distinct,量大了也扛不住。

这三个需求单独看都不难,但放在一个项目里,你自然就会想到 Redis。因为 Redis 本身就是为这些“高并发、高写入、实时性要求高”的场景设计的。而更巧的是,Redis 为这三类问题分别提供了专门的数据结构:GEO、BitMap、HyperLogLog。

1.2 为什么非要用这三种结构,而不是用数据库

我在最开始设计的时候,也犹豫过到底要不要直接走 MySQL。后来对比了一下方案,才发现差距不是一点半点。

拿“附近门店”来说,MySQL 的实现一般有两种:

  • 一种是经纬度直接存两个字段,查询时先粗筛一个矩形范围,再精确计算距离。但矩形范围要动态计算经纬度上下限,语句复杂,索引利用率也不高。
  • 另一种是使用 PostGIS 这类空间扩展,但中小型项目引入额外组件成本较高,而且查询性能仍然受限于数据量和索引设计。

而 Redis GEO 直接提供了“经纬度写入 + 距离计算 + 半径搜索 + 距离排序”的能力。底层实现是跳表加哈希,查询效率是 O(log n) 级别,而且天然支持按距离从近到远返回,一条命令就能完成“附近的人”这类需求。

签到功能,如果用 BitMap,一个用户一年只需要 365 个 bit,也就是 46 个字节左右。100 万用户一年也就 46MB 左右,这个内存占用可以忽略不计。更重要的是,连续签到天数的判断,可以转换成位运算,比如把某个用户 7 天的签到位拼成一个 7 位二进制数,判断结果是否等于 127 就能知道是不是全勤。这种在高并发下非常关键,因为位运算在 CPU 层面是非常快的。

UV 统计用 HyperLogLog,标准误差在 0.81% 以内,但占用的内存是固定的,每个 key 最多 12KB。也就是说,无论你统计 1000 个访客还是 1 亿个访客,它占的内存都是 12KB。这个特性是其他数据结构完全不具备的。

所以我最后的结论是:这三个需求,使用 Redis 的这三个结构,是性能和代码复杂度的最优解。

2. GEO 实战:附近门店功能从命令到代码的完整落地

2.1 GEOSEARCH:附近门店查询的黄金命令

GEO 在 Redis 3.2 版本就有了,但真正好用起来是 6.2 版本,因为 6.2 引入了GEOSEARCH命令,替代了老的GEORADIUSGEOSEARCH支持按“中心点 + 半径”或者“中心点 + 边界矩形”搜索附近的位置,同时还能按距离排序,甚至返回每个点与中心点的距离。

基础命令如下:

GEOADD shop:geo 116.397128 39.916527 "shop_1" GEOADD shop:geo 116.394827 39.913148 "shop_2" GEOADD shop:geo 116.403119 39.919578 "shop_3"

写入坐标用GEOADD,注意 Redis 的经纬度顺序是“经度在前,纬度在后”,这个顺序非常容易搞反。之前我就因为经纬度顺序写反,导致搜出来的结果永远在非洲,排查了半天才反应过来。

附近搜索的命令:

GEOSEARCH shop:geo FROMLONLAT 116.403873 39.915107 BYRADIUS 3 km ASC WITHDIST

这个命令的意思是:以经度 116.403873、纬度 39.915107 为中心点,搜索半径 3 公里内的所有商家,按距离从近到远排序,并且返回距离字段。ASC表示距离升序,如果想降序就用DESC。还可以用BYBOX指定矩形范围,但对“附近感”更强的场景,建议用BYRADIUS

如果你用的是老版本 Redis,只能用GEORADIUS,命令参数顺序差不多,但GEOSEARCH更加语义化,而且可以直接在 Spring Data Redis 中映射。所以开发前先确认一下你 Redis 的版本,推荐至少 6.2。

2.2 Java 代码实现:Spring Data Redis 调用 GEO

黑马点评项目里,我用的是Spring Data Redis,封装程度不错,但也踩了一些坑。首先是注入RedisTemplate,GenericJackson2JsonRedisSerializer 一般会把 GEO 相关对象序列化搞得很别扭,所以建议单独用一个只操作字符串的 RedisTemplate,或者直接用StringRedisTemplate,因为 GEO 内部存储本质也是字符串相关的结构。

核心实现代码大致是这样的:

public void addShopLocation(Long shopId, Double lng, Double lat) { String key = RedisConstants.SHOP_GEO_KEY; redisTemplate.opsForGeo().add(key, new Point(lng, lat), shopId.toString()); } public List<ShopDistanceVO> searchNearbyShops(Long currentShopId, Double lng, Double lat, Integer radius) { String key = RedisConstants.SHOP_GEO_KEY; Circle circle = new Circle(new Point(lng, lat), new Distance(radius, RedisDistanceMetrics.KILOMETERS)); RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs .newGeoRadiusArgs() .includeDistance() .sortAscending() .limit(20); GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().search(key, circle, args); // 遍历 results,封装返回 return results.getContent().stream().map(r -> { String shopId = r.getContent().getName(); double distance = r.getDistance().getValue(); return new ShopDistanceVO(Long.valueOf(shopId), distance); }).collect(Collectors.toList()); }

这里有几个重点:

  • opsForGeo().add()的参数是Point对象,构造时也是先经度后纬度。
  • search()方法可以传Circle或者RedisGeoCommands.GeoRadiusCommandArgs,一个指定搜索中心与半径,一个指定返回格式。
  • includeDistance()可以在结果里拿到距离值,不调用的话只有店铺 ID,还需要再算一次,很麻烦。
  • limit(20)限制返回数量,避免一次取出过多数据。

黑马点评里的场景是“首页附近门店”,我会在项目启动的时候把商家的经纬度一次性预热到 Redis 里,之后查询全部走GEOSEARCH,实测几百家门店数据下,查询耗时在 1ms 以下,性能非常稳。

2.3 GEO 的实现原理与坐标编码技巧

GEO 底层为什么快?它其实不是自己发明了一个数据结构,而是把经纬度编码成一个一维的 52 位整数,然后放进 ZSET 里。这个编码方案叫 geohash,具体过程是把经纬度范围不断二分,交叉合并成二进制位,最终得到一个整数。ZSET 的成员就是门店 ID,score 就是这个编码后的整数。

所以你会发现,GEO 天然支持 ZSET 的一些操作,比如用ZREM删除某个位置,用ZRANGE查看所有位置。实际项目中如果要移除某个门店,可以直接:

ZREM shop:geo "shop_1"

这个编码特性还带来一个好处:搜索附近门店时,Redis 不会全量遍历,而是通过 ZSET 的范围查询快速定位到可能包含目标点的附近区域,再对候选成员做精确距离计算。所以复杂度才能维持在 log(n) 级别。

明白了这个原理,你就知道为什么一个 GEO key 里不要放太多不同类型的数据,因为本质上它还是一个 ZSET。如果门店数量到了百万级、千万级,单个 key 还是会有性能压力。这种情况下建议按城市拆 key,比如shop:geo:{cityId},查询时先根据用户所在城市定位到对应的 key。

2.4 GEO 实操中的几个坑

经纬度顺序坑。前面提过了,Redis 坐标顺序是“经度 纬度”,也就是“lng lat”。但你从高德地图、百度地图拿到的坐标,经常也是“lng lat”,问题不大。但如果你使用的第三方 SDK 返回的是“lat lng”,那就一定要做转换。这个坑不是只在写入时要注意,构造Point对象时也要小心,很多封装库构造器就是Point(longitude, latitude)

坐标精度与坐标系问题。Redis GEO 默认使用的是 WGS-84 坐标系,而高德地图、百度地图用的是 GCJ-02 或 BD-09,它们之间会有几十到几百米的偏移。如果直接拿高德地图的坐标存进 Redis,再和其他来源的坐标混合计算,距离会不准。最稳妥的办法是统一一个坐标系来源,要么全部用高德的坐标,要么在写入前做坐标转换。项目里我直接用高德坐标统一存入 Redis,因为所有门店坐标和用户定位都来自高德,就不存在混用的偏差了。

距离单位Distance对象支持KILOMETERSMETERSMILES单位,但GEOSEARCH命令里的半径默认是米还是千米?很多人会记混。Spring Data Redis 里如果你不指定单位,默认是米。例如半径写5表示 5 米,不是 5 公里。我建议在代码里显式传入RedisDistanceMetrics.KILOMETERS,这样后续看代码不会产生歧义。

空结果时返回的坑。如果搜索范围内没有门店,GeoResults为空,但个别封装版本会返回空的 content 对象而不是 null,直接遍历可能 NPE。建议先用results == null || results.getContent().isEmpty()判断一下。

3. BitMap 实战:签到功能与连续签到统计

3.1 为什么签到功能要散装成“位”而不是“行”

传统的签到表设计,每个用户每一天一行数据,表结构大概是这样:user_id, sign_date, is_sign。功能上没问题,但有两个隐患:数据量增长太快;连续签到判断复杂。

签到本质是“用户在某一天是否签到”,是一个布尔值。用 BitMap 表达时,bit 位下标就是天数偏移,值 0 或 1 就代表未签到或已签到。如果按用户建立 key,每个用户的每个月(或每年)就是一条字符串,长度只有几十字节。

在黑马点评里,我采用的方案是“按月存一个 BitMap key”,key 的格式类似sign:user:{userId}:{yyyyMM}。为什么要按月而不是按年?因为按年存,一个 key 365 个 bit,读取当月数据时还要截取,逻辑稍复杂;按月存,一个月最多 31 天,一个 key 最多 31 个 bit,脚本和位运算都非常直观,而且 key 的粒度更细,避免了某个大用户长期占用一个超长字符串的情况。

3.2 签到的写入与查询:SETBIT / GETBIT / BITFIELD

签到动作本身就是把一个 bit 设为 1:

SETBIT sign:user:1001:202401 0 1

这里的0表示该月第一天。如果今天是 1 月 15 日,则需要把 offset 设置为15 - 1 = 14

SETBIT sign:user:1001:202401 14 1

判断某一天是否签到:

GETBIT sign:user:1001:202401 14

统计当月签到总天数:

BITCOUNT sign:user:1001:202401

这三个命令是最基本的用法,但黑马点评里要求“统计连续签到天数”,这个就没办法直接用现成命令了,需要自己计算。Redis 5.0 以后提供的BITFIELD命令非常合适,它可以一次性读取一段连续的 bit,比如从第 0 位开始读 15 位,返回一个十进制整数。

BITFIELD sign:user:1001:202401 GET u15 0

假设返回值是 31599,它的二进制是111101101101111。怎么知道今天之前已经连续签到几天?从最低位(代表今天)往高位看,连续 1 的个数就是连续签到天数。我们可以用代码循环,也可以进一步用位运算优化:

public long countContinueSigns(byte[] bits, Integer todayOffset) { if (bits == null || bits.length == 0) return 0; int offset = todayOffset; int count = 0; for (int i = offset; i >= 0; i--) { if ((bits[i / 8] & (1 << (i % 8))) != 0) { count++; } else { break; } } return count; }

当然也可以直接把BITFIELD GET u31 0读取整个月的 bit 位,然后从右往左数连续的 1。用 StringRedisTemplate 实现时,返回值通常是 List 里面的 Long,再逐位解析。

3.3 用 BitMap 做签到统计的内存账本

很多人会问,BitMap 真的省内存吗?我们来算一笔账。

假设有 1000 万用户,每人每月最多 31 个 bit。如果采用“按月 + 按用户”存 key,那么平均每个用户每月的 key 长度为 4 字节左右(Redis 内部还会有一些 overhead),这里先按 1 个 bit 每月计算。

  • 1000 万用户 × 31 个 bit ≈ 3.1 亿 bit ≈ 38.75 MB。
  • 即使把所有用户的签到都占满,也才 38.75 MB(bit 层)。
  • 签到时写入一条记录:1 次 SETBIT + 1 次 EXPIRE,写放大和传统表相比极小。

MySQL 方案呢?1000 万用户,每人每天一条记录,一个月就是 3 亿行。就算每行只存 user_id + date + flag,按 30 字节算,一个月也是 9GB 级别。索引再翻倍,运维成本和备份成本都很高。

这就是 BitMap 的杀手级优势。你不需要一台高配 MySQL,就能支撑百万日活用户的签到业务。

3.4 BitMap 签到功能的进阶玩法与避坑

连续签到统计可以提前在数据库还是 Redis 计算?我的建议是全部在 Redis 完成,因为 Redis 的操作是原子性的,而且位操作非常快。即便要查最近 7 天的连续签到,也只需要一个BITFIELD命令读取连续 7 位,不需要轮询 7 次。

不要忽略跨月问题。如果今天是 3 月 1 日,往前推连续签到,昨天是 2 月的最后一天,这时候需要读两个 key。我的处理方法是先读当月的 BITFIELD,然后再判断是否达到当月天数上限,如果达到了,再去读上个月的 key,拼接起来继续算。代码逻辑不复杂,但一定要写好边界。

给 key 设置过期时间。签到数据虽然小,但不设置过期时间的话,一年 12 个 key,长期占用也是一种浪费。建议按业务情况设置 1 年或 2 年过期,避免冷用户数据一直占内存。比如:

EXPIRE sign:user:1001:202401 365

关于 value 能否用 String 类型直接操作。有些同学会用redisTemplate.opsForValue().setBit(),也是可以的。但要注意 Redis 的SETBIT命令是对字符串类型做位操作,所以返回结果类型是 Long(该位之前的值)。如果你的 RedisTemplate 是 JSON 序列化,这个 Long 类型反序列化没问题,但 key 千万不要被序列化器加前缀,建议使用StringRedisTemplate,省心很多。

4. HyperLogLog 实战:店铺访问 UV 统计

4.1 从 Set 到 HyperLogLog:我如何测量内存差异

UV 统计最直白的做法是用 Set 存储访问过的用户 ID,然后SCARD获取数量。这个方案在小流量下毫无问题,但数据量一大就现出原形。假设一个热门店铺当天有 10 万 UV,每个用户 ID 是 16 个字符的字符串,那这个 Set 至少要占用:

  • 10 万 × 16 字节 ≈ 1.6 MB,这还不包含 Redis 内部维护哈希表和指针的开销。如果有一万个人气店铺,内存占用就无法接受了。

HyperLogLog 的出现,就是为了解决这种“去重计数没必要精确到个位”的场景。它的核心思路是:不记每一个数据本身,只记录这个数据经过哈希后,二进制位上最早出现 1 的位置。用多个分桶的估计值来推算整个集合的基数。它不会精确等于真实值,但误差能控制在 0.81% 以内。对 UV 统计来说,这个精度已经足够。

4.2 HyperLogLog 的常用命令与 Java 接入

HyperLogLog 的命令只有三个:

PFADD page:uv:shop:1001 "user_123" PFCOUNT page:uv:shop:1001 PFMERGE page:uv:shop:1001:week page:uv:shop:1001:20240101 page:uv:shop:1001:20240102
  • PFADD添加元素,可以一次添加多个。
  • PFCOUNT统计当前 key 的基数,也就是去重后的数量。
  • PFMERGE合并多个 key,可以用来统计“多个时间窗口的合计 UV”,比如说某一周的 UV 等于 7 天分别存储的 HyperLogLog key 合并后计数。

Java 里的使用方式也一样直接:

public Long addUv(Long shopId, Long userId) { String key = RedisConstants.SHOP_UV_KEY + shopId; return redisTemplate.opsForHyperLogLog().add(key, userId.toString()); } public Long getUv(Long shopId) { String key = RedisConstants.SHOP_UV_KEY + shopId; return redisTemplate.opsForHyperLogLog().size(key); }

opsForHyperLogLog().add()对应 PFADD,size()对应 PFCOUNT。如果你使用StringRedisTemplate,可以避免序列化问题。

4.3 时间窗口 UV 统计:按天分 key 还是按周分 key

黑马点评里要展示“今日访客”“累计访客”,如果只是简单一个 key,那“今日”和“累计”就混在一起了。我的方式是:

  • uv:shop:{shopId}:{yyyyMMdd}存当天 UV。
  • uv:shop:{shopId}:total存累计 UV,每次访问时同时 PFADD 到当天的 key 和 total key。
  • 如果要看 7 天 UV,用PFMERGE把最近 7 天的 key 合并,然后 PFCOUNT。注意 PFMERGE 结果可以直接放入一个新 key,再 PFCOUNT,这样不污染原始 key。

这里有一个容易被忽略的问题:PFADD写入的 key 如果不设置过期时间,每天都会产生新 key,时间久了也会积累不少 key 实例。建议对按日 key 设置 30 天过期,对累计 key 设置较长过期或不设置。

4.4 HyperLogLog 的边界和坑

不能反查用户 ID。HyperLogLog 不是一个存储容器,它不会记录到底添加了哪些元素,所以不要说“统计出有哪些访客”,这做不到。如果你需要明细,需要再加一个 Set 或者 List,两个结构配合使用。

误差在数据量小时会比较“扎眼”。比如实际只有 3 个用户,PFCOUNT 可能返回 2 或者 4。这属于正常现象,基数越小时误差占比越大。所以在展示时,如果数量小于 100,建议直接精确计数或单独维护一个计数器。

PFMERGE 不要合并太多 key。虽然理论上没有上限,但一次合并几百个 key,Redis 客户端要发送大量参数,网络与命令解析成本都会上升。建议分批合并,或者预计算周聚合 key。比如每天凌晨跑一次任务,把过去 7 天的日 key 合并进一个周 key,展示时直接 PFCOUNT 周 key。

PFCOUNT 的精度和内存是绑定的。HyperLogLog 存储精度取决于每个桶的位数,Redis 使用了 16384 个桶,所以固定占用 12KB。这个固定内存很优秀,但不要以为它像 String 一样可以无限扩容,如果往同一个 key 里塞太多数据,可能达到精度上限,不过实际业务几乎不会触发。

和 BitMap 配合的场景。有一种情况很常见:UV 统计要求按“活跃用户”维度去重,而这个用户同时也在做签到。这时候可以先用 BitMap 记录用户签到状态,再用 HyperLogLog 统计访问 UV,两者互不干扰,但都是基于用户 ID 做 key 设计,注意格式统一即可。

5. 三个结构的横向对比与项目落地总结

5.1 结构选型速查表

我在做完项目后,整理了一张表格,方便以后快速选型:

需求Redis 结构核心命令空间成本精度是否支持明细
附近门店 / 附近的人GEOGEOADD, GEOSEARCH, GEODIST每个点约 52 bit 的 score,加成员存储距离计算准确支持查询成员及坐标
签到 / 打卡 / 在线状态BitMapSETBIT, GETBIT, BITCOUNT, BITFIELD每个状态 1 bit,内存极小精确支持按位查询
UV / PV 去重计数HyperLogLogPFADD, PFCOUNT, PFMERGE固定约 12KB / key0.81% 标准误差不支持反查明细

选型时我一般分三步:第一,这个需求是要“查明细”还是要“算数量”?要明细基本排除 HyperLogLog;第二,数据是否天然可以映射成布尔值或位图?如果是,优先 BitMap;第三,是否依赖位置关系?依赖则用 GEO。

5.2 项目中的组合拳实战经验

黑马点评这个项目里,这三个结构不是孤立的。举个例子,首页加载附近门店时,我先用GEOSEARCH得到门店列表和距离;点击进入门店后,用HyperLogLog记录一次访问 UV;而用户当天是否已经访问过、是不是新用户,则用BitMap做一个快速标记。三者链条非常流畅,整体响应时间也没有因为统计逻辑变慢。

有一个细节值得注意:这三个结构虽然都是 Redis 的数据类型,但它们的 key 设计习惯不同。GEO 因为底层是 ZSET,key 尽量按业务域区分,不要和签到 key 复用;BitMap 的 key 建议带上时间和用户维度,否则有一天你会发现某个 key 被撑得特别大;HyperLogLog 的 key 建议按时间粒度拆分,这样方便做日周月聚合。

5.3 常见问题速查

问题 1:使用 GEOSEARCH 时,为什么搜不到距离很近的点?

先确认经纬度顺序写反没有,再确认坐标系是否统一。如果你从 Redis 写入和从客户端查询使用的是不同的地图坐标系,可能出现偏移,导致明明在附近却搜不到。还有一种可能是半径单位搞错了,比如把千米写成了米。

问题 2:BITFIELD 读取出来的十进制数怎么转换成天数?

把十进制数转成二进制,然后从低位往高位数连续 1 的个数。比如1111表示连续签到 4 天(今天的位是 1,昨天也是 1,前天也是 1,大前天也是 1)。注意查询的 bit 长度要覆盖到今天,否则会漏算。

问题 3:HyperLogLog 统计出来的 UV 比实际值多很多,正常吗?

如果数据量很小,误差可能显得很大,这是正常现象。但如果你添加的元素数量级很大,误差一般会稳定在 0.81% 附近。如果真的偏离太多,检查一下是不是把同一个 key 既做了 PFADD 又做了 PFMERGE,或者多个线程并发导致 key 被覆盖了。

问题 4:这三个结构能持久化吗?

都能。默认 Redis 开启 RDB 或 AOF 都能保存它们。不过 HyperLogLog 在持久化和加载时可能会稍微慢一点,但问题不大。如果业务允许,可以在极端并发下将 UV 统计的写操作做成异步,用消息队列削峰,也能减少 Redis 压力。

问题 5:key 拆分后,跨 key 查询怎么做?

GEO 跨城市查询,可以在应用层并行查询多个城市 key,再在内存中做距离排序和合并。BitMap 跨月查询,需要读取两个 key 并拼接位序列。HyperLogLog 跨天查询,建议用 PFMERGE 合并 key 后统计。总结下来就是:拆 key 的代价是“查询逻辑变复杂”,但收益是“单 key 更小、扩展性更好”。分与不分的取舍取决于数据量级。

5.4 最后的实操心得

我做完黑马点评里这三个模块,最大的感受是:Redis 的命令不难,难的是知道什么时候用哪个。GEO、BitMap、HyperLogLog 这三个东西单独看都很简单,但组合起来能解决很多实际问题。

如果你现在正在写类似的实战项目,我的建议是不要急着写代码,先把业务场景里每个字段的数据类型想清楚。比如签到,你先问自己“是不是一次一个 bit 就够了”;UV 统计,先问“是不是能接受千分之一的误差”;附近门店,先确认“我是不是真的需要距离计算逻辑”。想清楚了,代码只是顺势而为的事。

最后再分享一个小技巧:这三个结构在 Redis 里其实可以互相转换思路。比如 BitMap 的位数据可以用BITFIELD一次读出来,配合 Lua 脚本可以做到原子性的连续签到判断;GEO 的底层是 ZSET,你甚至可以临时用 ZSET 范围操作做附近门店的排位;HyperLogLog 虽然不能存明细,但结合 BitMap 可以做“本周访问过且签到的用户有多少”。这些组合玩法,等你自己上手之后一定会觉得很有意思。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 12:13:37

安卓10系统定制:彻底隐藏设备信息菜单的ROM修改指南

1. 项目背景与需求解析 最近在折腾一台老旧的安卓10.0设备时&#xff0c;遇到了一个很有意思的需求&#xff1a;客户需要隐藏系统设置中的"我的设备"菜单选项。这个需求在商用设备定制、企业终端管理等领域其实很常见——比如共享设备厂商不希望用户查看硬件配置&…

作者头像 李华
网站建设 2026/9/11 12:12:45

生物智能与AI融合:技术边界模糊的挑战与机遇

1. 项目概述&#xff1a;当技术边界模糊的临界点2003年波士顿动力公司成立时&#xff0c;大多数人还认为双足机器人行走是天方夜谭。如今当Atlas机器人完成后空翻的瞬间&#xff0c;我们突然意识到&#xff1a;那个机器人只能呆在工厂流水线上的时代正在终结。这个标题揭示的正…

作者头像 李华
网站建设 2026/9/11 12:11:11

项目管理深度解析(三十六)——项目怎么估算活动资源

摘要&#xff1a;本文系统讲解项目管理中如何估算活动资源&#xff0c;涵盖估算的输入信息、常用工具与技术、输出成果及实践建议。文章先厘清活动资源估算的概念与作用&#xff0c;再梳理项目管理计划、项目文件、事业环境因素等输入&#xff0c;重点对比专家判断、自下而上估…

作者头像 李华
网站建设 2026/9/11 12:10:50

microduck:嵌入式硬件验证的最小可行闭环方法论

1. 什么是microduck&#xff1f;它不是玩具&#xff0c;而是一套可落地的嵌入式产品验证方法论“microduck”这个词最近在硬件开发圈、嵌入式初学者社区和产品经理技术转型群里高频出现&#xff0c;但它既不是某家公司的注册商标&#xff0c;也不是某个开源项目的官方代号——它…

作者头像 李华