【Redis分布式缓存实战】第2章 Redis核心数据结构与业务实战场景

本章前言

Redis 之所以能成为分布式架构中首选的缓存、中间件,高性能、低延迟的核心根基,正是其精心设计的底层数据结构。Redis 对外提供了十余种数据类型,不同类型对应不同的底层实现、性能特征与适用场景。

在分布式缓存落地过程中,80% 的线上问题都源于数据结构选型错误:比如用字符串存储结构化对象导致维护困难、用 List 实现海量排行榜引发性能卡顿、盲目使用集合运算造成 CPU 飙升等。

本章将分为基础五大核心类型高级特色类型两大板块,逐一讲解每种数据结构的原理、高频命令、核心特性,并结合互联网真实业务场景落地,搭配可直接复用的代码示例、设计思路与踩坑要点。最后附上全类型选型对照表与线上避坑指南,帮你做到 "根据业务选结构,而不是只会用字符串"。

学习目标:

  1. 掌握 Redis 所有实战高频数据结构的特性与常用命令;
  2. 理解每种结构底层编码对性能、内存的影响;
  3. 能够根据业务需求快速匹配合适的数据结构;
  4. 掌握主流业务(计数器、排行榜、签到、UV、消息队列等)的 Redis 落地方案。

1. 5大基础数据结构底层原理 + 真实业务落地场景(String/List/Hash/Set/ZSet)

Redis 数据结构整体概述

Redis 对外暴露的数据类型 ,和其底层真正的存储编码是两个概念,这是实战优化的基础:

  1. 对外数据类型 :开发者通过命令操作的类型,分为基础 5 大类型 (String、List、Hash、Set、ZSet)和高级特色类型(Bitmap、HyperLogLog、Geo、Stream 等);
  2. 底层编码 :Redis 会根据数据大小、数量自动切换底层编码(如压缩列表、双向链表、跳表、字典等),目的是兼顾内存占用与读写性能

所有 Redis 数据结构都以 key-value 形式存储,​​key​​ 统一为字符串,我们主要根据 ​​value​​ 的类型区分使用场景。

下面从使用频率最高的基础类型开始讲解。


一、String 字符串类型(最通用、使用最广)

结构简介

String 是 Redis 最基础的数据类型,单个 value 最大容量为 512MB

  • 底层编码:根据内容自动切换 int(纯数字)、embstr(短字符串)、raw(长字符串);
  • 核心特性:单线程原子操作、支持读写、数值自增、过期设置、批量操作;
  • 适用定位:单值缓存、简单计数、临时状态存储。
高频实战命令

仅整理线上开发必用命令,舍弃冷门语法:

Crystal 复制代码
# 1. 基础读写
SET key value [EX 过期秒数]  # 设置值,支持同时设置过期时间
GET key                      # 获取值
DEL key                      # 删除key

# 2. 批量操作(提升网络IO效率)
MSET k1 v1 k2 v2 k3 v3      # 批量设置
MGET k1 k2 k3               # 批量获取

# 3. 原子数值运算(核心:单线程保证并发安全)
INCR key                    # 数值+1
INCRBY key 步长             # 数值+指定步长
DECR key                    # 数值-1

# 4. 追加字符串
APPEND key value
底层原理

SDS(Simple Dynamic String)结构:

Crystal 复制代码
struct sdshdr {
    int len;        // 已使用的字节长度
    int free;       // 未使用的字节长度
    char buf[];     // 字节数组
};

内存优化:

  • 长度≤39字节:embstr编码(RedisObject和SDS连续存储)
  • 长度>39字节:raw编码(RedisObject和SDS分开存储)
  • 数字类型:int编码(直接存储整数)
核心业务实战场景

String 几乎覆盖半数缓存场景,以下为互联网主流落地案例。

场景 1:通用业务缓存(用户基础信息、商品简介、配置项)

业务描述 :热点静态数据(商品名称、头像、系统配置、首页公告),查询频率极高、修改频率低。设计思路:直接将 JSON 字符串存入 String,设置合理过期时间,减轻数据库压力。

Java 示例(Spring Data Redis)

java 复制代码
// 存入商品信息(JSON字符串)
redisTemplate.opsForValue().set("goods:info:1001", "{\"id\":1001,\"name\":\"手机\",\"price\":2999}", 3600, TimeUnit.SECONDS);

// 查询商品信息
String goodsJson = redisTemplate.opsForValue().get("goods:info:1001");

落地要点

  • 统一 key 命名规范:业务:模块:唯一标识,便于运维排查;
  • 静态数据设置过期时间,配合缓存穿透 / 缓存雪崩防护(后续章节详解);
  • 单个 value 尽量不超过 10KB,过大字符串会增加网络传输耗时。
场景 2:分布式计数器(阅读量、点赞数、播放量)

业务描述 :文章阅读数、短视频播放量、帖子点赞数,高并发累加场景。设计思路 :利用 ​​INCR​​ 原子自增,无需额外加锁,天然支持并发。

落地要点

  • 热点计数器可拆分 key分片,避免单个 key 压力过大;
  • 定时将 Redis 计数同步至数据库,防止数据丢失。
场景 3:简易分布式全局 ID

业务描述 :分布式系统生成全局唯一自增 ID。设计思路 :使用 ​​INCR​​ 自增,结合业务前缀拼接成唯一 ID。

场景 4:接口限流(单机 / 分布式简易限流)

业务描述 :限制单个 IP / 用户每秒请求次数,防刷接口。设计思路 :以 ​​IP+接口名​​ 为 key,1 秒过期,每次请求自增,超过阈值则拒绝访问。

场景 5:简易分布式锁

利用 ​​SET key value EX 过期时间 NX​​ 实现排他锁,适用于低并发简单锁场景。

注意:简易锁存在续约、误删问题,高并发分布式场景建议使用 RedLock(后续分布式锁章节详解)。

优缺点总结
  • 优点:命令简单、原子性强、通用性高、支持过期;
  • 缺点:不适合结构化对象(JSON 字符串更新单个字段需全量覆盖)、超大 value 影响性能。
Java实战场景
java 复制代码
// 1. 缓存场景
@Component
public class UserCacheService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 缓存用户信息
    public void cacheUserInfo(User user) {
        String key = "user:" + user.getId();
        String value = JSON.toJSONString(user);
        // 设置30分钟过期
        redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
    }
    
    // 2. 计数器场景
    public Long incrementViewCount(Long articleId) {
        String key = "article:view:" + articleId;
        return redisTemplate.opsForValue().increment(key);
    }
    
    // 3. 分布式锁
    public boolean tryLock(String lockKey, String requestId, long expireTime) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue().setIfAbsent(
                lockKey, 
                requestId, 
                expireTime, 
                TimeUnit.SECONDS
            )
        );
    }
    
    // 4. Session共享
    public void saveSession(String sessionId, UserSession session) {
        String key = "session:" + sessionId;
        redisTemplate.opsForValue().set(key, session, 30, TimeUnit.MINUTES);
    }
}

二、List 列表类型(有序、可重复、双向队列)

结构简介

List 是有序、元素可重复 的字符串列表,底层采用双向链表实现。

  • 核心特性:头尾操作极快(O (1)),中间元素读写性能差(O (n));
  • 支持左右两端插入、弹出,天然具备队列、栈的特性;
  • 可实现阻塞队列(客户端等待消息)。
高频实战命令
Crystal 复制代码
# 左侧/右侧插入(左=表头,右=表尾)
LPUSH key v1 v2    # 表头插入
RPUSH key v1 v2    # 表尾插入

# 左侧/右侧弹出
LPOP key
RPOP key

# 阻塞弹出(无元素时客户端阻塞等待,超时自动返回)
BLPOP key 超时秒数
BRPOP key 超时秒数

# 范围查询(分页)
LRANGE key start end  # end=-1 代表查询所有元素
底层原理

两种实现方式:

  1. 压缩列表(ziplist) :元素数量<512且元素大小<64字节时使用
  2. 双向链表(linkedlist) :不满足上述条件时使用
  3. Redis 3.2+ :quicklist(链表+ziplist组合)
核心业务实战场景
场景 1:简易消息队列 / 任务队列

