Skip to content

Redis 9 种数据类型与性能对比

你用 Redis 存 String,但别人用 Redis 存 List、Set、Hash...

同样都是 Redis,为什么性能差距那么大?

因为数据类型选择错误,会让 Redis 从「极速」变成「蜗牛」

今天,我们来彻底搞清楚 Redis 的 9 种数据类型,以及它们的性能差异。


Redis 数据类型一览

类型底层实现特点典型场景
StringSDS(简单动态字符串)最简单缓存、计数器、分布式锁
Hash压缩列表 / 字典field-value 对对象存储、购物车
List压缩列表 / 双向链表有序、可重复消息队列、最新动态
Set整数集合 / 字典无序、不重复标签、好友关系
ZSet压缩列表 / 跳表 + 字典有序、不重复排行榜、延迟队列
Bitmap位图按位操作签到、用户在线状态
HyperLogLog基数统计概率算法UV 统计
Geospatial有序集合地理位置附近的人、LBS
StreamRadix Tree + 消息队列消息流消息队列、事件流

String:最常用的类型

底层实现

Redis 使用 SDS(Simple Dynamic String) 存储字符串:

SDS 结构:
┌────────────┬───────────────────┬──────────────────────┐
│ len (4B)   │ free (4B)         │ chars[]              │
│ 已用长度    │ 剩余空间           │ 实际字符串数据         │
└────────────┴───────────────────┴──────────────────────┘

相比 C 字符串的优势:
- O(1) 获取长度(不需要遍历)
- 防止缓冲区溢出
- 空间预分配 + 惰性释放

性能特点

操作时间复杂度说明
SETO(1)常数时间
GETO(1)常数时间
MGETO(n)n 个 key
INCRO(1)原子递增
SETRANGEO(n)范围写入

使用场景

java
// 1. 普通缓存
redisTemplate.opsForValue().set("user:1001", userJson);

// 2. 计数器
redisTemplate.opsForValue().increment("page:view:20240101");

// 3. 分布式锁
redisTemplate.opsForValue().setIfAbsent("lock:order", "1", Duration.ofSeconds(10));

// 4. 分布式 session
redisTemplate.opsForValue().set("session:abc123", sessionJson, Duration.ofHours(2));

字符串的「编码」

bash
# 查看 key 的编码类型
OBJECT ENCODING "user:1001"

# 可能的结果:
# "raw"      - 超过 44 字节
# "int"      - 纯数字字符串(特殊优化)
# "embstr"   - 44 字节以下(更高效的内存布局)

Hash:对象存储

底层实现

Hash 有两种编码:

  • ziplist(压缩列表):元素少时用,内存紧凑
  • hashtable(字典):元素多时自动转换
ziplist:适合 < 512 个元素,每个 field/value < 64 字节
hashtable:无限制,但内存占用稍高

性能对比

操作ziplisthashtable
HGETO(n)O(1)
HSETO(n)O(1)
HGETALLO(n)O(n)
内存占用
适用场景少量字段多字段

使用场景

java
// 购物车:userId → {productId: count}
public void addToCart(Long userId, Long productId, int count) {
    String key = "cart:" + userId;
    
    if (count <= 0) {
        // 移除商品
        redisTemplate.opsForHash().delete(key, productId.toString());
    } else {
        // 添加/更新商品
        redisTemplate.opsForHash().put(key, productId.toString(), count);
    }
}

public Map<Long, Integer> getCart(Long userId) {
    String key = "cart:" + userId;
    Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
    
    return entries.entrySet().stream()
        .collect(Collectors.toMap(
            e -> Long.parseLong(e.getKey().toString()),
            e -> Integer.parseInt(e.getValue().toString())
        ));
}

Hash vs String(JSON)

java
// 方式 1:String 存储整个对象(JSON)
// 优点:简单,序列化/反序列化灵活
// 缺点:修改一个字段需要读取整个对象
String userJson = (String) redisTemplate.opsForValue().get("user:1001");
User user = JSON.parseObject(userJson);
user.setName("新名字");
redisTemplate.opsForValue().set("user:1001", JSON.toJSONString(user));

// 方式 2:Hash 存储对象字段
// 优点:可以单独修改字段
// 缺点:无法批量操作
redisTemplate.opsForHash().put("user:1001", "name", "新名字");

