【Redis合集-03】Redis常用数据结构选型指南|面试&生产场景全覆盖

目录

1.Redis7 数据结构概览

2. String 类型应用

2.1 内部结构:SDS(简单动态字符串)

2.2 常用命令实战

2.3 应用场景补充

缓存对象

分布式锁

计数器与限流

3. Hash 类型应用

3.1 常用命令

3.2 实战:购物车

3.3 底层优化

4. List 类型应用

4.1 常用命令

4.2 实战:消息队列(简易版)

4.3 实战:最新评论列表

5. Set 类型应用

5.1 内部编码

5.2 常用命令

5.3 实战场景补充

共同关注 / 推荐好友

抽奖系统

标签系统

6. Zset(Sorted Set)类型应用

6.1 常用命令

6.2 实战:排行榜

6.3 实战:延迟队列

7. Bitmap 数据类型详解

7.1 常用命令

7.2 实战:用户签到

7.3 实战:布隆过滤器

8. HyperLogLog 数据类型详解

8.1 常用命令

8.2 实战:页面 UV 统计

9. Geo 数据类型详解

9.1 常用命令

9.2 实战:附近的人

10. Stream 数据类型详解

10.1 核心概念

10.2 常用命令

10.3 实战:可靠的消息队列

11. Spring Boot 快速连接 Redis

11.1 引入依赖

11.2 配置文件

11.3 配置 RedisTemplate 序列化

11.4 使用示例

11.5 实战中的注意点

总结(个人心得)

1.Redis7 数据结构概览

Redis 7 在数据结构上延续了之前的丰富性,但底层有许多优化(例如用 listpack 替代 ziplist)。核心数据结构可以分为:

  • 基础类型:string、hash、list、set、zset

  • 扩展类型:bitmap、hyperloglog、geo、stream

  • 底层数据结构:SDS、listpack、quicklist、dict、skiplist、intset 等

每种类型都有自己最合适的应用场景,别什么都用 string,这会浪费内存并丧失 Redis 的能力。

2. String 类型应用

String 是 Redis 中最基础的类型。

2.1 内部结构:SDS(简单动态字符串)

Redis 的 string 不是直接用 C 语言的 char*,而是封装了 SDS(Simple Dynamic String)。好处:

  • O(1) 获取长度(len 字段)

  • 杜绝缓冲区溢出(自动扩容)

  • 减少内存重分配(预分配空间和惰性释放)

  • 二进制安全(可以存图片、序列化对象等)

在 Redis 7 里,string 的底层编码有 embstr 和 raw 两种:

  • 长度 ≤ 44 字节(不同版本阈值不同,7.x 为 44)时使用 embstr,内存连续,只读不修改时效率高。

  • 长度 > 44 字节或发生修改时转为 raw,即 SDS 和 RedisObject 分开存储。

2.2 常用命令实战

# 基本 SET / GET
SET user:1:name "zhangsan"
GET user:1:name

# 批量操作(减少网络 RTT)
MSET user:1:age 25 user:1:city "beijing"
MGET user:1:name user:1:age user:1:city

# 数值操作(计数器)
INCR article:100:views
INCRBY article:100:views 10
DECR article:100:views

# 设置过期时间
SETEX token:abc123 3600 "user_session_data"

# 不存在才设置(分布式锁的基石)
SETNX lock:order:123 unique_value
# Redis 7 推荐使用 SET ... NX EX 原子命令
SET lock:order:123 unique_value NX EX 30

# 获取并设值
GETSET counter "0"    # 旧版本,现推荐 GETEX 或组合命令
GETEX key EX 60        # 获取值同时设置过期时间

2.3 应用场景补充

缓存对象

把对象序列化成 JSON 存成 string。优点:简单。缺点:频繁修改某个字段时需要整体读出来再写回去,浪费流量和 CPU。如果对象修改频繁,后面讲的 hash 更适合。

// Spring Boot 示例
redisTemplate.opsForValue().set("user:1", JSON.toJSONString(user), 30, TimeUnit.MINUTES);
分布式锁

