熬夜整理!Redis 9种数据类型详解,第6种大部分人都没用过

从 String 到 Stream,从理论到实战,一文搞懂 Redis 所有数据类型


📌 写在前面

最近在复习 Redis,逛了下技术贴,发现挺多人只知道 String、List、Hash、Set、Sorted Set 这五种基本类型,遇到 UV 统计、附近的人、消息队列等场景就不知道怎么下手了。

其实 Redis 还有 Bitmap、HyperLogLog、Geo、Stream 等高级类型,用好它们,很多难题迎刃而解。

这篇文章花了几天时间整理,结合 Java 代码 + 实战场景,建议收藏慢慢看。


一、五种基本类型(面试必问,开发必会)

1.1 String —— 最通用的存储单元

一句话解释:一个 Key 对应一个字符串值,最大 512MB。二进制安全,图片、序列化对象都能存。

底层结构:SDS(简单动态字符串),预分配空间减少内存重分配。

实战场景

  • 缓存对象(JSON 字符串)
  • 计数器(阅读量、点赞数)
  • 分布式锁
  • Session 共享
// 实战:接口限流(每分钟最多100次)
public boolean rateLimit(String userId) {
    String key = "rate:limit:" + userId;
    try (Jedis jedis = pool.getResource()) {
        long count = jedis.incr(key);
        if (count == 1) {
            jedis.expire(key, 60);
        }
        return count <= 100;
    }
}

1.2 Hash —— 对象存储神器

一句话解释:类似 Java 的 HashMap,适合存储对象的各个属性,可单独操作每个字段。

底层结构:字段少用压缩列表(ziplist),字段多用哈希表(hashtable)。

实战场景

  • 用户信息(可单独更新年龄/昵称)
  • 购物车(商品ID为field,数量为value)
  • 配置项
// 实战:购物车功能
public void addItem(String userId, String productId, int quantity) {
    String key = "cart:" + userId;
    try (Jedis jedis = pool.getResource()) {
        jedis.hincrBy(key, productId, quantity);
        jedis.expire(key, 86400); // 保留1天
    }
}

public Map<String, String> getCart(String userId) {
    try (Jedis jedis = pool.getResource()) {
        return jedis.hgetAll("cart:" + userId);
    }
}

1.3 List —— 有序可重复的队列

一句话解释:双向链表结构,可以从头尾两侧操作,有序可重复。

底层结构:数据少用压缩列表,数据多用快速列表(quicklist)。

实战场景

  • 消息队列(生产者 rpush,消费者 lpop)
  • 最新动态(lpush + ltrim)
  • 历史记录
// 实战:用户操作日志(保留最近100条)
public void recordAction(String userId, String action) {
    String key = "actions:" + userId;
    try (Jedis jedis = pool.getResource()) {
        jedis.lpush(key, System.currentTimeMillis() + "|" + action);
        jedis.ltrim(key, 0, 99); // 只保留100条
    }
}

1.4 Set —— 自动去重的集合

一句话解释:无序且元素唯一,支持交集、并集、差集运算。

底层结构:全是整数用整数集合(intset),否则用哈希表。

实战场景

  • 标签系统(文章的标签集合)
  • 共同好友(sinter)
  • 抽奖(spop随机抽取)
// 实战:抽奖系统
public Set<String> draw(String activityId, int n) {
    try (Jedis jedis = pool.getResource()) {
        // 随机抽取n个中奖者(不放回)
        return jedis.spop("lottery:" + activityId + ":participants", n);
    }
}

1.5 Sorted Set —— 带排名的集合

一句话解释:每个元素关联一个分数(score),按分数自动排序,元素唯一。

底层结构:数据少用压缩列表,数据多用跳表(skiplist)+ 哈希表。

实战场景

  • 排行榜(游戏积分榜)
  • 延迟队列(分数=执行时间戳)
  • 带权重的任务队列
// 实战:游戏排行榜
public void updateScore(String playerId, long score) {
    try (Jedis jedis = pool.getResource()) {
        jedis.zadd("game:leaderboard", score, playerId);
    }
}

// 获取前10名
public Set<Tuple> getTop10() {
    try (Jedis jedis = pool.getResource()) {
        return jedis.zrevrangeWithScores("game:leaderboard", 0, 9);
    }
}

二、高级类型(大部分人没用过,但真的很强)

以下内容建议重点看,面试官问"用过哪些高级数据类型"时,你可以侃侃而谈。

2.1 Bitmap —— 位操作的大杀器

本质:String 类型的特殊用法,把字符串当成位数组操作。1个字节 = 8个位。

