1. Redis String 类型:最容易被低估的基础类型
先说说为什么把 String 单独拎出来写一篇实战。很多开发者对 Redis 的第一印象就是get/set,觉得 String 太简单,不就是放个字符串嘛。但实际上,String 是 Redis 里使用频率最高、承载业务场景最广的类型——缓存、计数器、分布式锁、分布式 ID、位图统计,全部建立在 String 之上。我见过不少团队,项目里 Redis 用了几年,String 停留在set key value的水平,位图 bitmap 更是完全没碰过。这篇文章就把 String 的底层结构、核心命令的实战姿势、以及 bitmap 这个藏在 String 里的“特殊视图”讲透。
如果你已经在用 Redis 做缓存,但对 SDS、embstr、raw 这些底层概念模模糊糊,或者听说过 bitmap 做签到、统计在线用户,却不知道怎么落地,这篇比较适合你。我会从底层原理讲到实际命令,再讲到典型场景和排坑经验,全程用我在项目里踩过的真实案例说话。
2. String 的底层设计:为什么 Redis 非要自己搞一套字符串
2.1 C 字符串的痛点与 SDS 的解决方案
Redis 是用 C 语言写的,但它在 String 的实现上没有直接用 C 语言原生的char*字符串,而是自己封装了一个叫 SDS(Simple Dynamic String,简单动态字符串)的结构。原因很现实:C 字符串在 Redis 这种高性能场景下根本不够用。
传统的 C 字符串有三个硬伤。第一,获取长度需要遍历整个字符串,时间复杂度是 O(n),Redis 作为内存数据库,高频读取操作如果每次都 O(n) 扫描长度,性能损耗完全不可接受。第二,C 字符串以\0作为结束标志,这意味着字符串中间不能包含空字符,存二进制数据(比如图片、序列化对象)时很容易被截断,Redis 是二进制安全的,这种限制不能有。第三,修改字符串内容涉及内存重新分配,比如拼接一个字符串要先重新申请内存、拷贝、释放旧内存,频繁操作既慢又容易产生内存碎片。
SDS 的解决办法很直接。它的结构大致长这样:
struct sdshdr { int len; // 已使用的长度 int alloc; // 分配的总容量 char buf[]; // 字节数组,真正存储数据的地方 };len字段直接记录了字符串长度,所以获取长度是 O(1) 操作。数据存储靠len来界定边界,不再依赖\0,二进制安全的问题也解决了。另外 SDS 在分配内存时会额外预留一些空间(alloc - len),当字符串需要拼接时,如果剩余空间够用就直接追加,不用重新分配内存,这就是所谓的“预分配”策略。这套设计在 Redis 源码里维护了几十年,是支撑 Redis 高性能的基石之一。
2.2 int、embstr、raw 三种编码切换的隐藏细节
很多人不知道 String 类型内部还有多种编码方式。Redis 会根据字符串的内容和长度,自动选择 int、embstr、raw 三种编码中的一种来存储,这个细节在面试里经常被问到,在实战中也会影响你的内存占用。
- int 编码:字符串能被解析成 64 位有符号整数时使用。比如执行
set num 100,这个 key 的 value 会用 8 字节的 long 类型存储,而不是字符串数组,省内存又高效。 - embstr 编码:字符串长度小于等于 44 字节时使用。这种编码把 RedisObject 头部和 SDS 结构分配在一块连续内存里,一次内存分配搞定,读取时缓存友好。
- raw 编码:字符串长度超过 44 字节时使用。RedisObject 和 SDS 分开分配,需要两次内存分配。
这个 44 字节的阈值是 Redis 3.2 之后调整的。原因是内存分配器通常按 16/32/64 字节的粒度分配,RedisObject 头部占 16 字节,SDS 头部占若干字节,64 减去这些头部开销后,剩下 44 字节可以存放字符串内容。在 Redis 3.2 之前这个阈值是 39 字节,升级版本时如果发现同样长度的短字符串内存占用有变化,不要惊讶。
有个容易踩的坑:编码方式是会变化的。比如set key "100"时是 int 编码,但执行append key "abc"之后就变成了 raw 编码。同理,set key "a"时是 embstr 编码,但执行append key "b",Redis 会先将 embstr 转成 raw 再执行追加操作。这种转换是不可逆的,意味着短字符串的追加操作相比直接set会多一些转换开销。所以如果你要频繁拼接一个短字符串,与其用append多次操作,不如在客户端拼好之后一次set进去,性能和内存都更好。
3. String 的五大高频实战场景与命令组合
3.1 缓存读写:SET NX EX 的正确姿势
String 最常见的用途就是做缓存。基础的set key value加上expire命令应该每个人都会,但在生产环境里,我强烈建议直接用一条命令完成设置值和过期时间:
SET user:1001 '{"name":"zhangsan","age":18}' EX 300 NX这条命令同时做了三件事:设置 key 的值、设置 300 秒过期时间、只有 key 不存在时才生效。NX参数在缓存场景里非常有用,它可以天然实现“分布式锁”的加锁语义,后面细说。
缓存穿透、缓存击穿、缓存雪崩这三个问题,我相信大家都听说过。这里给出我在项目里实际使用的应对方案:
- 缓存穿透(查询一个根本不存在的数据,Redis 和数据库都没有):除了在代码里对空结果也做短暂缓存外,更可靠的方式是在缓存前加一层布隆过滤器。布隆过滤器的底层也可以基于 Redis bitmap 实现,后面会展开。
- 缓存击穿(某个热点 key 在过期瞬间,大量请求同时打到数据库):用
SET key value NX EX实现互斥锁,只放一个请求去查数据库回源,其他请求短暂等待。 - 缓存雪崩(大量 key 同一时间过期):在设置过期时间时加一个随机偏移量,比如
EX 300 + random(0, 60),别让所有 key 在同一时刻失效。
回源更新缓存的伪代码大致是这样:
# 简化版:缓存击穿互斥锁 value = redis.get(key) if value is None: if redis.set("lock:" + key, "1", ex=5, nx=True): try: value = db.query(key) redis.set(key, value, ex=300) finally: redis.delete("lock:" + key) else: time.sleep(0.1) return redis.get(key) # 重试获取注意锁的粒度,lock:前缀的 key 要和业务 key 一一对应,防止某一个热点 key 失效时拖慢其他 key 的回源。
3.2 计数器与限流:INCR 的原子性红利
String 的计数器功能经常被忽略,但它可能是除了缓存之外性价比最高的能力。INCR命令在 Redis 内部是原子操作,不存在并发环境下先读后写的数据竞争问题。
典型场景是文章阅读量、商品销量、点赞数。直接用:
INCR article:readcount:10001 INCRBY article:readcount:10001 10INCRBY支持一次加指定的步长,DECR和DECRBY就是反向操作。这些操作全部是原子的,在高并发下不会出现计数丢失。如果是浮点计数,用INCRBYFLOAT。
计数器还有一个经典场景是接口限流。最简单的固定窗口限流可以这样实现:
# 每个用户每分钟最多请求 10 次 SET rate:limit:user_123 0 EX 60 NX INCR rate:limit:user_123 # 如果返回值 > 10,拒绝请求第一次执行SET ... NX EX初始化计数器并设置过期时间,之后每次请求执行INCR。这个方案有一个临界问题(窗口切换的瞬间可能通过两倍请求),但对于大多数内部系统的接口保护已经完全够用了。更严格的滑动窗口限流可以用 Redis 的 ZSet 实现,那是另一个话题。
3.3 分布式锁:SETNX 的升级玩法
在单机多线程环境下,Java 里的synchronized、Go 里的mutex都能解决并发问题,但到了分布式环境下,多个进程同时操作共享资源,就需要一个大家都认的第三方来协调,Redis 就是最常见的那个协调者。
Redis 分布式锁的官方推荐实现是:
SET lock:order:10001 "uuid-xxxx" EX 10 NX这里的NX保证同一时刻只有一个客户端能设置成功,EX 10给锁设置自动过期时间,防止客户端宕机之后锁无法释放形成死锁。value 必须是一个唯一标识(比如 UUID),这样解锁的时候才能确认是自己的锁。
解锁操作要用 Lua 脚本保证原子性:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end为什么不能先get判断再del?因为两步操作不是原子的,如果判断完还没删,锁过期了,另一个线程拿到了锁,这时候你的del就会把别人的锁删掉。用 Lua 把判断和删除合并在一个脚本里执行,Redis 单线程保证脚本执行期间不会有其他命令插入。
我在实践中还发现一个细节:锁的过期时间不能设置得太短,否则业务还没执行完锁就自动释放了,其他线程趁虚而入;也不能太偏长,否则客户端宕机后要等很久锁才能被回收。一个常见的做法是把过期时间设置为业务预期耗时的 3-5 倍,同时允许业务方在锁快到期时执行续期脚本。
3.4 全局 ID 生成器与对象存储
在分库分表的场景下,数据库自增主键不好使了,用 Redis 的INCR可以生成全局递增 ID:
INCR global:id:order返回结果可以直接作为订单号的后缀。如果 ID 需要带日期前缀,可以用INCR global:id:order:20241001,每天一个 key,顺便还能统计当天的单量。
对象存储是 String 最朴素的一个场景,把 Java/Python 对象序列化成 JSON 字符串存储。这里要特别注意的是 key 的命名规范。我见过很多团队 key 命名极其随意,什么a:1、data:test都有。我自己的习惯是遵循“业务域:对象类型:ID[:子属性]”的格式,比如user:profile:1001、order:detail:20240901001。这套规范在后期的排查和维护中价值巨大,尤其是配合 Redis 的SCAN命令做数据清理时。
对于大对象,默认超过 512MB 才报错,但实际工程里一个 value 超过 1MB 就该警惕了。超大 value 不仅占用内存,还会拖慢网络传输和序列化反序列化的时间。如果遇到大对象,优先考虑拆分 key,或者换用 Hash 类型。
4. bitmap 实战入门:String 的“位视图”到底是怎么玩的
4.1 bitmap 的基本原理和命令全景
bitmap 不是一个独立的数据类型,它本质上是 String 类型的一种位操作视图。String 的底层是字节数组,bitmap 把这个数组再切细一层,让人可以直接操作每一个二进制位(bit)。一个字节有 8 个 bit,String 最大 512MB,所以一个 bitmap 最多可以表示 512MB × 8 = 约 42.9 亿个 bit。这意味着你可以用 512MB 内存表达 40 多亿个布尔状态。
因为 bitmap 是 String 的视图,所以 markerString有的操作它基本都有,同时它又额外提供了一套位操作命令。常用的命令如下:
| 命令 | 作用 | 示例 |
|---|---|---|
SETBIT key offset value | 将 key 对应的字符串的偏移量 offset 处的 bit 设置为 0 或 1 | SETBIT sign:2024:1001 5 1 |
GETBIT key offset | 获取偏移量 offset 处的 bit 值 | GETBIT sign:2024:1001 5 |
BITCOUNT key [start end] | 统计字符串中被设置为 1 的 bit 数量 | BITCOUNT visitor:20241001 |
BITPOS key bit [start end] | 查找第一个为 0 或 1 的 bit 位置 | BITPOS sign:2024:1001 0 |
BITOP operation destkey key [key...] | 对多个 bitmap 执行与、或、异或、非操作,结果保存到 destkey | BITOP AND dest:7day user:login:1001 user:login:1002 |
BITFIELD key [GET type offset ...] | 对 bitmap 做原子性的位域操作,支持批量 GET/SET/INCRBY | BITFIELD sign:2024:1001 GET u31 0 |
这里有一个非常容易踩的坑:位序问题。Redis 的 bit 序列遵循 MSB(高位优先)规则,也就是说,一个字节里的 8 个 bit 从高位到低位排列,offset 0 对应的是第一个字节的最高位(第 7 位),offset 7 对应第一个字节的最低位(第 0 位)。
来看个具体例子:
SETBIT bitmap:demo 0 1 GET bitmap:demo执行完这两条命令后,GET返回的结果会是"\x80",也就是二进制10000000,十进制 128。因为 offset 0 对应的是第一个字节的最高位。如果你执行:
SETBIT bitmap:demo 7 1 GET bitmap:demo这时返回的就是"\x81",二进制10000001。这个位序容易和大部分编程语言里“bit 0 是最低位”的习惯搞混,写代码时一定要在注释里标明。
4.2 bitmap 的偏移量计算与内存模型
使用 bitmap 之前,想清楚 offset 的语义是最重要的事情。通常把某个业务实体的 ID(用户 ID、设备 ID)当作 offset,把 bit 的值当作状态(0 表示未发生,1 表示已发生)。
offset 有两个硬限制:不能为负数,不能超过 2^32-1(4294967295)。超过上限会直接报错,提示bit offset is not an integer or out of range。这意味着一张 bitmap 最多只能覆盖 42.9 亿个不同的偏移量,对于目前绝大多数互联网产品来说够用,但如果你的用户 ID 是雪花算法生成的 64 位大整数,直接拿来做 offset 就会立刻超界,必须先做一层映射(比如自增的用户序号)。
还要理解 bitmap 的内存是按“最大偏移量”分配的,而不是按“实际有数据的偏移量”分配。执行SETBIT key 4294967295 1虽然只设置了一个 bit,但 Redis 会为了这个偏移量分配完整的 512MB 内存。这一点非常重要,后面会专门讲这个坑。
4.3 BITFIELD 的进阶能力
BITFIELD是 Redis 3.2 引入的命令,它能让你对 bitmap 里的多个 bit 做原子性批量操作。一个常见的需求是:统计某个用户一年 365 天的签到情况,需要判断最近连续签到了多少天。
用BITFIELD可以一次取出一整段位数据(比如 31 个 bit 组成的一个整数)进行分析:
BITFIELD sign:2024:1001 GET u31 0这个命令表示:从 offset 0 开始取 31 个 bit,解析成一个无符号 31 位整数。因为一个整数最多只能覆盖 31 位(要留一位给符号位,这里用 31 是为了避开 64 位无符号整数的边界问题),所以如果要分析整年的签到,可以分多段取。拿到这个整数之后,在客户端计算二进制里有多少个连续的 1 就可以了。
这是我实际用过的方法,比一条条GETBIT查询高效得多,尤其适合批量判断“是否连续打卡 N 天”这类需求。
5. 三个让我印象深刻的 bitmap 实战场景
5.1 用户签到系统:几个月时间省下几百万条数据库记录
签到系统是 bitmap 最经典的落地场景。我参与过的一个社区产品,需要记录每个用户每天的签到状态,运营还要频繁查看“本月签到人数”“连续签到 7 天的人数”这类统计。
如果用关系型数据库,需要建一张签到表,每个用户每天签到就插入一条记录。假设有 1000 万用户,一个月签到 30 天,就是 3 亿条记录,光存储和索引就是不小的开销。
用 Redis bitmap 的方案是:每个用户一年用一个 key,offset 对应当年的第几天。
# 用户 1001 在 2024 年的第 100 天签到了 SETBIT sign:2024:1001 100 1 # 查询用户 1001 今天是否签到 GETBIT sign:2024:1001 100 # 统计用户 1001 2024 年签到的总天数 BITCOUNT sign:2024:1001内存怎么估算?一个用户一年 365 天,占 365 个 bit,约 46 字节。1000 万用户就是 1000 万 × 46 字节 ≈ 460MB。如果只保留当年的数据,这个内存开销完全在可接受范围。换成数据库方案,1000 万用户 × 365 条记录就是 36.5 亿行,两种方案的成本差距不在一个量级。
这里要提醒一句:签到场景下,每个用户单独一个 key 的位图比较稀疏(一年 365 个 bit 只用几十个),但整体数据量可控,而且BITCOUNT统计起来特别方便。优化时还可以按月建 key,比如sign:202410:1001表示用户 1001 在 2024 年 10 月的签到情况,这样单 key 更小,过期清理更灵活。
5.2 在线用户统计与 DAU 计算:BITOP 的组合拳
统计在线人数是很多系统的刚需。用 bitmap 的思路是:建立一张“在线状态表”,key 表示某个时间点,offset 表示用户 ID,bit 为 1 表示在线。
# 用户 1001 在 2024-10-01 登录过 SETBIT online:20241001 1001 1 # 用户 1002 也在当天登录过 SETBIT online:20241001 1002 1 # 当天总登录用户数(UV) BITCOUNT online:20241001这里有个细节:如果用用户 ID 直接做 offset,那 key 的内存大小取决于用户 ID 最大值。如果一个产品的注册用户 ID 已经到 1 亿,但日活只有 10 万,用 bitmap 存储每日在线状态时,每个 key 都要按 1 亿个 bit(约 12MB)的内存算,30 天就是 360MB。这个场景下,如果活跃用户密度低,bitmap 不一定比 Set 类型省内存,需要具体算账。
但 bitmap 真正强大的地方是跨天统计。比如要看“最近 7 天都活跃的用户数”,一条BITOP命令就搞定:
BITOP AND dest:7day online:20241001 online:20241002 online:20241003 online:20241004 online:20241005 online:20241006 online:20241007 BITCOUNT dest:7dayBITOP AND的结果保存到 destkey,destkey 中每一位为 1 就表示这个用户在这 7 天每天都登录过。同理,BITOP OR可以统计 7 天内活跃过的总用户数。这种多条命令组合的统计方式,放在传统 SQL 里要写多表 JOIN 或子查询,Redis 一行搞定,性能快到毫秒级。
性能上要注意的是BITOP的时间复杂度是 O(N),N 是 bitmap 的长度。如果每个 key 都是几百万 bit,对 7 个 key 做 AND 操作耗时几十毫秒是正常的。如果统计频率很高,可以把中间结果缓存起来,或者只在离线任务里跑。
5.3 布隆过滤器:几十 MB 挡住上亿无效请求
布隆过滤器(Bloom Filter)是一种概率型数据结构,用来判断“某个元素一定不在集合里”“某个元素可能在集合里”。它的典型场景是防止缓存穿透:当客户端请求一个不存在的商品 ID 时,如果每次都打到数据库,数据库压力很大,这时候布隆过滤器可以先过滤掉大部分不存在的 ID。
Redis 4.0 之后官方提供了布隆过滤器模块,但如果不想引入额外模块,手写一个基于 bitmap 的简易版也够用。原理是:用一个大的 bitmap(假设 1 亿 bit),准备 K 个哈希函数,插入元素时把 K 个哈希值对应的 bit 全部置为 1;查询时看 K 个 bit 是否全为 1,有一个为 0 就说明元素一定不存在。
全部为 1 也不能说一定存在,因为可能出现哈希碰撞导致误判,所以叫“可能在集合里”。但布隆过滤器的核心价值在于:它能以极低的内存成本,绝对正确地判断“不存在”的情况,从而拦截大量无效请求。
我曾经用 bitmap 给某系统的商品查询做防护,1 亿个 bit 大约 12MB 内存,存储了 500 万条已存在的商品 ID,误判率控制在 1% 以内。接口被刷时,绝大多数无效请求直接返回“商品不存在”,数据库查询量下降了 95% 以上。
6. 实战中常见的坑与排查技巧
6.1 大偏移量导致的内存暴涨
这是 bitmap 最容易踩、后果也最严重的问题。前面提到过,Redis 的 String 类型内存是按最大偏移量分配的,而不是按实际使用的 bit 数。
SETBIT user:online:20241001 4294967295 1执行这一条命令,Redis 会立刻为这个 key 分配 512MB 内存。如果代码里用了一个没有经过映射的用户 ID(比如某个删除标记、负数转换出的超大数)作为 offset,一个误操作就能把 Redis 内存撑爆,进而引发主从同步延迟、内存碎片、清理淘汰等问题。
我在项目里经历过一次真实故障:某个统计模块的开发者把数据库自增 ID 直接当作 bitmap 偏移,删除了一段时间的数据后 ID 间隙越来越大,某天一个 ID 跳到了 8000 万,Redis 直接为这个 key 分配了约 10MB 内存,本来一天只有几千次写入,结果内存每天晚上涨 10MB,半个月后 Redis 内存报警。排查时用DEBUG STRING key或MEMORY USAGE key才定位到问题。
排查命令:
MEMORY USAGE user:online:20241001返回值会精确显示这个 key 占用的字节数。如果发现数据量不大但内存占用异常大,大概率是偏移量出现了问题。
6.2 BITCOUNT 的字节范围陷阱
BITCOUNT命令支持可选的 start 和 end 参数,但这里有个特别容易出错的地方:start 和 end 是字节偏移,不是 bit 偏移。
# 统计前 10 个字节(80 个 bit)中有多少个 1 BITCOUNT sign:2024:1001 0 10如果你以为0 30是统计第 0 到第 30 个 bit,那就大错特错了。它统计的是第 0 字节到第 30 字节,共 248 个 bit。这意味着用BITCOUNT的 start/end 参数做“精确到 bit”的统计是不可行的,它只能按字节粒度裁剪统计范围。
那怎么精确统计某个用户前 31 天的签到天数?有两个办法。
办法一是用BITFIELD取出整段 bit 后,在客户端做二进制统计:
BITFIELD sign:2024:1001 GET u31 0办法二是在 Redis 7.0 之后,BITCOUNT支持了BYTE|BIT参数:
BITCOUNT sign:2024:1001 0 30 BIT显式指定单位为BIT,统计第 0 到第 30 bit 范围内的 1 的数量。如果你的 Redis 版本支持这个参数,用起来会方便很多。
6.3 编码转换与类型错误的边界情况
如果对一个已经存在的 key 执行SETBIT,但这个 key 不是 String 类型,Redis 会返回类型错误:WRONGTYPE Operation against a key holding the wrong kind of value。所以确保 bitmap 的 key 不要和其他逻辑共用名,比如不要用SETBIT counter 0 1,如果有人用INCR counter,操作就会报错。
另外,当一个 key 的 value 本身是 int 编码时,执行SETBIT会把它转成 raw 编码,这本身是正常操作,但如果这个 key 同时被其他业务当作计数器使用,就会造成逻辑混乱。key 的职责要单一,一个 key 要么是计数器,要么是位图,不能混着用。
6.4 稀疏位图场景下 bitmap 不一定省内存
很多人一听 bitmap 就条件反射地认为省内存,这个认知需要纠正。bitmap 的内存消耗取决于最大偏移量,而不是实际有多少个位被置为 1。如果一个表里有 1 亿个用户,但只有 100 个用户有数据,每个 key 也要占约 12MB。
判断是否用 bitmap 的简单标准是:看实际活跃位数占最大偏移量的比例。如果比例高于 1/8,bitmap 比用 Set 类型(每个元素约 64 字节)省空间;如果比例很低,Set 类型或 Hash 类型反而更合适。我见过一个业务场景,每月活跃用户只有 3 万,但用户 ID 已经到 3000 万的量级, bitmap 每个 key 占 3.6MB,而用 Set 存储 3 万个 ID 只要 1-2MB。这时候 bitmap 不仅没省内存,反而多用了近一倍。
6.5 稀疏大 key 的过期策略
bitmap 的 key 如果设置了过期时间,要考虑一个场景:一个 12MB 的 bitmap 在某个时间点同时过期,Redis 删除大 key 时会有短暂的阻塞风险,尤其是 Redis 4.0 之前使用同步删除。4.0 之后虽然默认开启了 lazy free 异步删除,但老版本没有这个能力。
超大 bitmap 不建议设置统一的过期时间,更稳妥的做法是:数据过期通过业务逻辑判断(比如 key 名称里带上日期,定期删除旧日期的 key),或者把大 bitmap 拆分成多个小 key。对于确实需要设置过期时间的场景,可以用UNLINK命令主动异步删除,避免阻塞。
7. 最后再说几个实用技巧
这篇文章写到这里,String 和 bitmap 的核心内容基本讲完了,但还有几个小的实战经验想一并分享出来。
第一个是关于 append 命令的性能。如果你要频繁修改一个字符串,比如给日志 key 追加内容,尽量在客户端攒一批再写,减少 Redis 交互次数。Redis 本身很快,但网络 IO 才是瓶颈。
第二个是位运算的场景选择。BITOP适合做离线分析和批量统计,比如每天跑一次把 7 天 DAU 合并到一张表里,而不是实时响应时现场计算。实时查询尽量用GETBIT和BITCOUNT这种单个 key 的操作,避免跨 key 的复杂运算阻塞主线程。
第三个是关于 key 命名的建议。bitmap 的 key 一定要包含日期或业务周期,比如online:20241001、sign:2024:1001。一方面方便按时间清理过期数据,另一方面排查问题的时候,看 key 就能立刻知道数据对应的是哪个业务、哪个时间窗口。我见过最头疼的命名就是bit:1、bit:2这种,线上出了问题根本没法定位。
第四个是 Redis 版本的影响。如果你用的 Redis 版本比较老(3.x 甚至 2.x),建议尽早规划升级。Redis 7.0 之后BITCOUNT支持按 bit 统计、SET命令支持 KEEPTTL 参数、UNLINK异步删除,这些功能在实际项目里的实用价值很高。老版本的 bitmap 也能用,但很多细节会让你写代码时束手束脚。
最后说说我的体会:String 类型是 Redis 所有数据类型的基础,理解 SDS 和编码机制,你才能真正理解 Redis 为什么快、为什么省内存;bitmap 是藏在 String 里的一把利器,签到、在线统计、布隆过滤器、二进制状态标记,这些场景用对方法,性能和成本的优势是立竿见影的。但“用对方法”这四个字很关键,偏移量设计、内存估算、位序方向,每一个细节都决定着你是在享受 Redis 的高性能,还是在给 Redis 埋雷。希望这篇实战笔记能帮你少走一些我走过的弯路。