用 string 实现简单的分布式锁。Java 代码展示一个完整版:

String lockKey = "lock:order:123";
String clientId = UUID.randomUUID().toString();
// 加锁,同时设置过期时间防止死锁
Boolean lockSuccess = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lockSuccess)) {
    try {
        // 执行业务
    } finally {
        // 释放锁:lua 脚本保证原子性,判断 value 是否为当前客户端
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(lockKey), clientId);
    }
}
计数器与限流

文章阅读量、API 调用次数等直接用 INCR。滑动窗口限流可以基于 string + 过期时间实现:

String rateKey = "rate:api:" + userId;
Long count = redisTemplate.opsForValue().increment(rateKey);
if (count == 1) {
    redisTemplate.expire(rateKey, 1, TimeUnit.MINUTES);
}
if (count > 100) {
    throw new RuntimeException("请求过于频繁");
}

注意:这种简易限流不是滑动窗口的,更精确的需要用 zset 或 Redis-Cell 模块。

3. Hash 类型应用

Hash 适合存储对象,特别是那些字段经常单独更新的场景。内部编码有 listpack(Redis 7 中取代了 ziplist)和 hashtable

3.1 常用命令

HSET user:1 name "lisi" age 28 city "shanghai"   # 设置哈希表 user:1 中的多个字段:name="lisi", age=28, city="shanghai"。返回新添加的字段数量(此处为3)
HGET user:1 name                                 # 获取哈希表 user:1 中字段 name 的值,返回 "lisi"
HMGET user:1 name age                            # 批量获取字段 name 和 age 的值,返回 ["lisi", "28"]
HINCRBY user:1 age 1                             # 将字段 age 的值增加 1(原子操作),返回增加后的值(29)。要求原值可解析为整数
HGETALL user:1                                   # 获取哈希表 user:1 中所有字段和值,以列表形式返回,如 ["name","lisi","age","29","city","shanghai"]
HEXISTS user:1 name                              # 检查字段 name 是否存在,存在返回 1,不存在返回 0
HDEL user:1 city                                 # 删除字段 city,返回成功删除的字段数量(1)
HLEN user:1                                      # 返回哈希表 user:1 中的字段数量(删除 city 后,还剩 name 和 age,所以返回 2)
HSCAN user:1 0 MATCH *                           # 大 hash 用 scan 避免阻塞:迭代哈希表 user:1 中的字段。游标从 0 开始,MATCH * 匹配所有字段。每次返回一批字段和值,用于安全遍历大哈希

3.2 实战:购物车

# 用户1 的购物车
HSET cart:1 1001 2    # 设置哈希表 cart:1 中商品1001的数量为2(如果字段不存在则创建,存在则覆盖)。返回新添加的字段数量(此处为1)
HSET cart:1 1002 1    # 添加商品1002,数量为1。返回新添加的字段数量(此处为1)
HINCRBY cart:1 1001 1 # 将商品1001的数量增加1(原子操作),返回增加后的数量(3)。可用于购物车加一件商品
HGETALL cart:1        # 获取购物车中所有商品ID和数量,以列表形式返回,如 ["1001","3","1002","1"]

用 hash 存购物车的好处是每个商品数量可以独立操作,不用像 string 那样序列化整个对象。

3.3 底层优化

当字段数量少且每个字段值短时,hash 用 listpack 紧凑存储,节省内存;当超过 hash-max-listpack-entries 或 hash-max-listpack-value 时转为 hashtable。Redis 7 用 listpack 替代了 ziplist,级联更新问题彻底解决。

4. List 类型应用

List 是简单的字符串列表,底层用 quicklist(Redis 7 仍是 quicklist,但内部节点用 listpack 替代了 ziplist)。它可以用作栈、队列、阻塞队列等。

4.1 常用命令