业务描述 :异步任务分发(日志收集、短信发送、离线任务),轻量化队列场景。设计思路 :​​RPUSH​​ 生产消息,​​BLPOP​​ 阻塞消费,实现简易消息队列。落地要点

  • List 队列无消息确认、无重试、无消费组,仅适用于非核心、允许丢失的轻量任务;
  • 核心业务消息推荐使用 Redis Stream 或专业 MQ(RocketMQ/Kafka)。
场景 2:Feed 流 / 朋友圈时间线

业务描述 :社交 APP 朋友圈、动态列表,按发布时间倒序展示。设计思路 :用户发布动态时,向粉丝 List 表头 ​​LPUSH​​ 动态 ID,查询时用 ​​LRANGE​​ 分页读取。

场景 3:时序数据临时分页

对日志、操作记录等有序数据做临时分页查询。

优缺点总结
  • 优点:头尾操作性能极致、支持阻塞、实现队列成本极低;
  • 缺点:中间元素遍历慢、不支持去重、无高级队列特性,不适合海量长链表。
Java实战场景
java 复制代码
@Component
public class MessageQueueService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 1. 消息队列
    public void pushMessage(String queueName, String message) {
        redisTemplate.opsForList().leftPush(queueName, message);
        // 限制队列长度,防止内存溢出
        redisTemplate.opsForList().trim(queueName, 0, 999);
    }
    
    public String popMessage(String queueName) {
        return redisTemplate.opsForList().rightPop(queueName);
    }
    
    // 2. 最新动态/时间线
    public void addUserActivity(Long userId, String activity) {
        String key = "user:activity:" + userId;
        redisTemplate.opsForList().lPush(key, activity);
        // 只保留最近100条动态
        redisTemplate.opsForList().trim(key, 0, 99);
    }
    
    // 3. 商品浏览历史
    public void addBrowseHistory(Long userId, Long productId) {
        String key = "user:browse:" + userId;
        String value = String.valueOf(productId);
        
        // 移除已存在的记录(去重)
        redisTemplate.opsForList().remove(key, 1, value);
        // 添加到列表头部
        redisTemplate.opsForList().lPush(key, value);
        // 只保留最近20条记录
        redisTemplate.opsForList().trim(key, 0, 19);
    }
    
    // 4. 分页获取数据
    public List<String> getPagedActivities(Long userId, int page, int size) {
        String key = "user:activity:" + userId;
        int start = (page - 1) * size;
        int end = start + size - 1;
        return redisTemplate.opsForList().range(key, start, end);
    }
}

三、Hash 哈希类型(结构化对象首选)

结构简介

Hash 类似 Java 中的 ​​HashMap​​,是键中键 结构:​​key -> field -> value​​。

  • 底层编码:元素较少时使用 压缩列表 (ziplist)(内存极小),超过阈值自动转为字典 (hashtable);
  • 核心特性:支持单个字段独立读写,无需修改整个对象,非常适合结构化数据。
高频实战命令
Crystal 复制代码
# 设置单个/多个字段
HSET key field value
HMSET key f1 v1 f2 v2

# 获取单个/多个字段
HGET key field
HMGET key f1 f2

# 字段原子自增
HINCRBY key field 步长

# 获取所有字段和值
HGETALL key
底层原理

两种实现方式:

  1. ziplist :field数量≤512且value大小≤64字节
  2. hashtable :不满足上述条件时使用
核心业务实战场景
场景 1:结构化对象缓存(用户 / 商品详情)

业务对比

  • String 方案:将对象转为 JSON,修改单个字段需要全量更新,效率低;
  • Hash 方案:对象 = Redis Key,属性 = field,属性值 = value,支持局部更新

适用案例:用户信息(昵称、头像、手机号、积分)、商品多维度属性。

场景 2:购物车实现

业务描述:电商用户购物车,用户 ID 为 key,商品 ID 为 field,购买数量为 value。优势:新增、删除、修改商品数量都只操作单个 field,性能远优于 String。

场景 3:多维度统计指标

比如商品的浏览量、收藏量、加购量,统一存放在一个 Hash 中,归类清晰。

优缺点 & 调优要点
  • 优点:结构化存储、局部更新、内存占用低;
  • 缺点:HGETALL 会一次性取出所有字段,字段过多时网络开销大;
  • 调优:控制单个 Hash 的 field 数量,默认阈值可在配置文件调整 hash-max-ziplist-entries
Java实战场景
java 复制代码
@Component
public class ShoppingCartService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 1. 购物车实现
    public void addToCart(Long userId, Long productId, Integer quantity) {
        String key = "cart:" + userId;
        String field = "product:" + productId;
        
        if (quantity <= 0) {
            // 删除商品
            redisTemplate.opsForHash().delete(key, field);
        } else {
            // 添加或更新商品数量
            redisTemplate.opsForHash().put(key, field, quantity.toString());
        }
        
        // 设置购物车过期时间(7天)
        redisTemplate.expire(key, 7, TimeUnit.DAYS);
    }
    
    // 2. 对象缓存(用户信息)
    public void cacheUserDetails(Long userId, Map<String, String> userDetails) {
        String key = "user:details:" + userId;
        redisTemplate.opsForHash().putAll(key, userDetails);
    }
    
    // 3. 商品规格属性存储
    public void cacheProductAttributes(Long productId, Map<String, String> attributes) {
        String key = "product:attrs:" + productId;
        redisTemplate.opsForHash().putAll(key, attributes);
    }
    
    // 4. 统计信息
    public void incrementStats(String date, String metric) {
        String key = "stats:" + date;
        redisTemplate.opsForHash().increment(key, metric, 1);
    }
    
    // 获取购物车商品总数
    public Long getCartItemCount(Long userId) {
        String key = "cart:" + userId;
        return redisTemplate.opsForHash().size(key);
    }
}

四、Set 集合类型(无序、自动去重)

结构简介

Set 是无序、元素唯一的字符串集合,底层基于字典实现,天生自动去重。

  • 核心特性:元素不可重复、支持交集、并集、差集三大集合运算、支持随机取元素。
高频实战命令
Crystal 复制代码
SADD key v1 v2        # 添加元素(自动去重)
SISMEMBER key v      # 判断元素是否存在(是否点赞/关注)
SMEMBERS key         # 获取所有元素
SRANDMEMBER key 数量 # 随机获取元素(抽奖)

# 集合运算(多Set之间计算)
SINTER 集合1 集合2    # 交集(共同好友)
SUNION 集合1 集合2    # 并集
SDIFF  集合1 集合2    # 差集
底层原理

两种实现方式:

  1. intset :元素都是整数且元素数量≤512
  2. hashtable :不满足上述条件时使用(value为null的哈希表)
核心业务实战场景
场景 1:点赞、收藏、关注(去重核心场景)

业务描述 :帖子点赞、用户关注列表,一个用户只能点赞一次。设计思路 :帖子 ID 为 key,点赞用户 ID 为 Set 元素,利用 Set 自动去重,​​SISMEMBER​​ 判断是否已点赞。

场景 2:好友关系、共同好友计算

用户好友列表存入 Set,通过 ​​SINTER​​ 计算两人共同好友。

场景 3:随机抽奖

活动奖品抽取,使用 ​​SRANDMEMBER​​ 随机取出指定数量用户,保证不重复中奖。

落地要点

大数据量集合(十万级以上)谨慎使用集合运算,集合运算属于重 CPU 操作,易引发服务卡顿。