内存计算:存储 1 亿个用户的布尔状态仅需 12MB

实战场景

  • 签到记录(365天只需46字节)
  • 在线状态
  • 用户行为埋点
// 实战:签到系统
public void signIn(long userId, LocalDate date) {
    String key = "sign:" + userId + ":" + date.getYear();
    int dayOfYear = date.getDayOfYear(); // 1-365
    try (Jedis jedis = pool.getResource()) {
        jedis.setbit(key, dayOfYear - 1, true);
    }
}

// 本月签到天数
public long getMonthSignCount(long userId, YearMonth month) {
    String key = "sign:" + userId + ":" + month.getYear();
    int startDay = month.atDay(1).getDayOfYear();
    int endDay = month.atEndOfMonth().getDayOfYear();
    try (Jedis jedis = pool.getResource()) {
        return jedis.bitcount(key, startDay - 1, endDay - 1);
    }
}

2.2 HyperLogLog —— 基数统计黑科技

一句话解释:统计不重复元素的数量(基数),不存储元素本身,只存概率估算值。

内存占用:每个 key 仅 12KB,可统计上亿数据!

错误率:约 0.81%,适合不要求精确计数的场景。

实战场景

  • UV 统计(独立访客数)
  • 日活月活
  • 搜索词去重计数
// 实战:页面UV统计
public void visit(String pageId, String userId) {
    String key = "uv:page:" + pageId + ":" + LocalDate.now();
    try (Jedis jedis = pool.getResource()) {
        jedis.pfadd(key, userId);
        jedis.expire(key, 2592000); // 30天后过期
    }
}

public long getTodayUV(String pageId) {
    try (Jedis jedis = pool.getResource()) {
        return jedis.pfcount("uv:page:" + pageId + ":" + LocalDate.now());
    }
}

2.3 Geo —— 地理位置就用它

一句话解释:存储经纬度,支持距离计算和范围查询。基于 Sorted Set 实现。

实战场景

  • 附近的人
  • 打车匹配
  • 门店距离排序
// 实战:附近店铺搜索
public void addShop(Long shopId, double lng, double lat, String name) {
    try (Jedis jedis = pool.getResource()) {
        jedis.geoadd("geo:shops", lng, lat, String.valueOf(shopId));
        jedis.hset("shop:" + shopId, "name", name);
    }
}

// 查找附近1km内的店铺
public List<GeoRadiusResponse> findNearby(double lng, double lat, double radiusKm) {
    try (Jedis jedis = pool.getResource()) {
        return jedis.georadius("geo:shops", lng, lat, radiusKm, 
            GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().sortAscending());
    }
}

2.4 Bitfield —— 终极位操作器

一句话解释:Redis 3.2+ 引入,可对一个字符串进行多段位域操作,支持有符号/无符号整数。

优势:一条命令完成多个位的原子操作,比多次 getbit/setbit 高效得多。

实战场景

  • 紧凑存储多个小数字(用户的多项统计数据)
  • 原子计数器(库存分段扣减)
// 实战:用户原子统计数据(64位存储多个指标)
// 位域布局:bit0-31登录次数 | bit32-47等级 | bit48-55VIP等级
public void incrLoginCount(String userId) {
    try (Jedis jedis = pool.getResource()) {
        // INCRBY 对指定位域递增(32位无符号)
        jedis.bitfield("stats:" + userId, "INCRBY", "u32", "0", "1");
    }
}

public Map<String, Long> getAllStats(String userId) {
    try (Jedis jedis = pool.getResource()) {
        List<Long> results = jedis.bitfield("stats:" + userId,
            "GET", "u32", "0",  // 登录次数
            "GET", "u16", "32", // 等级
            "GET", "u8", "48"); // VIP等级
        // 封装返回...
    }
}

2.5 Stream —— 消息队列完全体

一句话解释:Redis 5.0 引入,专为消息队列设计,类似 Kafka 的简化版。

核心特性

  • ✅ 消费者组(Consumer Group):多个消费者分摊消息
  • ✅ 消息确认机制(ACK):可靠消费
  • ✅ 消息持久化:消息存在 Redis 中
  • ✅ 阻塞读取:支持阻塞和非阻塞两种模式

实战场景

  • 替代 List 做更可靠的消息队列
  • 事件溯源
  • 日志收集
  • 通知系统