LPUSH queue "task1" "task2"   # 将一个或多个值插入到列表 queue 的头部(左侧)。返回插入后列表的长度。此处插入 task1, task2,顺序为:先插入 task1,再插入 task2,因此 task2 在头部
RPUSH queue "task3"           # 将一个或多个值插入到列表 queue 的尾部(右侧)。返回插入后列表的长度。此处 task3 追加到末尾
LPOP queue                    # 移除并返回列表 queue 头部(最左侧)的元素。如果列表为空,返回 nil。此处返回 "task2"(因为上一步 LPUSH 后头部是 task2)
RPOP queue                    # 移除并返回列表 queue 尾部(最右侧)的元素。如果列表为空,返回 nil。此处返回 "task3"(因为上一步 RPUSH 添加了 task3 到尾部)
LRANGE queue 0 -1             # 返回列表 queue 中指定区间的元素。索引从 0 开始,-1 表示最后一个元素。0 -1 表示返回所有元素。结果以数组形式返回
BLPOP queue 5                 # 阻塞式左弹出(Blocking Left Pop):依次检查多个列表(此处只有一个 queue),如果指定列表非空则立即弹出头部元素;如果所有列表都为空,则阻塞等待最多 5 秒。超时后返回 nil。适用于生产者-消费者模式

BLPOP queue 5 之所以适用于生产者-消费者模式,是因为它解决了消费者端的关键问题:如何高效地等待新任务

  • 传统轮询的缺陷:如果消费者不断用 LPOP 检查队列,队列为空时会产生大量无效请求(CPU 空转 + 网络开销)。

  • BLPOP 的阻塞机制:当队列为空时,消费者会阻塞等待,直到:

    • 生产者通过 LPUSH / RPUSH 向队列中推入新元素 → 消费者立即弹出该元素并继续处理。

    • 或者超过指定的超时时间(此处为 5 秒)→ 返回 nil,让消费者有机会做其他任务或重试。

优势总结

  1. 零无效轮询:不浪费 CPU 和 Redis 资源。

  2. 即时响应:任务到达后立即被唤醒,延迟低。

  3. 避免死等:超时机制防止消费者永久阻塞。

典型场景:多个消费者进程/线程同时 BLPOP 同一个队列,Redis 保证每个任务只被一个消费者取走,实现天然的负载均衡任务分发

4.2 实战:消息队列(简易版)