Java实战场景
java 复制代码
@Component
public class SocialService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 1. 好友关系/关注系统
    public void followUser(Long userId, Long targetUserId) {
        String followingKey = "user:following:" + userId;
        String followersKey = "user:followers:" + targetUserId;
        
        redisTemplate.opsForSet().add(followingKey, targetUserId.toString());
        redisTemplate.opsForSet().add(followersKey, userId.toString());
    }
    
    // 2. 共同好友/兴趣匹配
    public Set<String> getCommonFriends(Long user1Id, Long user2Id) {
        String key1 = "user:friends:" + user1Id;
        String key2 = "user:friends:" + user2Id;
        
        return redisTemplate.opsForSet().intersect(key1, key2);
    }
    
    // 3. 标签系统
    public void tagUser(Long userId, String tag) {
        String userTagsKey = "user:tags:" + userId;
        String tagUsersKey = "tag:users:" + tag;
        
        redisTemplate.opsForSet().add(userTagsKey, tag);
        redisTemplate.opsForSet().add(tagUsersKey, userId.toString());
    }
    
    // 4. 抽奖/随机推荐
    public String randomRecommendProduct(Long userId) {
        String viewedKey = "user:viewed:" + userId;
        String allProductsKey = "products:all";
        
        // 从未浏览的商品中随机推荐
        return redisTemplate.opsForSet().randomMember(
            redisTemplate.opsForSet().difference(allProductsKey, viewedKey)
        );
    }
    
    // 5. 数据去重
    public boolean isUrlCrawled(String url) {
        String key = "crawled:urls";
        boolean isMember = Boolean.TRUE.equals(
            redisTemplate.opsForSet().isMember(key, url)
        );
        
        if (!isMember) {
            redisTemplate.opsForSet().add(key, url);
        }
        
        return isMember;
    }
}

五、Sorted Set (ZSet) 有序集合(带分值、排序 + 去重)

结构简介

ZSet(有序集合)是 Redis 功能最强的基础类型,元素唯一、附带分值 (score)、自动按分值排序

  • 底层编码:跳表 + 字典结合,兼顾排序性能与查询性能;
  • 核心特性:去重 + 全局排序、支持按分值范围查询、分值可动态更新。
高频实战命令
Crystal 复制代码
ZADD key score1 v1 score2 v2  # 添加元素+分值
ZRANGE key start end [WITHSCORES]  # 正序分页(从小到大)
ZREVRANGE key start end [WITHSCORES] # 倒序分页(从大到小,排行榜主流)
ZINCRBY key 步长 元素  # 分值自增
ZREM key 元素          # 删除元素
底层原理

两种实现方式:

  1. ziplist :元素数量≤128且元素大小≤64字节
  2. skiplist + hashtable :不满足条件时使用
  1. 跳表:支持范围查询
  2. 哈希表:支持O(1)查找分数
核心业务实战场景
场景 1:各类排行榜(最经典场景)

业务描述 :热搜榜、商品销量榜、用户积分榜、短视频热度榜。设计思路

  • Key:排行榜名称(rank:hot:day 日热搜);
  • Member:排名对象(文章 ID / 用户 ID);
  • Score:排名依据(热度、销量、积分);
  • 查询:使用 ZREVRANGE 倒序分页,直接实现榜单。
场景 2:简易延时队列

利用分值存储时间戳,定时轮询读取小于当前时间戳的元素,实现延时任务。

场景 3:带权重的任务分配

根据权重分值排序,优先处理高权重任务。

落地要点
  • 百万级以上榜单建议做冷热分离,避免单个 ZSet 元素过多;
  • 分值支持小数,可灵活实现精细化排序。
Java实战场景
java 复制代码
@Component
public class RankingService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 1. 排行榜系统
    public void addPlayerScore(String gameId, String playerId, double score) {
        String key = "game:ranking:" + gameId;
        redisTemplate.opsForZSet().add(key, playerId, score);
        
        // 只保留前1000名
        redisTemplate.opsForZSet().removeRange(key, 0, -1001);
    }
    
    // 2. 延时任务
    public void addDelayTask(String taskId, long executeTime, String taskData) {
        String key = "delay:tasks";
        redisTemplate.opsForZSet().add(key, taskData, executeTime);
    }
    
    public List<String> getDueTasks() {
        String key = "delay:tasks";
        long now = System.currentTimeMillis();
        
        // 获取到期的任务
        Set<String> tasks = redisTemplate.opsForZSet().rangeByScore(key, 0, now);
        
        // 移除已获取的任务
        if (tasks != null && !tasks.isEmpty()) {
            redisTemplate.opsForZSet().removeRangeByScore(key, 0, now);
        }
        
        return new ArrayList<>(tasks != null ? tasks : Collections.emptySet());
    }
    
    // 3. 热门搜索词
    public void incrementSearchKeyword(String keyword) {
        String key = "search:hot";
        redisTemplate.opsForZSet().incrementScore(key, keyword, 1);
        
        // 每小时清理一次过时数据
        long oneHourAgo = System.currentTimeMillis() - 3600000;
        redisTemplate.opsForZSet().removeRangeByScore(key, 0, oneHourAgo);
    }
    
    // 4. 时间线排序
    public void addToTimeline(Long userId, String contentId, long timestamp) {
        String key = "user:timeline:" + userId;
        redisTemplate.opsForZSet().add(key, contentId, timestamp);
        
        // 保留最近1000条
        redisTemplate.opsForZSet().removeRange(key, 0, -1001);
    }
    
    // 5. 价格区间查询
    public Set<String> getProductsByPriceRange(double minPrice, double maxPrice) {
        String key = "products:price:sorted";
        return redisTemplate.opsForZSet().rangeByScore(key, minPrice, maxPrice);
    }
    
    // 获取用户排名
    public Long getPlayerRank(String gameId, String playerId) {
        String key = "game:ranking:" + gameId;
        Long rank = redisTemplate.opsForZSet().reverseRank(key, playerId);
        return rank != null ? rank + 1 : null; // 从1开始排名
    }
}

六、综合业务场景示例

java 复制代码
@Service
public class ECommerceService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 电商秒杀系统综合实现
     */
    public boolean seckill(Long productId, Long userId) {
        String productStockKey = "seckill:stock:" + productId;
        String productSoldKey = "seckill:sold:" + productId;
        String userBoughtKey = "seckill:users:" + productId;
        String orderQueueKey = "seckill:orders:queue";
        
        // 1. 校验库存(String)
        Long stock = redisTemplate.opsForValue().decrement(productStockKey);
        if (stock == null || stock < 0) {
            // 库存不足,恢复库存
            redisTemplate.opsForValue().increment(productStockKey);
            return false;
        }
        
        // 2. 防止重复购买(Set)
        Boolean isMember = redisTemplate.opsForSet().isMember(userBoughtKey, userId.toString());
        if (Boolean.TRUE.equals(isMember)) {
            redisTemplate.opsForValue().increment(productStockKey);
            return false;
        }
        
        // 3. 记录购买用户(Set)
        redisTemplate.opsForSet().add(userBoughtKey, userId.toString());
        
        // 4. 增加已售数量(String)
        redisTemplate.opsForValue().increment(productSoldKey);
        
        // 5. 创建订单到队列(List)
        Map<String, String> order = new HashMap<>();
        order.put("orderId", UUID.randomUUID().toString());
        order.put("productId", productId.toString());
        order.put("userId", userId.toString());
        order.put("createTime", String.valueOf(System.currentTimeMillis()));
        
        redisTemplate.opsForList().leftPush(orderQueueKey, JSON.toJSONString(order));
        
        // 6. 更新排行榜(ZSet)
        String rankingKey = "seckill:ranking";
        redisTemplate.opsForZSet().incrementScore(rankingKey, productId.toString(), 1);
        
        return true;
    }
    
    /**
     * 获取商品详情页数据
     */
    public Map<String, Object> getProductDetail(Long productId) {
        Map<String, Object> result = new HashMap<>();
        
        // 1. 商品基本信息(Hash)
        String productKey = "product:" + productId;
        Map<Object, Object> productInfo = redisTemplate.opsForHash().entries(productKey);
        result.put("productInfo", productInfo);
        
        // 2. 商品浏览次数(String)
        String viewKey = "product:views:" + productId;
        Long views = (Long) redisTemplate.opsForValue().get(viewKey);
        result.put("views", views != null ? views : 0);
        
        // 3. 商品评论(List)
        String commentKey = "product:comments:" + productId;
        List<Object> comments = redisTemplate.opsForList().range(commentKey, 0, 9);
        result.put("recentComments", comments);
        
        // 4. 用户收藏状态(Set)
        Long userId = getCurrentUserId();
        if (userId != null) {
            String favoriteKey = "user:favorites:" + userId;
            Boolean isFavorited = redisTemplate.opsForSet().isMember(favoriteKey, productId.toString());
            result.put("isFavorited", Boolean.TRUE.equals(isFavorited));
        }
        
        // 5. 相似商品推荐(Set交集)
        String productTagsKey = "product:tags:" + productId;
        Set<Object> similarProducts = redisTemplate.opsForSet().intersect(
            productTagsKey, 
            "products:hot"
        );
        result.put("similarProducts", similarProducts);
        
        return result;
    }
}

