本章前言
Redis 之所以能成为分布式架构中首选的缓存、中间件,高性能、低延迟的核心根基,正是其精心设计的底层数据结构。Redis 对外提供了十余种数据类型,不同类型对应不同的底层实现、性能特征与适用场景。
在分布式缓存落地过程中,80% 的线上问题都源于数据结构选型错误:比如用字符串存储结构化对象导致维护困难、用 List 实现海量排行榜引发性能卡顿、盲目使用集合运算造成 CPU 飙升等。
本章将分为基础五大核心类型 、高级特色类型两大板块,逐一讲解每种数据结构的原理、高频命令、核心特性,并结合互联网真实业务场景落地,搭配可直接复用的代码示例、设计思路与踩坑要点。最后附上全类型选型对照表与线上避坑指南,帮你做到 "根据业务选结构,而不是只会用字符串"。
学习目标:
- 掌握 Redis 所有实战高频数据结构的特性与常用命令;
- 理解每种结构底层编码对性能、内存的影响;
- 能够根据业务需求快速匹配合适的数据结构;
- 掌握主流业务(计数器、排行榜、签到、UV、消息队列等)的 Redis 落地方案。
1. 5大基础数据结构底层原理 + 真实业务落地场景(String/List/Hash/Set/ZSet)
Redis 数据结构整体概述
Redis 对外暴露的数据类型 ,和其底层真正的存储编码是两个概念,这是实战优化的基础:
- 对外数据类型 :开发者通过命令操作的类型,分为基础 5 大类型 (String、List、Hash、Set、ZSet)和高级特色类型(Bitmap、HyperLogLog、Geo、Stream 等);
- 底层编码 :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 代表查询所有元素
底层原理
两种实现方式:
- 压缩列表(ziplist) :元素数量<512且元素大小<64字节时使用
- 双向链表(linkedlist) :不满足上述条件时使用
- 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
底层原理
两种实现方式:
- ziplist :field数量≤512且value大小≤64字节
- 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 # 差集
底层原理
两种实现方式:
- intset :元素都是整数且元素数量≤512
- 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 元素 # 删除元素
底层原理
两种实现方式:
- ziplist :元素数量≤128且元素大小≤64字节
- skiplist + hashtable :不满足条件时使用
- 跳表:支持范围查询
- 哈希表:支持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;
}
}
七、最佳实践建议
- 内存优化 :
- 合理设置过期时间
- 使用合适的数据结构
- 避免大Key(单个Key不要超过10KB)
- 性能优化 :
- 使用Pipeline批量操作
- 合理使用连接池
- 避免频繁的序列化/反序列化
- 数据结构选择指南 :
- 需要过期时间 → String
- 需要排序 → ZSet
- 需要去重 → Set
- 存储对象 → Hash
- 队列/栈 → List
- 监控指标 :
- 内存使用率
- 命令延迟
- 命中率
- 连接数
通过合理选择和使用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. 性能优化建议
-
数据分片 :单个实例存储不超过25GB
-
连接复用 :使用连接池,避免频繁创建连接
-
读写分离 :读多写少场景使用主从架构
-
内存淘汰策略 :根据业务选择合适策略
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;
}
}
}
四、工具推荐
- 监控工具 :
- RedisInsight(官方GUI)
- Prometheus + Grafana
- Spring Boot Actuator
- 压测工具 :
- redis-benchmark
- JMeter Redis插件
- 分析工具 :
- Redis RDB分析工具:rdr
- 内存分析: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;
}
}
}