利用 BLPOP(最左侧 或 BRPOP (最右侧)可以实现简单的生产者-消费者模型:

# 消费者1
BRPOP task_queue 0   # 永久阻塞等待
# 生产者
LPUSH task_queue "send_email"

这种队列没有 ack 机制,消息一旦弹出就没了,可靠性不如后面的 Stream。

4.3 实战:最新评论列表

用 LPUSH 插入新评论,LTRIM 保持固定长度:

LPUSH comment:article:100 "comment1"   # 将评论 "comment1" 插入到文章100的评论列表头部(左侧)。此时列表:["comment1"]
LPUSH comment:article:100 "comment2"   # 再将 "comment2" 插入头部,新评论在前。列表变为:["comment2", "comment1"]
LTRIM comment:article:100 0 9          # 修剪列表,只保留索引0到9的元素(即最多10条)。由于当前只有2条,全部保留。常用于控制评论列表最多显示最新10条(最新的在头部)
LRANGE comment:article:100 0 -1        # 返回列表所有元素(索引0到-1)。结果为 ["comment2", "comment1"],即最新评论在最前面

5. Set 类型应用

Set 是无序的字符串集合,支持交并差集。

5.1 内部编码

Set 有两种底层实现:

  • intset:当集合元素全是整数且数量较少时使用,有序存储,节省内存。

  • dict:当出现非整数元素或元素数量超过 set-max-intset-entries 时,转为哈希表,value 都为 NULL。

Redis 7 还优化了 intset 的转换条件。

5.2 常用命令

SADD users:follow:1 "user2" "user3"           # 将 user2 和 user3 添加到集合 users:follow:1 中(用户1的关注集合)。返回成功添加的元素数量(2)
SREM users:follow:1 "user2"                  # 从集合中移除 user2。返回成功移除的元素数量(1)
SMEMBERS users:follow:1                      # 返回集合中的所有成员,结果为 ["user3"](无序)
SISMEMBER users:follow:1 "user2"             # 检查 user2 是否在集合中,存在返回1,不存在返回0(此处返回0)
SCARD users:follow:1                         # 返回集合中的元素个数,此处为1(只有 user3)

# 集合运算
SADD set1 a b c                               # 向集合 set1 中添加元素 a, b, c,返回添加数量(3)
SADD set2 b c d                               # 向集合 set2 中添加元素 b, c, d,返回添加数量(3)
SINTER set1 set2                              # 返回两个集合的交集:["b", "c"]
SUNION set1 set2                              # 返回两个集合的并集:["a","b","c","d"](顺序可能不同)
SDIFF set1 set2                               # 返回差集(set1 中有但 set2 中没有的元素):["a"]
SINTERSTORE dest set1 set2                    # 计算 set1 和 set2 的交集,并将结果存储到新集合 dest 中(如果 dest 已存在则覆盖)。返回交集元素个数(2)。此命令原子完成,且避免网络传输大量数据

5.3 实战场景补充

共同关注 / 推荐好友
# 用户1和用户2的关注列表
SADD follow:1 userA userB userC
SADD follow:2 userB userC userD

# 共同关注
SINTER follow:1 follow:2   # userB userC

# 推荐给用户1:用户2关注了但用户1没关注的
SDIFF follow:2 follow:1    # userD
抽奖系统
# 将所有参与抽奖的用户放入set
SADD lottery "user1" "user2" "user3" "user4"

# 随机抽取1名用户(不删除)
SRANDMEMBER lottery 1

# 随机抽取1名并移除(保证不重复中奖)
SPOP lottery 1
标签系统

用 Set 给文章打标签,例如:

SADD article:1:tags "redis" "database"
SADD article:2:tags "redis" "cache"
# 返回同时拥有两个标签的文章 → "article1"
SINTER article:1:tags article:2:tags

6. Zset(Sorted Set)类型应用

Zset 是带分数的有序集合,内部使用 skiplist + dict 实现,兼顾范围查找和按成员查分的效率。Redis 7 在小数据量时先用 listpack 紧凑存储。

6.1 常用命令

ZADD rank 100 user1 90 user2 80 user3          # 向有序集合 rank 中添加三个成员,分数分别为 100(user1)、90(user2)、80(user3)。返回成功添加的数量(3)
ZINCRBY rank 5 user2                           # 给 user2 的分数增加 5 分(原子操作),返回增加后的分数(95)。若成员不存在则自动添加并设置分数为 5
ZRANGE rank 0 -1 WITHSCORES                    # 按分数升序返回所有成员及其分数(索引 0 到 -1)。结果为 ["user3","80","user2","95","user1","100"]
ZREVRANGE rank 0 -1 WITHSCORES                 # 按分数降序返回所有成员及其分数。结果为 ["user1","100","user2","95","user3","80"]
ZSCORE rank user1                              # 返回 user1 的分数,结果为 "100"
ZRANK rank user1                               # 返回 user1 在升序排名中的索引(分数最低的排名为 0)。user1 分数最高,排名为 2(因为 user3 索引0,user2索引1,user1索引2)
ZREVRANK rank user1                            # 返回 user1 在降序排名中的索引(分数最高的排名为 0)。user1 分数最高,所以返回 0
ZCOUNT rank 80 100                             # 返回分数在闭区间 [80, 100] 内的成员数量。所有三个成员分数均在区间内,返回 3
ZREMRANGEBYRANK rank 0 2                       # 移除升序排名索引从 0 到 2 的成员(即分数最低的前3个成员)。此处所有成员(索引0、1、2)均被删除,rank 变为空集合

6.2 实战:排行榜

# 游戏积分榜
ZADD game_score 500 playerA 480 playerB 450 playerC
# Top 3
ZREVRANGE game_score 0 2 WITHSCORES
# 查询玩家排名
ZREVRANK game_score playerA

6.3 实战:延迟队列

利用 score 作为时间戳,消费者不断获取分数 ≤ 当前时间的元素并处理。

# 生产者添加延迟任务
ZADD delay_queue 1680000000 "order_timeout_123"
# 消费者定期拉取到期的任务
ZRANGEBYSCORE delay_queue 0 1680000000 LIMIT 0 10
# 处理完删除
ZREM delay_queue "order_timeout_123"

7. Bitmap 数据类型详解

Bitmap 不是一种新的数据类型,本质是 string,但提供位操作命令。适合只有两种状态的布尔型海量数据统计。

7.1 常用命令

SETBIT user:login:20220101 100 1   # 将 key 为 "user:login:20220101" 的位图(bitmap)的第 100 位设置为 1,表示 uid=100 的用户在 2022年1月1日 登录了。返回该位原来的值(之前是 0,则返回 0)
GETBIT user:login:20220101 100    # 获取该位图中第 100 位的值,返回 1(表示已登录)。若用户未登录,返回 0
BITCOUNT user:login:20220101      # 统计该位图中所有值为 1 的位的个数,即当天登录的总用户数(假设每个 uid 对应一个偏移量)。返回整数值
BITOP AND result key1 key2        # 对 key1 和 key2 对应的位图执行按位与(AND)操作,结果存入 result 键。BITOP 还支持 OR、XOR、NOT 操作。返回结果位图的字节长度
BITPOS key 1                      # 返回 key 对应位图中第一个值为 1 的位的偏移量(从 0 开始计数)。若所有位都是 0,则返回 -1。可用于快速查找最早登录的用户等场景

7.2 实战:用户签到

# uid=1000 的用户在2022年第一天签到
SETBIT sign:1000:2022 0 1
# 第365天签到
SETBIT sign:1000:2022 364 1
# 统计该用户2022年签到总天数
BITCOUNT sign:1000:2022

7.3 实战:布隆过滤器

虽然布隆过滤器在 Redis 4.0 之后有了模块(RedisBloom),但我们可以用 bitmap 自己实现一个简单的布隆过滤器,原理是通过多个哈希函数把元素映射到位图的几个位置并置 1,查询时如果所有位置都为 1 则可能存在。

8. HyperLogLog 数据类型详解

HyperLogLog 是一种基数统计算法,占用空间固定(12 KB),误差率 0.81%。适合 UV 统计这种不需要精确值的场景。

8.1 常用命令

PFADD uv:page1 user1 user2 user3      # 将元素 user1, user2, user3 添加到 HyperLogLog 结构 uv:page1 中。HyperLogLog 是一种概率数据结构,用于高效统计基数(唯一元素数量),内存占用固定(约12KB)。返回 1 表示底层估计值发生变化(通常是第一次添加新元素),返回 0 表示添加的元素可能已经存在过(不影响基数估计)
PFCOUNT uv:page1                      # 返回 uv:page1 的近似基数(唯一用户数)。由于使用概率算法,结果存在误差,默认标准误差为 0.81%(配置可调)。此处返回 3(因为有 3 个不同用户)。注意:PFCOUNT 可同时统计多个 HLL,如 PFCOUNT uv:page1 uv:page2
PFMERGE dest uv:page1 uv:page2        # 合并多个 HyperLogLog 结构(uv:page1 和 uv:page2)到一个新的 HLL(dest)中。dest 会被创建或覆盖,合并后的基数估计等于多个集合的并集基数。常用于汇总多个维度的 UV 数据,比如合并不同日期的 HLL 得到月活 UV

8.2 实战:页面 UV 统计

不用为每个用户维护 set,直接用 HLL 节省内存。

# 记录用户访问
PFADD uv:homepage userId1 userId2 ...
# 获取访问量近似值
PFCOUNT uv:homepage

用 Set 存储用户 ID 和 HLL 的内存差异:当 UV 达到亿级时,Set 消耗 GB 级别内存,HLL 仍然 12KB。

9. Geo 数据类型详解

Geo 内部基于 zset 实现,将经纬度编码成一个 score,用来计算地理位置距离、范围查询等。

9.1 常用命令

GEOADD cities 116.40 39.90 "beijing"           # 将北京(经度116.40,纬度39.90)添加到地理集合 cities 中。返回成功添加的元素数量(1)
GEOADD cities 121.47 31.23 "shanghai"          # 添加上海(经度121.47,纬度31.23)。返回成功添加的数量(1)
GEODIST cities beijing shanghai km             # 计算北京到上海的距离,单位 km。返回距离值(约1067.8 km,球面距离)。单位还可选 m(米)、mi(英里)、ft(英尺)
GEORADIUS cities 116 39 500 km WITHCOORD       # 从指定坐标(经度116,纬度39)为中心,半径500公里内查找城市,并返回坐标(WITHCOORD)。可加 WITHHASH、WITHDIST 等选项。结果可能包含北京(距离在500km内),上海超出范围
GEORADIUSBYMEMBER cities beijing 1000 km       # 以城市集合中的北京为中心,半径1000公里内查找其他城市。返回符合的城市名称(例如上海可能在此范围内,实际约1067km > 1000km,所以不返回上海;若半径设为1100km,则返回上海)
GEOHASH cities beijing                         # 返回北京的地理位置 Geohash 字符串(如 "wx4g0s8p3t")。Geohash 是一种将二维经纬度编码为字符串的算法,可用于快速邻近查找

9.2 实战:附近的人

# 存储用户位置
GEOADD user_location 116.38 39.92 user1
# 查找5km范围内的用户
GEORADIUS user_location 116.38 39.92 5 km

可以配合移动端实时更新位置。

10. Stream 数据类型详解

Stream 是 Redis 5.0 引入的持久化消息队列,Redis 7 中功能更完善。它弥补了 List 做队列没有消费者组、没有 ACK 的缺陷。

10.1 核心概念

  • 消息:键值对组成的条目。

  • 消息 ID:时间戳-序号形式,如 1680000000000-0

  • 消费者组:组内消费者分摊消息,支持 ACK 和重试

10.2 常用命令

# 添加消息
XADD mystream * field1 value1 field2 value2   # 向 Stream (消息队列) mystream 中添加一条消息,* 表示由 Redis 自动生成消息 ID(格式:<毫秒时间戳>-<序列号>),后续字段为 field1=value1, field2=value2。返回生成的消息 ID,如 "1680000000000-0"

# 读取消息(从某个ID开始)
XREAD COUNT 2 STREAMS mystream 0              # 从 Stream mystream 中读取最多 2 条消息,从消息 ID 大于 0 的位置开始(0 表示从最早的消息开始)。返回消息列表(包含消息 ID 和字段)。常用用法:XREAD BLOCK 0 STREAMS mystream $ 阻塞等待新消息

# 创建消费者组
XGROUP CREATE mystream mygroup 0              # 在 Stream mystream 上创建消费者组 mygroup,起始偏移量为 0(表示从 Stream 的开头开始消费,也可用 $ 表示只消费新消息)。返回 OK。若组已存在可加 MKSTREAM 选项自动创建 Stream

# 消费者组读取(读取未确认的新消息)
XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >   # 消费者 consumer1 从消费者组 mygroup 中读取 1 条尚未被任何消费者确认的消息。> 表示只读取从未投递给其他消费者的新消息(即待处理队列中的未分发消息)。返回消息及消息 ID,同时该消息会进入消费者的 Pending 状态,直到被 XACK 确认

# 确认消息
XACK mystream mygroup 1680000000000-0         # 确认消费者组 mygroup 已成功处理消息 ID 为 1680000000000-0 的消息。从 Pending 列表中移除该消息。返回成功确认的数量(1 或 0)

# 查看挂起的消息(未ACK)
XPENDING mystream mygroup                     # 查看消费者组 mygroup 中所有已投递但尚未确认的消息(即 Pending 条目)的统计信息。返回总条数、最小和最大 ID,以及各消费者的消费条目概览(消费者名及其挂起消息数)。用于监控消息处理进度

10.3 实战:可靠的消息队列

# 生产者
XADD order_stream * order_id 123 action "create"   # 向 Stream(消息队列)order_stream 中添加一条订单消息,* 表示自动生成消息 ID(格式:<毫秒时间戳>-<序列号>),消息包含两个字段:order_id=123,action="create"。返回生成的消息 ID,如 "1680000000000-0"

# 消费者组处理
XGROUP CREATE order_stream order_group 0           # 在 Stream order_stream 上创建消费者组 order_group,起始消费偏移量为 0(表示从 Stream 的最早消息开始读取)。如果 Stream 不存在,可加 MKSTREAM 选项自动创建。返回 OK

XREADGROUP GROUP order_group order_consumer1 COUNT 1 BLOCK 5000 STREAMS order_stream >   # 消费者 order_consumer1 从消费者组 order_group 中读取消息。COUNT 1 表示最多读取 1 条消息;BLOCK 5000 表示如果没有新消息,则阻塞等待最多 5000 毫秒(5 秒);> 表示只读取从未被任何消费者读取过的**新消息**(即消费组中尚未分发的消息)。返回消息内容(包含消息 ID 和字段),同时该消息会进入该消费者的 Pending 列表(待确认状态)

# 处理成功后 ACK
XACK order_stream order_group messageId            # 消费者确认已成功处理指定消息(messageId 需替换为实际的消息 ID,如 "1680000000000-0")。确认后,消息会从消费者组的 Pending 列表中移除,避免重复消费。返回成功确认的消息数量(1 或 0)

与 List 做队列的区别:

  • List 弹出后消息就没了,消费者崩溃会丢消息。

  • Stream 有 ACK 机制和待处理消息列表,保证消息被正确处理。

11. Spring Boot 快速连接 Redis

整合 Spring Boot 和 Redis 7,我将整理出核心配置和常见坑点。

11.1 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 连接池(推荐) -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

11.2 配置文件

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: yourpassword
    database: 0
    lettuce:            # 默认 lettuce 客户端
      pool:
        max-active: 10
        max-idle: 5
        min-idle: 2

11.3 配置 RedisTemplate 序列化

如果不配置,默认使用 Jdk 序列化,可读性差且占用大。建议改为 JSON 序列化:

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // Jackson 序列化
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, 
            ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSerializer.setObjectMapper(om);
        
        // String 序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

11.4 使用示例

@RestController
public class RedisController {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @GetMapping("/set")
    public String setValue() {
        redisTemplate.opsForValue().set("test_key", "hello redis7");
        return "ok";
    }
    
    @GetMapping("/zset")
    public void zsetTest() {
        redisTemplate.opsForZSet().add("rank", "user1", 100);
        Set<Object> top = redisTemplate.opsForZSet().reverseRange("rank", 0, 2);
        System.out.println(top);
    }
}

11.5 实战中的注意点

  • 大 Key 问题:用 HSCANSSCANZSCAN 分批获取,别用 HGETALLSMEMBERS 直接暴拉。

  • 热 Key 问题:高频访问的 key 加随机后缀分散到多个 key,或使用本地缓存缓解。

  • 连接池配置:根据并发量合理设置,避免连接耗尽。

总结(个人心得)

通过学习Redis 7 数据结构,最深的感受是:选对类型比优化代码更重要。下面是我自己的决策小抄:

  • 需要简单缓存 → String

  • 对象字段频繁独立更新 → Hash

  • 队列 / 栈 → List

  • 去重、标签、交并差 → Set

  • 排行榜、延迟队列 → Zset

  • 签到、大量布尔统计 → Bitmap

  • UV 统计 → HyperLogLog

  • 附近的人、距离计算 → Geo

  • 高可靠消息队列 → Stream(消息有专属中间件 如MQ等,感觉用处不大)

在实际项目中,我习惯结合 Spring Boot + RedisTemplate,配合 lua 脚本保证原子性,基本上能满足分布式系统中绝大部分场景的需求。

最后,如有改进之处欢迎指正,觉得有帮助的话不妨点赞收藏支持一下,感谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值