七、最佳实践建议

  1. 内存优化 :
  1. 合理设置过期时间
  2. 使用合适的数据结构
  3. 避免大Key(单个Key不要超过10KB)
  1. 性能优化 :
  1. 使用Pipeline批量操作
  2. 合理使用连接池
  3. 避免频繁的序列化/反序列化
  1. 数据结构选择指南 :
  1. 需要过期时间 → String
  2. 需要排序 → ZSet
  3. 需要去重 → Set
  4. 存储对象 → Hash
  5. 队列/栈 → List
  1. 监控指标 :
  1. 内存使用率
  2. 命令延迟
  3. 命中率
  4. 连接数

通过合理选择和使用Redis的数据结构,可以显著提升系统性能,解决分布式系统中的常见问题。

2. 高阶特殊数据结构实战:Bitmap、HyperLogLog、Geo、Stream、BitField

一、概述

Redis不仅支持基本的字符串、列表、集合等数据结构,还提供了多种高阶数据结构,适用于特定场景。本文将详细介绍Bitmap、HyperLogLog、Geo、Stream和BitField这五种高阶数据结构,并提供Java实战代码。

二、环境准备

2.1 Maven依赖
XML 复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.1</version>
</dependency>
2.2 Redis连接配置
java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisClient {
    private static JedisPool jedisPool;
    
    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);
        
        jedisPool = new JedisPool(config, "localhost", 6379);
    }
    
    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}

三、Bitmap(位图)实战

3.1 基本概念
  • 使用字符串类型存储二进制位数组
  • 适合存储大量布尔值
  • 内存效率极高
3.2 实战应用:用户签到系统
java 复制代码
public class BitmapDemo {
    private Jedis jedis;
    
    public BitmapDemo() {
        this.jedis = RedisClient.getJedis();
    }
    
    /**
     * 用户签到
     * @param userId 用户ID
     * @param date 日期(格式:yyyyMMdd)
     */
    public void signIn(String userId, String date) {
        String key = "sign:" + userId + ":" + date.substring(0, 6);
        int day = Integer.parseInt(date.substring(6));
        
        jedis.setbit(key, day - 1, true);
        jedis.expire(key, 60 * 60 * 24 * 30); // 30天过期
    }
    
    /**
     * 检查某天是否签到
     */
    public boolean isSigned(String userId, String date) {
        String key = "sign:" + userId + ":" + date.substring(0, 6);
        int day = Integer.parseInt(date.substring(6));
        
        return jedis.getbit(key, day - 1);
    }
    
    /**
     * 获取当月签到总数
     */
    public long getMonthSignCount(String userId, String month) {
        String key = "sign:" + userId + ":" + month;
        return jedis.bitcount(key);
    }
    
    /**
     * 获取连续签到天数
     */
    public int getContinuousSignDays(String userId, String date) {
        String key = "sign:" + userId + ":" + date.substring(0, 6);
        int day = Integer.parseInt(date.substring(6));
        int continuousDays = 0;
        
        for (int i = day - 1; i >= 0; i--) {
            if (jedis.getbit(key, i)) {
                continuousDays++;
            } else {
                break;
            }
        }
        
        return continuousDays;
    }
    
    /**
     * 统计活跃用户(使用BITOP)
     */
    public long countActiveUsers(List<String> userKeys) {
        String destKey = "active_users:result";
        
        // 多个用户位图进行OR操作
        jedis.bitop(BitOP.OR, destKey, 
            userKeys.toArray(new String[0]));
        
        long count = jedis.bitcount(destKey);
        jedis.del(destKey);
        
        return count;
    }
}

四、HyperLogLog实战

4.1 基本概念
  • 用于基数统计(去重计数)
  • 误差率约0.81%
  • 固定使用12KB内存
4.2 实战应用:UV统计
java 复制代码
public class HyperLogLogDemo {
    private Jedis jedis;
    
    public HyperLogLogDemo() {
        this.jedis = RedisClient.getJedis();
    }
    
    /**
     * 记录页面访问
     */
    public void recordPageView(String pageId, String userId) {
        String key = "uv:" + pageId + ":" + getCurrentDate();
        jedis.pfadd(key, userId);
        jedis.expire(key, 60 * 60 * 24 * 7); // 保留7天
    }
    
    /**
     * 获取页面UV
     */
    public long getPageUV(String pageId, String date) {
        String key = "uv:" + pageId + ":" + date;
        return jedis.pfcount(key);
    }
    
    /**
     * 合并多日UV数据
     */
    public long getTotalUV(String pageId, List<String> dates) {
        String[] keys = dates.stream()
            .map(date -> "uv:" + pageId + ":" + date)
            .toArray(String[]::new);
        String destKey = "uv:" + pageId + ":total";
        
        jedis.pfmerge(destKey, keys);
        long count = jedis.pfcount(destKey);
        jedis.del(destKey);
        
        return count;
    }
    
    /**
     * 统计独立搜索词
     */
    public void recordSearchTerm(String term) {
        String key = "search_terms:" + getCurrentDate();
        jedis.pfadd(key, term);
    }
    
    private String getCurrentDate() {
        return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
    }
}

五、Geo(地理位置)实战

5.1 基本概念
  • 基于有序集合实现
  • 支持地理位置存储和查询
  • 使用WGS84坐标系
5.2 实战应用:附近的人
java 复制代码
public class GeoDemo {
    private Jedis jedis;
    
    public GeoDemo() {
        this.jedis = RedisClient.getJedis();
    }
    
    /**
     * 添加地理位置
     */
    public void addLocation(String userId, double longitude, double latitude) {
        String key = "user_locations";
        jedis.geoadd(key, longitude, latitude, userId);
    }
    
    /**
     * 获取用户位置
     */
    public List<GeoCoordinate> getUserLocation(String... userIds) {
        String key = "user_locations";
        return jedis.geopos(key, userIds);
    }
    
    /**
     * 查找附近的人
     * @param userId 中心用户
     * @param radius 半径(米)
     * @param count 返回数量
     */
    public List<GeoRadiusResponse> findNearbyUsers(String userId, 
                                                   double radius, 
                                                   int count) {
        String key = "user_locations";
        List<GeoCoordinate> coordinates = jedis.geopos(key, userId);
        
        if (coordinates == null || coordinates.isEmpty()) {
            return new ArrayList<>();
        }
        
        GeoCoordinate coord = coordinates.get(0);
        
        // 查找指定半径内的用户
        GeoRadiusParam param = GeoRadiusParam.geoRadiusParam()
            .withDist()          // 返回距离
            .withCoord()         // 返回坐标
            .sortAscending()     // 按距离升序
            .count(count);       // 限制返回数量
        
        return jedis.georadius(key, 
            coord.getLongitude(), 
            coord.getLatitude(), 
            radius, 
            GeoUnit.M, 
            param);
    }
    
    /**
     * 计算两个用户距离
     */
    public Double getDistance(String user1, String user2) {
        String key = "user_locations";
        return jedis.geodist(key, user1, user2, GeoUnit.M);
    }
    
    /**
     * 存储店铺位置信息
     */
    public void addStore(String storeId, String storeName, 
                        double longitude, double latitude) {
        String key = "store_locations";
        Map<String, String> storeInfo = new HashMap<>();
        storeInfo.put("name", storeName);
        storeInfo.put("id", storeId);
        
        // 使用GEO存储位置
        jedis.geoadd(key, longitude, latitude, storeId);
        
        // 使用Hash存储详细信息
        jedis.hset("store_info:" + storeId, storeInfo);
    }
}

六、Stream实战

6.1 基本概念
  • Redis 5.0引入的消息队列
  • 支持消费者组
  • 消息持久化
6.2 实战应用:消息队列系统
java 复制代码
public class StreamDemo {
    private Jedis jedis;
    
    public StreamDemo() {
        this.jedis = RedisClient.getJedis();
    }
    
    /**
     * 生产者:发送消息
     */
    public String produceMessage(String streamKey, 
                                Map<String, String> message) {
        return jedis.xadd(streamKey, 
            XAddParams.xAddParams(), 
            message);
    }
    
