实践篇 12 | 有一亿个 keys 要统计,应该用哪种集合

本文探讨了Redis在web和移动应用业务场景中的应用,利用其集合类型如Set、SortedSet、Bitmap和HyperLogLog进行数据统计。针对新增用户数、留存用户、商品评论排序、签到打卡和网页UV统计等场景,提出了聚合统计、排序统计、二值状态统计和基数统计四种模式。通过Bitmap和HyperLogLog等数据结构,实现了高效且节省内存的统计方法。例如,使用Bitmap记录签到情况并统计连续签到用户,或使用HyperLogLog估算网页独立访客量。这些策略有助于在大数据量下进行实时统计,降低系统资源消耗。

在 web 和 移动应用的业务场景中,我们经常要保存这样的信息:单key对应的数据集合。

  • 手机APP的用户日登录信息:一天对应一系列的用户ID或移动设备ID
  • 电商网站上商品用户评论列表:一个商品对应一系列的评论
  • 用户在手机APP上的签到打卡信息:一天对应一系列的用户签到记录
  • 应用网站上的网页访问信息:一个网页对应一系列用户的访问点击。

我们知道,redis集合类型的特点是一个键对应一系列的数据,所以非常适合用来存取这些数据。

但是在这些场景中,除了记录信息,我们往往还需要对集合中数据进行统计。

  • 移动应用中,统计每天的新增用户数(DAU)和第二天的用户留存数;
  • 电商网站的商品评论中,需统计评论列表的最新评论;
  • 签到打开中,需要统计一个月连续打卡的用户数
  • 网页访问记录中,需要统计独立访客(UV unique visitor)量

通常情况下,我们面临的用户数量以及访问量都是巨大的,百千万,甚至亿级别。因此必须要选择高效统计大量数据的集合类型。

下面一起了解些常用的集合统计模式,及其对应的高效结构(省时省空间)。

聚合统计

指统计多个集合元素的聚合结果,如统计多集合的交集,差集,并集。

新增用户数和第二天用户留存。

累积用户set(Su):user:id -> set (登录过app的用户id)

每日用户set(Sd):user:id:date -> set (date当天登录过app的用户id)

统计新增用户:Sn = Sd - Su

SDIFFSTORE user:new user:id:20200804 user:id

Su的每日迭代:Su = Su ∪ Sd

SUNIONSTORE user:id user:id user:id:20200803

统计留存用户:Ss = Sd2 - Sd1

SDIFFSTORE user:id:rem user:id:20200804 user:id:20200803

聚合统计合适的结构为set。风险点在于集合运算操作复杂度高,大数据量将导致实例阻塞。可通过指定从库专门负责局和计算,或由客户端进行的聚合统计的方式规避主库和其他从库实例阻塞的风险。

排序统计

指对集合元素排序。

如电商网站商品最新评论。

要求集合保持元素有序,即有序集合。redis的常用4个集合类型(List,Hash,Set,Sorted Set)中,List和Sorted Set就属于有序集合。

List是按照元素进入List的顺序进行排序的,而Sorted Set可根据元素权重排序。

二者区别在当List分页读取(LRANGE)和插入(LPUSH)同时进行的时候,分页会读到前一页的数据(由于数据变更)。Sorted Set 则可通过 ZRANGEBSCORE 获取不变的按序排列数据。

LRANGE product1 3 5
1) "C"
2) "D"
3) "E"

ZRANGEBYSCORE comments N-9 N

二值状态统计

二值状态指集合元素的取值只有0和1两种。

如签到打卡,就只有签到或未签到。

使用BitMap可以记录单用户的各天的签到情况。BitMap底层为String类型,String类型会保存为二进制的字节数组。提供GetBit/SetBit操作,使用offset对bit数组的某一bit位进行读写,偏移量从0开始。BitCount用于统计1的个数。

// 用户id为3000的在8月份的第三天(0开始的idx)的打卡
SETBIT uid:sign:3000:202008 2 1

// 检查是否签到
GETBIT uid:sign:3000:202008 2

// 统计用户在8月份的签到次数
BITCOUNT uid:sign:3000:202008