List:消息队列

底层实现

  • ziplist:元素少时用
  • linkedlist:元素多时用

性能特点

操作时间复杂度说明
LPUSH/RPOPO(1)左进右出(队列)
RPUSH/LPOPO(1)右进左出
LPUSH/LPUSHO(1)左进左出(栈)
LINDEXO(n)按索引获取(慎用)
LRANGEO(start+n)范围获取

使用场景

java
// 最新消息列表(类似微博 Timeline)
public void publishMessage(Long userId, String message) {
    String key = "timeline:" + userId;
    
    // LPUSH:左侧插入(最新在左边)
    redisTemplate.opsForList().leftPush(key, message);
    
    // 保留最新 100 条
    redisTemplate.opsForList().trim(key, 0, 99);
}

public List<String> getTimeline(Long userId, int page, int size) {
    String key = "timeline:" + userId;
    int start = page * size;
    
    return redisTemplate.opsForList().range(key, start, start + size - 1);
}

List vs Stream

java
// List 实现简单消息队列(生产一次,消费一次)
// 问题:无法确认消息是否被处理过

// Stream 实现可靠消息队列(支持消费确认)
// 优点:消息持久化、消费者组、确认机制
XADD mystream * sensor-id 1 temperature 25.5
XREADGROUP group1 consumer1 STREAMS mystream ">"
XACK mystream group1 $message-id

Set:集合运算

底层实现

  • intset:所有元素都是整数,且数量少
  • hashtable:其他情况

性能特点

操作时间复杂度说明
SADDO(1)添加元素
SISMEMBERO(1)存在性检查
SINTERO(n×m)交集(最慢)
SUNIONO(n)并集
SDIFFO(n)差集

使用场景

java
// 标签系统:一个商品多个标签
redisTemplate.opsForSet().add("product:1001:tags", "手机", "5G", "旗舰");

// 判断是否包含某个标签
Boolean hasTag = redisTemplate.opsForSet()
    .isMember("product:1001:tags", "5G");

// 好友关系
redisTemplate.opsForSet().add("user:1001:friends", "1002", "1003", "1004");
redisTemplate.opsForSet().add("user:1002:friends", "1001", "1003");

// 共同好友
Set<Object> mutualFriends = redisTemplate.opsForSet()
    .intersect("user:1001:friends", "user:1002:friends");

ZSet:排行榜

底层实现

  • ziplist:元素少时用(内存紧凑,但操作 O(n))
  • skiplist + dict:元素多时用(跳表保证有序,字典保证 O(1) 查找)

为什么用跳表?

普通链表:O(n) 查找
跳表:O(log n) 查找(多级索引)

跳表结构:
Level 2:  1 ────────────────────────────→ 10
Level 1:  1 ─────→ 5 ─────→ 8 ─────→ 10
Level 0:  1 → 3 → 5 → 7 → 8 → 9 → 10

查找 7:从 Level 2 开始,逐步下沉

性能特点

操作时间复杂度说明
ZADDO(log n)添加/更新
ZRANGEO(log n + m)按排名范围查
ZREVRANGEO(log n + m)倒序
ZSCOREO(1)获取分数
ZRANKO(log n)获取排名

使用场景

java
// 实时排行榜
public void updateScore(Long userId, double score) {
    redisTemplate.opsForZSet().add("leaderboard", userId.toString(), score);
}

public List<Long> getTopN(int n) {
    Set<Object> top = redisTemplate.opsForZSet()
        .reverseRange("leaderboard", 0, n - 1);
    
    return top.stream()
        .map(Object::toString)
        .map(Long::parseLong)
        .collect(Collectors.toList());
}

public Long getRank(Long userId) {
    Long rank = redisTemplate.opsForZSet()
        .reverseRank("leaderboard", userId.toString());
    return rank != null ? rank + 1 : null;  // 排名从 1 开始
}

Bitmap:位图操作

原理

用 bit 位存储数据,每个 bit 可以是 0 或 1。

用户签到(1 年 365 天):
普通存储:365 个 bool = 365 字节
Bitmap 存储:365 bits ≈ 46 字节
节省 80% 空间!