    /**
     * 消费者:读取消息
     */
    public List<Map.Entry<String, List<StreamEntry>>> consumeMessages(
            String streamKey, 
            String lastId) {
        
        // 从指定ID开始读取,0-0表示从头开始
        Map<String, StreamEntryID> streams = new HashMap<>();
        streams.put(streamKey, 
            lastId != null ? new StreamEntryID(lastId) : StreamEntryID.LAST_ENTRY);
        
        return jedis.xread(
            XReadParams.xReadParams().block(1000), // 阻塞1秒
            streams
        );
    }
    
    /**
     * 创建消费者组
     */
    public void createConsumerGroup(String streamKey, 
                                   String groupName) {
        try {
            jedis.xgroupCreate(streamKey, 
                groupName, 
                new StreamEntryID("0-0"), 
                true);
        } catch (Exception e) {
            // 组已存在
        }
    }
    
    /**
     * 消费者组消费
     */
    public List<StreamEntry> groupConsume(String streamKey, 
                                         String groupName, 
                                         String consumerName) {
        
        // 从消费者组读取消息
        List<Map.Entry<String, List<StreamEntry>>> result = 
            jedis.xreadGroup(groupName, 
                consumerName, 
                XReadGroupParams.xReadGroupParams()
                    .block(1000)
                    .count(10), 
                Collections.singletonMap(streamKey, StreamEntryID.UNRECEIVED_ENTRY));
        
        if (result != null && !result.isEmpty()) {
            return result.get(0).getValue();
        }
        
        return new ArrayList<>();
    }
    
    /**
     * 确认消息已处理
     */
    public void ackMessage(String streamKey, 
                          String groupName, 
                          String messageId) {
        jedis.xack(streamKey, groupName, new StreamEntryID(messageId));
    }
    
    /**
     * 查看消费者组信息
     */
    public List<StreamGroupInfo> getStreamGroups(String streamKey) {
        return jedis.xinfoGroups(streamKey);
    }
}

七、BitField实战

7.1 基本概念
  • 对位图进行复杂操作
  • 支持有符号/无符号整数
  • 可执行原子操作
7.2 实战应用:用户标签系统
java 复制代码
public class BitFieldDemo {
    private Jedis jedis;
    
    public BitFieldDemo() {
        this.jedis = RedisClient.getJedis();
    }
    
    /**
     * 设置用户标签(每个标签占1位)
     */
    public void setUserTag(String userId, int tagIndex, boolean value) {
        String key = "user_tags:" + userId;
        
        // 使用BitField设置特定位
        String command = "SET";
        String type = "u1";  // 无符号1位整数
        long offset = tagIndex;
        long bitValue = value ? 1 : 0;
        
        jedis.bitfield(key, 
            command, type, offset, bitValue);
    }
    
    /**
     * 获取用户标签
     */
    public boolean getUserTag(String userId, int tagIndex) {
        String key = "user_tags:" + userId;
        
        // 获取特定位的值
        List<Long> result = jedis.bitfield(key, 
            "GET", "u1", tagIndex);
        
        return result != null && !result.isEmpty() && result.get(0) == 1;
    }
    
    /**
     * 批量设置多个标签
     */
    public void batchSetTags(String userId, Map<Integer, Boolean> tags) {
        String key = "user_tags:" + userId;
        List<String> arguments = new ArrayList<>();
        
        for (Map.Entry<Integer, Boolean> entry : tags.entrySet()) {
            arguments.add("SET");
            arguments.add("u1");
            arguments.add(String.valueOf(entry.getKey()));
            arguments.add(entry.getValue() ? "1" : "0");
        }
        
        jedis.bitfield(key, arguments.toArray(new String[0]));
    }
    
    /**
     * 原子递增计数器(使用有符号整数)
     */
    public long incrementCounter(String key, int bitWidth, long increment) {
        // bitWidth: 8, 16, 32, 64
        String type = "i" + bitWidth;  // 有符号整数
        
        List<Long> result = jedis.bitfield(key, 
            "OVERFLOW", "SAT",  // 饱和溢出处理
            "INCRBY", type, "0", increment);
        
        return result != null && !result.isEmpty() ? result.get(0) : 0;
    }
    
    /**
     * 存储多个计数器在一个key中
     */
    public void setMultipleCounters() {
        String key = "app_counters";
        
        // 设置4个计数器,分别占用8, 16, 32, 64位
        jedis.bitfield(key,
            "SET", "u8", "0", "100",    // 计数器1:0-7位
            "SET", "u16", "8", "1000",  // 计数器2:8-23位
            "SET", "u32", "24", "50000", // 计数器3:24-55位
            "SET", "u64", "56", "1000000" // 计数器4:56-119位
        );
    }
}

八、综合实战案例

8.1 电商系统应用
java 复制代码
public class ECommerceSystem {
    private Jedis jedis;
    private GeoDemo geoDemo;
    private HyperLogLogDemo hllDemo;
    private BitmapDemo bitmapDemo;
    private StreamDemo streamDemo;
    
    public ECommerceSystem() {
        this.jedis = RedisClient.getJedis();
        this.geoDemo = new GeoDemo();
        this.hllDemo = new HyperLogLogDemo();
        this.bitmapDemo = new BitmapDemo();
        this.streamDemo = new StreamDemo();
    }
    
    /**
     * 用户行为追踪系统
     */
    public void trackUserBehavior(String userId, 
                                 String productId, 
                                 String behavior) {
        // 1. 使用HyperLogLog统计独立访客
        hllDemo.recordPageView(productId, userId);
        
        // 2. 使用Bitmap记录用户行为
        String today = LocalDate.now().format(
            DateTimeFormatter.ofPattern("yyyyMMdd"));
        bitmapDemo.signIn(userId + ":" + behavior, today);
        
        // 3. 使用Stream记录行为日志
        Map<String, String> message = new HashMap<>();
        message.put("userId", userId);
        message.put("productId", productId);
        message.put("behavior", behavior);
        message.put("timestamp", String.valueOf(System.currentTimeMillis()));
        
        streamDemo.produceMessage("user_behavior_stream", message);
    }
    
    /**
     * 附近店铺推荐
     */
    public List<GeoRadiusResponse> recommendNearbyStores(
            String userId, 
            double radiusKm) {
        
        // 获取用户位置
        List<GeoCoordinate> userLocation = 
            geoDemo.getUserLocation(userId);
        
        if (userLocation == null || userLocation.isEmpty()) {
            return new ArrayList<>();
        }
        
        GeoCoordinate coord = userLocation.get(0);
        
        // 查找附近店铺
        GeoRadiusParam param = GeoRadiusParam.geoRadiusParam()
            .withDist()
            .withCoord()
            .sortAscending()
            .count(10);
        
        return jedis.georadius("store_locations", 
            coord.getLongitude(), 
            coord.getLatitude(), 
            radiusKm * 1000, 
            GeoUnit.M, 
            param);
    }
    
    /**
     * 实时UV统计看板
     */
    public Map<String, Object> getRealTimeStats(String productId) {
        Map<String, Object> stats = new HashMap<>();
        
        // 今日UV
        String today = LocalDate.now().format(
            DateTimeFormatter.ofPattern("yyyyMMdd"));
        long todayUV = hllDemo.getPageUV(productId, today);
        
        // 7日UV
        List<String> last7Days = new ArrayList<>();
        for (int i = 6; i >= 0; i--) {
            last7Days.add(LocalDate.now().minusDays(i)
                .format(DateTimeFormatter.ofPattern("yyyyMMdd")));
        }
        long weeklyUV = hllDemo.getTotalUV(productId, last7Days);
        
        // 用户行为统计
        long todayViews = bitmapDemo.getMonthSignCount(
            productId + ":view", today.substring(0, 6));
        long todayPurchases = bitmapDemo.getMonthSignCount(
            productId + ":purchase", today.substring(0, 6));
        
        stats.put("todayUV", todayUV);
        stats.put("weeklyUV", weeklyUV);
        stats.put("todayViews", todayViews);
        stats.put("todayPurchases", todayPurchases);
        stats.put("conversionRate", 
            todayViews > 0 ? (double) todayPurchases / todayViews : 0);
        
        return stats;
    }
}