这样,就可知道用户8月的签到情况。如果记录了1亿用户10天的签到,如何统计10天连续签到用户总数。

BITMAP支持多个map间按位做 “与”,“或”,“异或” 的操作,结果保留到新的BITMAP。

 因此按每天日期为key,value是1亿的bitmap。10个bitmap做与后BITCOUNT即可。内存开销 1亿bitmap = 10^8 * 8 * 2^10 * 2^10 = 12MB,10天就是120MB,不算太大。实际应用设置bitmap过期时间,节省。

基数统计

指统计一个集合中不重复元素的个数

统计网页UV。一个用户一天多次访问只能算一次。Redis的集合类型中Set支持去重。Hash也可以。

// A访问了page1
SADD page1:uv A
HSET page1:uv A 1

// page1的UV
SCARD page1:uv
HLEN page1:uv

当页面或者访问量多,内存消耗过大。可使用HyperLogLog。其是一种用于统计基数的数据集合类型,最大优势在于,集合元素数量非常多时,计算基数所需空间固定且小。

每个HyperLogLog只需花费12KB的内存,就可以计算接近 2^64个元素的基数。

// 5个用户访问了page1
PFADD page1:uv user1 user2 user3 user4 user5

// 返回统计结果(UV)
PFCOUNT page1:uv

HyperLogLog的统计规则是基于概率完成的,所以给出的统计结果有一定误差,标准误算率是 0.81%。意指若得到 UV = 100万 (± 1万),误差不大,若追求精确使用线性空间复杂度的Set和Hash类型。

小结

结合新增用户和留存用户、最新评论列表、用户签到数、网页独立访客量、学习4中集合统计模式:聚合、排序、二值状态、基数统计。

数据类型聚合统计排序统计二值状态统计基数统计
Set -,∪,∩精确,效率低,吃内存
SortedSet∪,∩支持精确,效率低,吃内存
Hash精确,效率低,吃内存
List支持
BitMap∧, V, ⊕支持,效率高,省内存精确,开销大于HyperLogLog
HyperLogLog

概率统计,非常省内存

思考

是否遇到过其他的统计模式,如何使用redis解决?记录积累