// 实战:订单事件处理
public void publishOrderEvent(String orderId, String eventType, Map<String, String> data) {
    Map<String, String> event = new HashMap<>();
    event.put("orderId", orderId);
    event.put("eventType", eventType);
    event.put("timestamp", String.valueOf(System.currentTimeMillis()));
    event.putAll(data);
    
    try (Jedis jedis = pool.getResource()) {
        jedis.xadd("stream:order-events", StreamEntryID.NEW_ENTRY, event);
    }
}

// 消费者组消费
public void consumeMessages(String consumerName, Consumer<Map<String, String>> handler) {
    try (Jedis jedis = pool.getResource()) {
        while (true) {
            List<Map.Entry<String, List<StreamEntry>>> entries = jedis.xreadGroup(
                GroupParams.groupParams("order-processors", consumerName)
                    .block(1000).count(10),
                StreamEntryID.LAST_ENTRY, "stream:order-events");
            
            for (StreamEntry entry : entries.get(0).getValue()) {
                handler.accept(entry.getFields());
                jedis.xack("stream:order-events", "order-processors", entry.getID());
            }
        }
    }
}

2.6 Bloom Filter —— 布隆过滤器(Redis Module)

需要 Redis 4.0+ 并加载 RedisBloom 模块

一句话解释:一种概率性数据结构,能确定元素不存在,但判断存在可能有误判

内存对比:1亿数据,Set 需要 ~1.5GB,Bloom Filter 只需 ~170MB!

误判率:0.01%~1%,可通过参数调节。

实战场景

  • 缓存穿透防护(过滤不存在的 key)
  • 爬虫 URL 去重
  • 垃圾邮件过滤
  • 黑名单系统
// 实战:防缓存穿透
public Product getProduct(String productId) {
    // 1. 布隆过滤器判断(一定不存在就直接返回)
    if (!bloomFilter.mightExist(productId)) {
        return null;
    }
    
    // 2. 查缓存
    try (Jedis jedis = pool.getResource()) {
        String cached = jedis.get("product:" + productId);
        if (cached != null) {
            return "null".equals(cached) ? null : JSON.parseObject(cached, Product.class);
        }
    }
    
    // 3. 查数据库
    Product product = dao.findById(productId);
    
    // 4. 写入缓存
    try (Jedis jedis = pool.getResource()) {
        if (product == null) {
            jedis.setex("product:" + productId, 60, "null");
        } else {
            jedis.setex("product:" + productId, 3600, JSON.toJSONString(product));
        }
    }
    return product;
}

三、选型速查表(建议截图保存)

场景 推荐类型 原因
缓存对象(频繁改单字段) Hash 可单独更新,不用重写整个对象
缓存对象(整体读写) String(JSON) 一次读完,简单方便
排行榜 Sorted Set 天然按分数排序
UV统计(允许误差) HyperLogLog 12KB统计上亿数据
UV统计(精确) Bitmap + Set 精确但内存大
附近的人 Geo 开箱即用的距离计算
可靠消息队列 Stream 支持ACK和消费者组
简单任务队列 List 简单高效
去重+统计数量 Set 精确去重
去重判断(允许误差) Bloom Filter 内存节省90%+
签到/布尔状态 Bitmap 1个bit存一个状态
紧凑存储多个小数值 Bitfield 一个key存多个指标

四、常见面试题

Q1:Redis 为什么这么快?

  • 纯内存操作
  • 单线程(6.0前)避免上下文切换
  • IO 多路复用
  • 高效的数据结构设计

Q2:如何防止缓存穿透?

  • 布隆过滤器(推荐)
  • 缓存空对象
  • 接口层校验

Q3:HyperLogLog 的误差是多少?

  • 约 0.81%,但每个 key 仅 12KB,适合大数据量 UV 统计

Q4:Stream 和 List 做消息队列有什么区别?

  • List 不支持 ACK,消费者挂了消息就丢失
  • Stream 支持消费者组、ACK、消息持久化,更可靠

五、写在最后

Redis 的数据类型远不止面试常问的那五种。用好高级类型,可以:

  • ✅ 节省 90%+ 内存(Bitmap、HyperLogLog、Bloom Filter)
  • ✅ 简化代码逻辑(Geo 省去手动计算距离)
  • ✅ 提升系统可靠性(Stream 提供 ACK 机制)

建议收藏本文,遇到对应场景时翻出来看看,选对数据结构,代码优雅十倍。


📢 如果觉得有用,点赞 + 评论 + 收藏 三连支持一下~

💬 评论区聊聊:你还用过哪些 Redis 高级功能?


标签Redis Java 后端开发 面试题 缓存


本文作者:code_space | 首发:CSDN | 转载请联系作者

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