3. 各数据结构性能对比、使用禁忌与避坑指南

一、Redis数据结构性能对比

1. String(字符串)
  • 时间复杂度 :
  • 读写:O(1)
  • 批量操作:O(n)
  • 内存占用 :最低,适合存储简单值
  • 适用场景 :缓存、计数器、分布式锁
  • Java示例 :
java 复制代码
// Jedis
jedis.set("key", "value");
String value = jedis.get("key");

// Lettuce/Spring Data Redis
redisTemplate.opsForValue().set("key", "value");
redisTemplate.opsForValue().get("key");
2. Hash(哈希表)
  • 时间复杂度 :
  • 单个字段操作:O(1)
  • 获取所有:O(n)
  • 内存优化 :使用ziplist编码时更省内存
  • 适用场景 :对象存储、购物车
  • Java示例 :
java 复制代码
// 存储对象
Map<String, String> user = new HashMap<>();
user.put("name", "张三");
user.put("age", "25");
redisTemplate.opsForHash().putAll("user:1001", user);

// 获取部分字段
String name = (String) redisTemplate.opsForHash()
    .get("user:1001", "name");
3. List(列表)
  • 时间复杂度 :
  • 两端插入/删除:O(1)
  • 按索引访问:O(n)
  • 内存 :ziplist编码下内存友好
  • 适用场景 :消息队列、最新列表、时间线
  • Java示例 :
java 复制代码
// 作为队列使用
redisTemplate.opsForList().leftPush("queue", "task1");
String task = redisTemplate.opsForList().rightPop("queue");

// 获取范围
List<String> items = redisTemplate.opsForList()
    .range("list", 0, -1);
4. Set(集合)
  • 时间复杂度 :
  • 添加/删除/存在判断:O(1)
  • 集合运算:O(n)
  • 内存 :intset编码对小整数集合特别高效
  • 适用场景 :标签、共同好友、去重
  • Java示例 :
java 复制代码
// 添加元素
redisTemplate.opsForSet().add("tags", "java", "redis", "spring");

// 集合运算
Set<String> intersection = redisTemplate.opsForSet()
    .intersect("set1", "set2");
5. ZSet(有序集合)
  • 时间复杂度 :
  • 添加/删除:O(log N)
  • 范围查询:O(log N + M)
  • 内存 :相比Set多存储score
  • 适用场景 :排行榜、延迟队列、带权重的队列
  • Java示例 :
java 复制代码
// 添加带分数的元素
redisTemplate.opsForZSet().add("rank", "user1", 100);
redisTemplate.opsForZSet().add("rank", "user2", 200);

// 获取排行榜
Set<ZSetOperations.TypedTuple<String>> top3 = redisTemplate.opsForZSet()
    .reverseRangeWithScores("rank", 0, 2);
6. 性能对比总结

|--------|-------|-------|-------|---------|
| 结构 | 读性能 | 写性能 | 内存效率 | 适用场景 |
| String | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 简单KV |
| Hash | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 对象存储 |
| List | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 队列/栈 |
| Set | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 去重/集合运算 |
| ZSet | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | 排行榜 |

二、使用禁忌与避坑指南

1. 大Key问题
java 复制代码
// ❌ 错误示例:大Key导致阻塞
redisTemplate.opsForValue().set("bigKey", hugeString); // 超过10KB

// ✅ 正确做法:
// 1. 拆分大Key
Map<String, String> chunks = splitLargeData(hugeString);
chunks.forEach((k, v) -> redisTemplate.opsForValue().set(k, v));

// 2. 使用Hash存储
Map<String, String> fields = new HashMap<>();
// 分字段存储
redisTemplate.opsForHash().putAll("large:object", fields);
2. 热Key问题
java 复制代码
// ❌ 单Key热点
String hotValue = redisTemplate.opsForValue().get("hot:key");

// ✅ 解决方案:
// 1. 本地缓存 + Redis
@Cacheable(value = "users", key = "#userId", cacheManager = "redisCacheManager")
public User getUser(String userId) {
    // ...
}

// 2. Key分片
String shardKey = "hot:key:" + (userId.hashCode() % 10);
redisTemplate.opsForValue().get(shardKey);
3. 过期策略不当
java 复制代码
// ❌ 同时大量Key过期导致雪崩
for (int i = 0; i < 10000; i++) {
    redisTemplate.opsForValue().set("key:" + i, "value", 5, TimeUnit.MINUTES);
}

// ✅ 添加随机过期时间
Random random = new Random();
for (int i = 0; i < 10000; i++) {
    int expireSeconds = 300 + random.nextInt(60); // 300-360秒
    redisTemplate.opsForValue().set("key:" + i, "value", 
        expireSeconds, TimeUnit.SECONDS);
}
4. Pipeline使用不当
java 复制代码
// ❌ 循环单个操作
for (String key : keys) {
    redisTemplate.opsForValue().get(key); // 网络往返次数多
}

// ✅ 使用Pipeline批量操作
List<Object> results = redisTemplate.executePipelined(
    new RedisCallback<Object>() {
        @Override
        public Object doInRedis(RedisConnection connection) {
            for (String key : keys) {
                connection.get(key.getBytes());
            }
            return null;
        }
    }
);
5. 事务使用误区
java 复制代码
// ❌ 事务中执行耗时操作
redisTemplate.execute(new SessionCallback<Object>() {
    @Override
    public Object execute(RedisOperations operations) {
        operations.multi();
        // 耗时操作会阻塞其他客户端
        operations.opsForValue().set("key1", heavyCompute());
        operations.opsForValue().set("key2", heavyCompute());
        return operations.exec();
    }
});

// ✅ 事务应快速执行,或使用Lua脚本
String luaScript = 
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    "    return redis.call('del', KEYS[1]) " +
    "else " +
    "    return 0 " +
    "end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
redisTemplate.execute(script, Arrays.asList("lockKey"), lockValue);
6. 内存优化配置
java 复制代码
@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // 使用String序列化器,减少内存占用
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        
        // 使用Jackson序列化Value
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = 
            new Jackson2JsonRedisSerializer<>(Object.class);
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);
        
        return template;
    }
    
    // 配置连接池优化
    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory() {
        LettuceClientConfiguration config = LettuceClientConfiguration.builder()
            .commandTimeout(Duration.ofSeconds(2))
            .shutdownTimeout(Duration.ZERO)
            .build();
        
        RedisStandaloneConfiguration standaloneConfig = 
            new RedisStandaloneConfiguration("localhost", 6379);
        
        return new LettuceConnectionFactory(standaloneConfig, config);
    }
}
7. 监控与告警配置
java 复制代码
// 使用Micrometer监控Redis指标
@Configuration
public class RedisMetricsConfig {
    
    @Bean
    public MeterRegistryCustomizer<MeterRegistry> redisMetrics() {
        return registry -> {
            registry.config().commonTags("application", "myapp");
        };
    }
}