使用场景

java
// 用户签到
public void signIn(Long userId, LocalDate date) {
    String key = "sign:" + userId + ":" + date.format(DateTimeFormatter.ofPattern("yyyy"));
    int offset = date.getDayOfYear() - 1;  // 0-364
    
    redisTemplate.opsForValue().setBit(key, offset, true);
}

public boolean hasSignIn(Long userId, LocalDate date) {
    String key = "sign:" + userId + ":" + date.format(DateTimeFormatter.ofPattern("yyyy"));
    int offset = date.getDayOfYear() - 1;
    
    return Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, offset));
}

public long getConsecutiveDays(Long userId, LocalDate date) {
    // 统计连续签到天数
    long count = 0;
    LocalDate current = date;
    
    while (hasSignIn(userId, current)) {
        count++;
        current = current.minusDays(1);
    }
    
    return count;
}

HyperLogLog:基数统计

原理

用概率算法统计不重复元素的数量,标准误差 ≈ 0.81%。

存储 100 万用户 ID:
- 普通 Set:需要 ~100MB 内存
- HyperLogLog:只需要 ~12KB 内存
- 误差:0.81%,即 ±8100

适合场景:UV 统计、注册用户数统计
不适合场景:需要 100% 准确的数据

使用场景

java
// UV 统计
public void recordVisit(String pageId, String visitorId) {
    redisTemplate.opsForHyperLogLog().add("uv:" + pageId, visitorId);
}

public long getUV(String pageId) {
    return redisTemplate.opsForHyperLogLog().size("uv:" + pageId);
}

// 合并多天数据
redisTemplate.opsForHyperLogLog().union("uv:week", 
    "uv:20240101", "uv:20240102", "uv:20240103");

Stream:消息流

与 List 的区别

特性ListStream
持久化可选默认持久化
消费者组不支持支持
ACK 确认不支持支持
消息 ID自动生成(时间戳+序号)
消费确认支持

使用场景

java
// 创建消费者组
redisTemplate.opsForStream().createGroup("orders", "order-processors", "0");

// 生产消息
Map<String, Object> fields = new HashMap<>();
fields.put("order_id", "ORDER001");
fields.put("amount", 100.00);
redisTemplate.opsForStream().add("orders", fields);

// 消费消息
StreamOffset<String> offset = StreamOffset.create("orders", ReadOffset.last());
List<Map> messages = redisTemplate.opsForStream().read(
    String.class, String.class,
    Consumer.from("consumer-1", "group-1"),
    StreamReadOptions.empty().count(10),
    offset
);

// 确认消息
for (Map message : messages) {
    // 处理消息
    processOrder(message);
    
    // ACK 确认
    String messageId = message.getId();
    redisTemplate.opsForStream().acknowledge("orders", "group-1", messageId);
}

性能对比总结

类型写入性能读取性能内存效率适用场景
String⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐通用
Hash⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐对象
List⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐队列
Set⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐去重
ZSet⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐排行
Bitmap⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐状态
HyperLogLog⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐统计
Stream⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐队列

总结

Redis 的 9 种数据类型各有特点:

场景推荐类型
简单缓存String
用户信息Hash
消息队列List / Stream
标签/好友Set
排行榜ZSet
签到/状态Bitmap
UV 统计HyperLogLog
LBS 附近Geospatial

最佳实践

  • 不要过度设计:能用 String 就不用 Hash
  • 不要嵌套使用:Hash 嵌套 Hash 性能差
  • 注意编码转换:元素少用 ziplist,多用 hashtable

留给你的问题

假设你需要实现一个朋友圈 Timeline 功能:

需求:

  1. 每个用户有自己的 Timeline(最新动态)
  2. 发布动态时,Timeline 需要更新
  3. Timeline 按时间倒序
  4. 需要支持分页加载

请思考:

  1. 用 List、ZSet 还是 Stream 实现?为什么?
  2. 如果用户关注了 1000 人,Timeline 如何聚合?
  3. 如何实现「下拉刷新」和「上拉加载更多」?
  4. 如果一个人发了 10000 条动态,Timeline 最多保留多少条?

提示:Timeline 是「读多写少」的场景,适合用推拉结合的模式。

基于 VitePress 构建