<think>好的,用户现在问的是在生产环境中有什么好的方法可以替代RedisKEYS命令。首先,我需要回顾一下RedisKEYS命令的问题。KEYS命令在数据量大的时候会导致性能问题,因为它是遍历所有键,时间复杂度是O(n),这会阻塞其他操作,尤其是在生产环境中,可能会引起服务延迟甚至崩溃。 接下来,我需要考虑用户为什么会问这个问题。他们可能在生产环境中遇到了性能问题,或者听说KEYS命令不安全,想寻找替代方案。用户可能是开发人员或者运维人员,需要优化Redis的使用,确保高可用性和性能。 然后,我要列举替代方法。常见的替代方案包括使用SCAN命令,它分批次迭代键,避免阻塞。此外,可能还有维护一个索引集合,记录所有相关的键,这样查询时直接访问这个集合。另外,使用外部工具如Redis的模块或监控工具,或者通过业务逻辑设计避免全量扫描的需求。 需要详细解释每个替代方法的原理、使用场景和注意事项。比如SCAN命令的游标机制,以及如何编写脚本来实现渐进式扫描。对于索引集合,需要说明如何维护,比如在每次添加键时同步更新一个集合,这样查询时用SSCAN来分页获取。 还要考虑用户可能没有提到的需求,比如如何监控键的数量,如何自动化处理过期键,或者如何结合Lua脚本来保证原子性。此外,可能需要提到版本兼容性,比如SCAN在Redis 2.8以上才支持,确保用户的环境符合要求。 另外,用户可能希望了解每种方法的优缺点,比如SCAN虽然安全但可能需要多次请求才能获取所有结果,而索引集合增加了维护成本。需要权衡这些因素,给出最佳实践建议,比如优先使用SCAN,结合索引和合理的数据结构设计。 最后,总结这些方法,强调避免使用KEYS的重要性,并根据不同的场景选择合适的替代方案,确保生产环境的稳定性和性能。</think>在生产环境中,Redis 的 `KEYS` 命令(如 `KEYS *` 或 `KEYS user:*`)存在严重性能问题,因为它会**遍历所有键**,时间复杂度为 $O(n)$($n$ 为键总数)。对于大容量 Redis 实例,此命令可能导致服务阻塞甚至崩溃。以下是安全替代方案和优化建议: --- ### **1. 使用 `SCAN` 命令替代** **原理**: `SCAN` 通过游标(cursor)分批次迭代键,每次返回少量键,避免阻塞 Redis 主线程。 **时间复杂度**:$O(1)$ 每次调用,整体为 $O(n)$ 但非阻塞。 **命令格式**: ```redis SCAN cursor [MATCH pattern] [COUNT count] ``` **示例**:查找所有以 `user:` 开头的键 ```redis SCAN 0 MATCH user:* COUNT 1000 # 返回下一个游标和匹配的键列表 1) "12345" # 新游标 2) 1) "user:1001" # 匹配的键 2) "user:1002" ``` **注意事项**: - 需循环调用 `SCAN` 直到游标返回 `0`(表示迭代完成)。 - `COUNT` 参数仅作为参考值,实际返回数量可能不一致。 - 迭代过程中若有键被修改或删除,可能重复或遗漏(最终一致性)。 --- ### **2. 维护专用索引集合** **原理**: 主动记录符合条件的键到 Redis 集合(Set)中,查询时直接读取索引集合,避免全量扫描。 **实现步骤**: 1. **写入时同步索引**: 每次创建键时,将键名添加到索引集合中。 ```redis # 添加键 user:1001 SET user:1001 "{...}" SADD user:indexes user:1001 ``` 2. **查询时通过索引获取**: ```redis # 获取所有 user:* 键 SMEMBERS user:indexes # 分页获取(使用 SSCAN) SSCAN user:indexes 0 MATCH * COUNT 100 ``` **优点**: - 查询复杂度为 $O(1)$(`SMEMBERS`)或 $O(n)$ 但可控(`SSCAN`)。 - 可结合过期时间(TTL)自动清理索引。 **缺点**: - 需业务代码维护索引一致性(如删除键时同步移除索引)。 --- ### **3. 利用 Hash 结构分组存储** **适用场景**: 若需按固定模式(如 `user:{id}`)管理键,可将数据存储在 Hash 中,直接通过 `HGETALL` 或 `HSCAN` 查询。 **示例**: ```redis # 存储用户数据 HSET users:index 1001 "{...}" 1002 "{...}" # 查询所有用户 HGETALL users:index # 或分页查询 HSCAN users:index 0 COUNT 100 ``` **优点**: - 天然分组,无需维护额外索引。 - 支持高效分页和条件查询。 --- ### **4. 使用 Redis Modules** **推荐工具**: - **RedisSearch**:支持全文搜索和二级索引,可直接替代 `KEYS` 的模糊查询需求。 - **RedisJSON**:对 JSON 数据建立索引查询。 **示例(RedisSearch)**: ```redis FT.CREATE user_idx ON HASH PREFIX 1 user: SCHEMA name TEXT FT.SEARCH user_idx "John" ``` --- ### **5. 监控与运维优化** 1. **禁用 `KEYS` 命令**: 在 `redis.conf` 中通过 `rename-command KEYS ""` 禁止生产环境使用。 2. **监控慢查询**: 使用 `SLOWLOG` 命令分析潜在性能问题: ```redis SLOWLOG GET 10 # 查看最近10条慢查询 ``` 3. **键数量统计**: 使用 `INFO keyspace` 或 `DBSIZE` 估算总键数(无需遍历)。 --- ### **总结** | **场景** | **推荐方案** | |-------------------------|----------------------------------| | 模糊查询键 | `SCAN` + `MATCH` | | 固定前缀/分组的键管理 | 预维护索引集合或 Hash 结构 | | 复杂查询(如全文搜索) | RedisSearch/RedisJSON 等模块 | | 键数量统计 | `DBSIZE` 或 `INFO keyspace` | **核心原则**: - 避免全量遍历,优先使用游标分批次查询。 - 通过数据设计(如索引、结构化存储)减少查询复杂度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值