// 自定义健康检查
@Component
public class RedisHealthIndicator implements HealthIndicator {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Override
    public Health health() {
        try {
            String result = redisTemplate.execute(
                (RedisCallback<String>) connection -> 
                    connection.ping()
            );
            return "PONG".equals(result) ? 
                Health.up().build() : 
                Health.down().build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

三、最佳实践总结

1. 编码规范
java 复制代码
// 1. Key命名规范
public class RedisKeyBuilder {
    private static final String DELIMITER = ":";
    
    public static String userKey(Long userId) {
        return "user" + DELIMITER + userId;
    }
    
    public static String sessionKey(String token) {
        return "session" + DELIMITER + token;
    }
}

// 2. 统一序列化配置
public abstract class BaseRedisService {
    protected String serializeKey(Object key) {
        return key.toString();
    }
    
    protected byte[] serializeValue(Object value) {
        // 使用高效序列化
        return ProtostuffUtil.serialize(value);
    }
}
2. 连接池配置优化
Crystal 复制代码
# application.yml
spring:
  redis:
    lettuce:
      pool:
        max-active: 50      # 最大连接数
        max-idle: 10       # 最大空闲连接
        min-idle: 5        # 最小空闲连接
        max-wait: 1000ms   # 获取连接最大等待时间
    timeout: 2000ms        # 命令超时时间
3. 性能优化建议
  1. 数据分片 :单个实例存储不超过25GB

  2. 连接复用 :使用连接池,避免频繁创建连接

  3. 读写分离 :读多写少场景使用主从架构

  4. 内存淘汰策略 :根据业务选择合适策略

    redis.conf

    maxmemory-policy allkeys-lru # 推荐使用

4. 故障处理
java 复制代码
@Component
public class RedisCircuitBreaker {
    
    private final AtomicInteger failureCount = new AtomicInteger(0);
    private volatile boolean circuitOpen = false;
    
    @Retryable(value = RedisCommandTimeoutException.class, 
               maxAttempts = 3,
               backoff = @Backoff(delay = 100))
    public <T> T executeWithRetry(Supplier<T> operation) {
        if (circuitOpen) {
            throw new CircuitBreakerOpenException();
        }
        
        try {
            T result = operation.get();
            failureCount.set(0);
            return result;
        } catch (Exception e) {
            if (failureCount.incrementAndGet() > 5) {
                circuitOpen = true;
                // 30秒后尝试恢复
                scheduler.schedule(() -> circuitOpen = false, 30, TimeUnit.SECONDS);
            }
            throw e;
        }
    }
}

四、工具推荐

  1. 监控工具 :
  1. RedisInsight(官方GUI)
  2. Prometheus + Grafana
  3. Spring Boot Actuator
  1. 压测工具 :
  1. redis-benchmark
  2. JMeter Redis插件
  1. 分析工具 :
  1. Redis RDB分析工具:rdr
  2. 内存分析:redis-memory-analyzer

记住:没有最好的数据结构,只有最适合场景的数据结构 。根据业务需求选择合适的数据结构,结合监控数据持续优化。

4. 实战演练:基于各数据结构实现签到、排行榜、限流、地理位置统计等通用功能

环境准备

Maven依赖

XML 复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.1</version>
</dependency>

Redis工具类

java 复制代码
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.GeoRadiusParam;
import redis.clients.jedis.resps.GeoRadiusResponse;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;

public class RedisUtil {
    private static JedisPool jedisPool;
    
    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(100);
        config.setMaxIdle(50);
        config.setMinIdle(10);
        config.setMaxWaitMillis(3000);
        
        jedisPool = new JedisPool(config, "localhost", 6379);
    }
    
    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}

签到功能实现

2.1 基于BitMap的签到系统

java 复制代码
public class SignInService {
    
    /**
     * 用户签到
     * @param userId 用户ID
     * @return 是否签到成功
     */
    public boolean signIn(Long userId) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            LocalDate today = LocalDate.now();
            String key = buildSignKey(userId, today);
            int offset = today.getDayOfMonth() - 1; // 位偏移量(0-based)
            
            return jedis.setbit(key, offset, true);
        }
    }
    
    /**
     * 检查某天是否签到
     */
    public boolean checkSignIn(Long userId, LocalDate date) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            String key = buildSignKey(userId, date);
            int offset = date.getDayOfMonth() - 1;
            return jedis.getbit(key, offset);
        }
    }
    
    /**
     * 获取本月签到天数
     */
    public int getSignCount(Long userId, LocalDate date) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            String key = buildSignKey(userId, date);
            byte[] bytes = jedis.get(key.getBytes());
            if (bytes == null) return 0;
            
            int count = 0;
            for (byte b : bytes) {
                count += Integer.bitCount(b & 0xFF);
            }
            return count;
        }
    }
    
    /**
     * 获取连续签到天数
     */
    public int getContinuousSignDays(Long userId) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            LocalDate today = LocalDate.now();
            int days = 0;
            
            for (int i = 0; i < 30; i++) { // 最多检查30天
                LocalDate date = today.minusDays(i);
                String key = buildSignKey(userId, date);
                int offset = date.getDayOfMonth() - 1;
                
                if (jedis.getbit(key, offset)) {
                    days++;
                } else {
                    break;
                }
            }
            return days;
        }
    }
    
    /**
     * 获取本月签到详情
     */
    public Map<String, Boolean> getSignDetail(Long userId, LocalDate date) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            String key = buildSignKey(userId, date);
            Map<String, Boolean> result = new LinkedHashMap<>();
            
            int daysInMonth = date.lengthOfMonth();
            for (int i = 0; i < daysInMonth; i++) {
                LocalDate day = date.withDayOfMonth(i + 1);
                boolean signed = jedis.getbit(key, i);
                result.put(day.toString(), signed);
            }
            return result;
        }
    }
    
    private String buildSignKey(Long userId, LocalDate date) {
        String month = date.format(DateTimeFormatter.ofPattern("yyyyMM"));
        return String.format("sign:%s:%s", userId, month);
    }
}

排行榜功能实现

3.1 基于SortedSet的排行榜

java 复制代码
public class RankingService {
    
    /**
     * 添加/更新用户分数
     */
    public void updateScore(String rankingKey, String userId, double score) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            jedis.zadd(rankingKey, score, userId);
        }
    }
    
    /**
     * 获取用户排名(从高到低)
     */
    public Long getUserRank(String rankingKey, String userId) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            // zrevrank返回的是0-based排名
            Long rank = jedis.zrevrank(rankingKey, userId);
            return rank != null ? rank + 1 : null;
        }
    }
    
    /**
     * 获取用户分数
     */
    public Double getUserScore(String rankingKey, String userId) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            return jedis.zscore(rankingKey, userId);
        }
    }
    
    /**
     * 获取Top N排行榜
     */
    public List<Map<String, Object>> getTopN(String rankingKey, int n) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            Set<String> topUsers = jedis.zrevrange(rankingKey, 0, n - 1);
            List<Map<String, Object>> result = new ArrayList<>();
            
            for (String userId : topUsers) {
                Map<String, Object> userInfo = new HashMap<>();
                userInfo.put("userId", userId);
                userInfo.put("score", jedis.zscore(rankingKey, userId));
                userInfo.put("rank", jedis.zrevrank(rankingKey, userId) + 1);
                result.add(userInfo);
            }
            return result;
        }
    }
    
    /**
     * 获取分数区间内的用户
     */
    public Set<String> getUsersByScoreRange(String rankingKey, double min, double max) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            return jedis.zrangeByScore(rankingKey, min, max);
        }
    }
    
    /**
     * 增加用户分数
     */
    public double incrementScore(String rankingKey, String userId, double increment) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            return jedis.zincrby(rankingKey, increment, userId);
        }
    }
    
    /**
     * 周榜/月榜实现
     */
    public void updateWeeklyRanking(String userId, double score) {
        LocalDate now = LocalDate.now();
        String weekKey = String.format("ranking:week:%d-%d", 
            now.getYear(), now.get(WeekFields.ISO.weekOfYear()));
        
        try (Jedis jedis = RedisUtil.getJedis()) {
            // 更新周榜
            jedis.zincrby(weekKey, score, userId);
            
            // 设置过期时间(7天)
            jedis.expire(weekKey, 7 * 24 * 3600);
        }
    }
}

限流功能实现

4.1 滑动窗口限流

java 复制代码
public class RateLimiterService {
    
    /**
     * 滑动窗口限流
     * @param key 限流key
     * @param windowSize 窗口大小(秒)
     * @param maxRequests 最大请求数
     * @return 是否允许请求
     */
    public boolean slidingWindowLimit(String key, int windowSize, int maxRequests) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            long currentTime = System.currentTimeMillis();
            long windowStart = currentTime - windowSize * 1000L;
            
            // 使用SortedSet存储请求时间戳
            String zsetKey = "rate_limit:" + key;
            
            // 移除窗口外的数据
            jedis.zremrangeByScore(zsetKey, 0, windowStart);
            
            // 获取当前窗口内的请求数
            long count = jedis.zcard(zsetKey);
            
            if (count < maxRequests) {
                // 添加当前请求
                jedis.zadd(zsetKey, currentTime, String.valueOf(currentTime));
                // 设置过期时间
                jedis.expire(zsetKey, windowSize + 1);
                return true;
            }
            return false;
        }
    }
    
    /**
     * 令牌桶限流
     */
    public boolean tokenBucketLimit(String key, int capacity, int refillRate) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            String tokenKey = "token_bucket:" + key;
            String timestampKey = "token_timestamp:" + key;
            
            long now = System.currentTimeMillis() / 1000;
            
            // 获取当前令牌数
            String tokenStr = jedis.get(tokenKey);
            int tokens = tokenStr != null ? Integer.parseInt(tokenStr) : capacity;
            
            // 获取上次更新时间
            String timestampStr = jedis.get(timestampKey);
            long lastRefill = timestampStr != null ? Long.parseLong(timestampStr) : now;
            
            // 计算需要补充的令牌
            long timePassed = now - lastRefill;
            int refillTokens = (int) (timePassed * refillRate);
            
            if (refillTokens > 0) {
                tokens = Math.min(capacity, tokens + refillTokens);
                jedis.set(timestampKey, String.valueOf(now));
            }
            
            if (tokens > 0) {
                tokens--;
                jedis.set(tokenKey, String.valueOf(tokens));
                jedis.expire(tokenKey, 3600);
                jedis.expire(timestampKey, 3600);
                return true;
            }
            return false;
        }
    }
    
    /**
     * 固定窗口限流
     */
    public boolean fixedWindowLimit(String key, int windowSize, int maxRequests) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            long currentWindow = System.currentTimeMillis() / 1000 / windowSize;
            String counterKey = "fixed_window:" + key + ":" + currentWindow;
            
            // 使用incr原子操作
            long count = jedis.incr(counterKey);
            
            if (count == 1) {
                // 第一次设置过期时间
                jedis.expire(counterKey, windowSize);
            }
            
            return count <= maxRequests;
        }
    }
}

地理位置功能实现

5.1 基于GEO的地理位置服务

java 复制代码
public class GeoLocationService {
    
    /**
     * 添加地理位置
     */
    public void addLocation(String key, String member, double longitude, double latitude) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            jedis.geoadd(key, longitude, latitude, member);
        }
    }
    
    /**
     * 获取两点距离
     */
    public Double getDistance(String key, String member1, String member2) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            return jedis.geodist(key, member1, member2);
        }
    }
    
    /**
     * 获取位置坐标
     */
    public List<Double> getPosition(String key, String member) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            return jedis.geopos(key, member);
        }
    }
    
    /**
     * 查找附近的人/地点
     */
    public List<GeoRadiusResponse> findNearby(String key, double longitude, double latitude, 
                                              double radius, String unit) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            GeoRadiusParam param = GeoRadiusParam.geoRadiusParam()
                    .withDist()  // 返回距离
                    .withCoord() // 返回坐标
                    .sortAscending(); // 按距离升序
            
            return jedis.georadius(key, longitude, latitude, radius, 
                    GeoUnit.fromString(unit), param);
        }
    }
    
    /**
     * 查找指定成员附近的地点
     */
    public List<GeoRadiusResponse> findNearbyByMember(String key, String member, 
                                                      double radius, String unit) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            GeoRadiusParam param = GeoRadiusParam.geoRadiusParam()
                    .withDist()
                    .withCoord()
                    .sortAscending();
            
            return jedis.georadiusByMember(key, member, radius, 
                    GeoUnit.fromString(unit), param);
        }
    }
    
    /**
     * 附近商家搜索示例
     */
    public List<Map<String, Object>> searchNearbyStores(double userLng, double userLat, 
                                                        double radiusKm, int limit) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            String geoKey = "geo:stores";
            
            GeoRadiusParam param = GeoRadiusParam.geoRadiusParam()
                    .withDist()
                    .withCoord()
                    .sortAscending()
                    .count(limit);
            
            List<GeoRadiusResponse> results = jedis.georadius(geoKey, userLng, userLat, 
                    radiusKm, GeoUnit.KM, param);
            
            List<Map<String, Object>> stores = new ArrayList<>();
            for (GeoRadiusResponse response : results) {
                Map<String, Object> store = new HashMap<>();
                store.put("storeId", response.getMemberByString());
                store.put("distance", response.getDistance());
                store.put("longitude", response.getCoordinate().getLongitude());
                store.put("latitude", response.getCoordinate().getLatitude());
                stores.add(store);
            }
            return stores;
        }
    }
}

综合示例:用户活跃度统计

java 复制代码
public class UserActivityService {
    
    /**
     * 记录用户活跃行为
     */
    public void recordActivity(Long userId, String action, double score) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            LocalDate today = LocalDate.now();
            
            // 1. 记录到BitMap(签到)
            String signKey = String.format("sign:%s:%s", 
                userId, today.format(DateTimeFormatter.ofPattern("yyyyMM")));
            jedis.setbit(signKey, today.getDayOfMonth() - 1, true);
            
            // 2. 更新日活跃度排行榜
            String dailyRankKey = String.format("ranking:daily:%s", 
                today.format(DateTimeFormatter.ofPattern("yyyyMMdd")));
            jedis.zincrby(dailyRankKey, score, String.valueOf(userId));
            jedis.expire(dailyRankKey, 48 * 3600); // 保留2天
            
            // 3. 更新周活跃度排行榜
            String weekRankKey = String.format("ranking:week:%d-%d", 
                today.getYear(), today.get(WeekFields.ISO.weekOfYear()));
            jedis.zincrby(weekRankKey, score, String.valueOf(userId));
            
            // 4. 记录行为日志(List)
            String logKey = String.format("activity:log:%s", userId);
            String log = String.format("%s|%s|%s|%.2f", 
                LocalDateTime.now(), action, today, score);
            jedis.lpush(logKey, log);
            jedis.ltrim(logKey, 0, 99); // 只保留最近100条
            
            // 5. 更新用户最后活跃时间(Hash)
            String userKey = String.format("user:info:%s", userId);
            jedis.hset(userKey, "last_active", LocalDateTime.now().toString());
            jedis.hincrBy(userKey, "activity_count", 1);
        }
    }
    
    /**
     * 获取用户活跃度统计
     */
    public Map<String, Object> getUserActivityStats(Long userId) {
        try (Jedis jedis = RedisUtil.getJedis()) {
            Map<String, Object> stats = new HashMap<>();
            LocalDate today = LocalDate.now();
            
            // 连续签到天数
            SignInService signService = new SignInService();
            stats.put("continuous_sign_days", signService.getContinuousSignDays(userId));
            
            // 本月活跃天数
            String signKey = String.format("sign:%s:%s", 
                userId, today.format(DateTimeFormatter.ofPattern("yyyyMM")));
            stats.put("month_active_days", jedis.bitcount(signKey));
            
            // 本周排名
            String weekRankKey = String.format("ranking:week:%d-%d", 
                today.getYear(), today.get(WeekFields.ISO.weekOfYear()));
            Long weekRank = jedis.zrevrank(weekRankKey, String.valueOf(userId));
            stats.put("week_rank", weekRank != null ? weekRank + 1 : "未上榜");
            
            // 总活跃次数
            String userKey = String.format("user:info:%s", userId);
            String totalCount = jedis.hget(userKey, "activity_count");
            stats.put("total_activity", totalCount != null ? totalCount : 0);
            
            return stats;
        }
    }
}
相关推荐
Rick19932 小时前
Redis 分布式锁 + 部署模式
redis·分布式
程序员老邢10 小时前
《技术底稿 43》今日踩坑复盘:Redis 乱码 + MySQL 配置注入失败
redis·技术底稿·redisson 序列化·mysql 配置·项目踩坑·微服务问题排查
phltxy12 小时前
RabbitMQ集群搭——多机多节点与单机多节点
分布式·rabbitmq·ruby
Mr. zhihao13 小时前
Redis五大高级数据结构:原理-场景-底层-横向对比
数据结构·redis
YL2004042617 小时前
【Redis实战篇】秒杀实现方案(以优惠券秒杀为例)
数据库·redis
三十..17 小时前
Ceph分布式存储核心技术精要与运维实践指南
运维·分布式·ceph
better_liang18 小时前
每日Java面试场景题知识点之-如何设计分布式锁
java·redis·zookeeper·面试·分布式锁
自传.19 小时前
Redis 高频考试面试知识点1
redis·aof·rdb·redis面试
小新同学^O^20 小时前
Redis的简单总结
数据库·redis·学习