难度:⭐⭐⭐ | 适合人群:想深入理解Redis的开发者
💥 开场:一次技术选型的争论
时间: 周三下午技术评审会
地点: 会议室
事件: 新项目技术选型
产品经理: "我们要做一个电商系统,需要商品列表、购物车、排行榜..."
我: "这些都存MySQL吧。"
哈吉米: "等等,用MySQL做缓存?性能能行吗?"
我: "那用什么?" 🤔
南北绿豆: "Redis啊!专业的缓存中间件。"
我: "Redis不就是个Key-Value数据库吗?能做这么多功能?"
阿西噶阿西: "Redis可不是简单的Key-Value!它支持5种基本数据类型,每种都有不同的使用场景。"
我: "5种?都是什么?" 😯
哈吉米: "来,我给你讲讲..."
🎯 第一问:为什么Redis这么快?
四大原因
南北绿豆: "Redis快的原因有四个。"
1. 内存存储
MySQL(磁盘)
读取速度:毫秒级(ms)
Redis(内存)
读取速度:微秒级(μs)
速度差距:1000倍!
2. 单线程模型
多线程数据库:
线程1、线程2、线程3...
├─ 需要加锁
├─ 上下文切换
└─ 竞争资源
Redis单线程:
一个线程处理所有请求
├─ 不需要加锁
├─ 不需要切换
└─ 顺序执行
注意: Redis 6.0引入了多线程,但只用于网络IO,核心数据处理还是单线程
3. 高效的数据结构
不是简单的哈希表!
每种数据类型都有多种底层实现:
- String → SDS(简单动态字符串)
- List → quicklist(快速列表)
- Hash → hashtable/ziplist
- Set → hashtable/intset
- ZSet → skiplist(跳表)
根据数据量自动选择最优实现
4. IO多路复用
传统IO:
一个请求 = 一个线程
10000个请求 = 10000个线程(爆炸)
IO多路复用(epoll):
一个线程处理10000个请求
监听所有连接,有数据就处理
📦 第二问:String字符串
基本使用
# 设置值
SET name "张三"
# 获取值
GET name
# 输出:"张三"
# 设置过期时间(秒)
SET session:123 "user-data" EX 3600
# 不存在才设置(分布式锁)
SET lock:order:123 "locked" NX EX 10
# 自增
SET counter 0
INCR counter
# 输出:1
INCR counter
# 输出:2
# 自减
DECR counter
# 输出:1
使用场景
场景1:缓存对象
@Service
public class UserService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public User getUser(Long userId) {
String key = "user:" + userId;
// 1. 从Redis获取
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
return JSON.parseObject(json, User.class);
}
// 2. 从数据库获取
User user = userDao.findById(userId);
// 3. 存入Redis(过期时间1小时)
redisTemplate.opsForValue().set(key, JSON.toJSONString(user),
1, TimeUnit.HOURS);
return user;
}
}
场景2:计数器
@Service
public class ArticleService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 文章点赞
*/
public void like(Long articleId) {
String key = "article:like:" + articleId;
redisTemplate.opsForValue().increment(key); // 自增
}
/**
* 获取点赞数
*/
public Long getLikeCount(Long articleId) {
String key = "article:like:" + articleId;
String count = redisTemplate.opsForValue().get(key);
return count == null ? 0 : Long.parseLong(count);
}
}
场景3:分布式锁(简易版)
public boolean tryLock(String lockKey, String lockValue, long expireTime) {
// SET key value NX EX seconds
Boolean result = redisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
expireTime,
TimeUnit.SECONDS
);
return Boolean.TRUE.equals(result);
}
// 使用
if (tryLock("lock:order:123", UUID.randomUUID().toString(), 10)) {
try {
// 执行业务逻辑
} finally {
// 释放锁
redisTemplate.delete("lock:order:123");
}
}
📋 第三问:List列表
基本使用
# 左侧插入
LPUSH list1 "a" "b" "c"
# 列表:["c", "b", "a"]
# 右侧插入
RPUSH list1 "d" "e"
# 列表:["c", "b", "a", "d", "e"]
# 左侧弹出
LPOP list1
# 输出:"c"
# 列表:["b", "a", "d", "e"]
# 右侧弹出
RPOP list1
# 输出:"e"
# 列表:["b", "a", "d"]
# 获取范围
LRANGE list1 0 -1
# 输出:["b", "a", "d"]
# 获取长度
LLEN list1
# 输出:3
使用场景
场景1:消息队列(简易版)
/**
* 生产者
*/
public void sendMessage(String message) {
redisTemplate.opsForList().rightPush("queue:message", message);
System.out.println("发送消息:" + message);
}
/**
* 消费者
*/
public void consumeMessage() {
while (true) {
// 阻塞式左侧弹出(没有消息会等待)
String message = redisTemplate.opsForList().leftPop(
"queue:message",
5,
TimeUnit.SECONDS
);
if (message != null) {
System.out.println("消费消息:" + message);
// 处理消息
}
}
}
场景2:文章列表(时间线)
@Service
public class ArticleService {
/**
* 发布文章
*/
public void publishArticle(Long userId, Article article) {
// 保存到数据库
articleDao.save(article);
// 添加到时间线(最新的在前面)
String key = "timeline:user:" + userId;
redisTemplate.opsForList().leftPush(key, article.getId().toString());
// 只保留最新的100篇
redisTemplate.opsForList().trim(key, 0, 99);
}
/**
* 获取时间线
*/
public List<Article> getTimeline(Long userId, int page, int size) {
String key = "timeline:user:" + userId;
// 分页获取文章ID
int start = page * size;
int end = start + size - 1;
List<String> articleIds = redisTemplate.opsForList().range(key, start, end);
// 根据ID批量查询文章
return articleDao.findByIds(articleIds);
}
}
🗂️ 第四问:Hash哈希
基本使用
# 设置字段
HSET user:1 name "张三"
HSET user:1 age "25"
HSET user:1 email "zhangsan@example.com"
# 批量设置
HMSET user:1 name "张三" age "25" email "zhangsan@example.com"
# 获取字段
HGET user:1 name
# 输出:"张三"
# 获取所有字段
HGETALL user:1
# 输出:
# 1) "name"
# 2) "张三"
# 3) "age"
# 4) "25"
# 5) "email"
# 6) "zhangsan@example.com"
# 删除字段
HDEL user:1 email
# 字段自增
HINCRBY user:1 age 1
使用场景
场景1:对象存储
阿西噶阿西: "存储对象时,Hash比String更节省内存!"
对比:
// 方式1:String存储(JSON)
String key = "user:1";
String json = "{\"name\":\"张三\",\"age\":25,\"email\":\"...\"}";
redisTemplate.opsForValue().set(key, json);
// 缺点:修改一个字段要重新序列化整个对象
// 方式2:Hash存储(推荐)
String key = "user:1";
Map<String, String> user = new HashMap<>();
user.put("name", "张三");
user.put("age", "25");
user.put("email", "zhangsan@example.com");
redisTemplate.opsForHash().putAll(key, user);
// 优点:可以只修改一个字段
修改字段:
// String方式:要重新设置整个JSON
String json = redisTemplate.opsForValue().get("user:1");
User user = JSON.parseObject(json, User.class);
user.setAge(26);
redisTemplate.opsForValue().set("user:1", JSON.toJSONString(user));
// Hash方式:直接修改字段
redisTemplate.opsForHash().put("user:1", "age", "26");
场景2:购物车
@Service
public class CartService {
/**
* 添加商品到购物车
*/
public void addToCart(Long userId, Long productId, Integer quantity) {
String key = "cart:" + userId;
// Hash的key是商品ID,value是数量
redisTemplate.opsForHash().put(key,
productId.toString(),
quantity.toString());
}
/**
* 修改商品数量
*/
public void updateQuantity(Long userId, Long productId, Integer quantity) {
String key = "cart:" + userId;
redisTemplate.opsForHash().put(key, productId.toString(), quantity.toString());
}
/**
* 删除商品
*/
public void removeFromCart(Long userId, Long productId) {
String key = "cart:" + userId;
redisTemplate.opsForHash().delete(key, productId.toString());
}
/**
* 获取购物车
*/
public Map<Long, Integer> getCart(Long userId) {
String key = "cart:" + userId;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
Map<Long, Integer> cart = new HashMap<>();
for (Map.Entry<Object, Object> entry : entries.entrySet()) {
cart.put(Long.parseLong(entry.getKey().toString()),
Integer.parseInt(entry.getValue().toString()));
}
return cart;
}
/**
* 获取购物车商品数量
*/
public Integer getCartSize(Long userId) {
String key = "cart:" + userId;
return redisTemplate.opsForHash().size(key).intValue();
}
}
🎲 第五问:Set集合
基本使用
# 添加元素
SADD tags:article:1 "Java" "Redis" "Spring"
# 查看所有元素
SMEMBERS tags:article:1
# 输出:["Java", "Redis", "Spring"]
# 判断元素是否存在
SISMEMBER tags:article:1 "Java"
# 输出:1(存在)
# 删除元素
SREM tags:article:1 "Redis"
# 获取元素数量
SCARD tags:article:1
# 输出:2
# 集合运算
SADD set1 "a" "b" "c"
SADD set2 "b" "c" "d"
# 交集
SINTER set1 set2
# 输出:["b", "c"]
# 并集
SUNION set1 set2
# 输出:["a", "b", "c", "d"]
# 差集
SDIFF set1 set2
# 输出:["a"]
使用场景
场景1:标签系统
@Service
public class TagService {
/**
* 给文章添加标签
*/
public void addTags(Long articleId, String... tags) {
String key = "article:tags:" + articleId;
redisTemplate.opsForSet().add(key, tags);
}
/**
* 获取文章的所有标签
*/
public Set<String> getTags(Long articleId) {
String key = "article:tags:" + articleId;
return redisTemplate.opsForSet().members(key);
}
/**
* 查找相似文章(共同标签最多的)
*/
public List<Long> findSimilarArticles(Long articleId, int limit) {
String key = "article:tags:" + articleId;
// 获取当前文章的标签
Set<String> tags = redisTemplate.opsForSet().members(key);
// 遍历其他文章,计算共同标签数
// 返回共同标签最多的文章
// ...
}
}
场景2:共同关注
@Service
public class FollowService {
/**
* 关注用户
*/
public void follow(Long userId, Long followUserId) {
String key = "user:follow:" + userId;
redisTemplate.opsForSet().add(key, followUserId.toString());
}
/**
* 获取共同关注
*/
public Set<String> getCommonFollows(Long userId1, Long userId2) {
String key1 = "user:follow:" + userId1;
String key2 = "user:follow:" + userId2;
// 求交集
return redisTemplate.opsForSet().intersect(key1, key2);
}
/**
* 可能认识的人(朋友的朋友)
*/
public Set<String> getMayKnow(Long userId) {
String key = "user:follow:" + userId;
// 获取我关注的人
Set<String> myFollows = redisTemplate.opsForSet().members(key);
Set<String> mayKnow = new HashSet<>();
// 遍历我关注的人,获取他们关注的人
for (String followId : myFollows) {
String followKey = "user:follow:" + followId;
Set<String> theirFollows = redisTemplate.opsForSet().members(followKey);
// 求差集(他们关注的 - 我关注的 - 我自己)
theirFollows.removeAll(myFollows);
theirFollows.remove(userId.toString());
mayKnow.addAll(theirFollows);
}
return mayKnow;
}
}
🏆 第六问:ZSet有序集合
基本使用
# 添加元素(score member)
ZADD rank 100 "张三"
ZADD rank 95 "李四"
ZADD rank 98 "王五"
# 获取排名(从高到低)
ZREVRANGE rank 0 -1 WITHSCORES
# 输出:
# 1) "张三"
# 2) "100"
# 3) "王五"
# 4) "98"
# 5) "李四"
# 6) "95"
# 获取排名(从低到高)
ZRANGE rank 0 -1 WITHSCORES
# 获取指定元素的分数
ZSCORE rank "张三"
# 输出:"100"
# 获取指定元素的排名
ZREVRANK rank "张三"
# 输出:0(第一名,从0开始)
# 增加分数
ZINCRBY rank 5 "李四"
# 李四的分数变成100
# 获取分数范围内的元素
ZRANGEBYSCORE rank 95 100
# 删除元素
ZREM rank "李四"
使用场景
场景1:排行榜
@Service
public class RankService {
/**
* 更新用户积分
*/
public void updateScore(Long userId, Integer score) {
String key = "rank:score";
redisTemplate.opsForZSet().add(key, userId.toString(), score);
}
/**
* 增加积分
*/
public void incrScore(Long userId, Integer delta) {
String key = "rank:score";
redisTemplate.opsForZSet().incrementScore(key, userId.toString(), delta);
}
/**
* 获取排行榜(前10名)
*/
public List<RankVO> getTopRank(int top) {
String key = "rank:score";
// 获取分数最高的N个(倒序)
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, top - 1);
List<RankVO> result = new ArrayList<>();
int rank = 1;
for (ZSetOperations.TypedTuple<String> tuple : tuples) {
RankVO vo = new RankVO();
vo.setRank(rank++);
vo.setUserId(Long.parseLong(tuple.getValue()));
vo.setScore(tuple.getScore().intValue());
result.add(vo);
}
return result;
}
/**
* 获取用户排名
*/
public Integer getUserRank(Long userId) {
String key = "rank:score";
// 获取排名(倒序,分数高的在前)
Long rank = redisTemplate.opsForZSet().reverseRank(key, userId.toString());
return rank == null ? null : rank.intValue() + 1; // 排名从1开始
}
/**
* 获取用户积分
*/
public Integer getUserScore(Long userId) {
String key = "rank:score";
Double score = redisTemplate.opsForZSet().score(key, userId.toString());
return score == null ? 0 : score.intValue();
}
}
场景2:延迟队列
@Service
public class DelayQueueService {
/**
* 添加延迟任务
*/
public void addDelayTask(String taskId, long delaySeconds) {
String key = "delay:queue";
// 分数是执行时间戳
long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
redisTemplate.opsForZSet().add(key, taskId, executeTime);
System.out.println("添加延迟任务:" + taskId + "," + delaySeconds + "秒后执行");
}
/**
* 消费延迟任务
*/
public void consumeDelayTask() {
String key = "delay:queue";
while (true) {
long now = System.currentTimeMillis();
// 获取分数小于当前时间的任务(到期的任务)
Set<String> tasks = redisTemplate.opsForZSet().rangeByScore(key, 0, now, 0, 1);
if (tasks.isEmpty()) {
try {
Thread.sleep(1000); // 没有任务,等待1秒
} catch (InterruptedException e) {
break;
}
continue;
}
String taskId = tasks.iterator().next();
// 删除任务(使用ZREM,避免重复消费)
Long removed = redisTemplate.opsForZSet().remove(key, taskId);
if (removed > 0) {
System.out.println("执行延迟任务:" + taskId);
// 处理任务
processTask(taskId);
}
}
}
private void processTask(String taskId) {
// 任务处理逻辑
System.out.println("任务处理完成:" + taskId);
}
}
测试:
// 添加任务
delayQueueService.addDelayTask("task1", 5); // 5秒后执行
delayQueueService.addDelayTask("task2", 10); // 10秒后执行
// 启动消费者
delayQueueService.consumeDelayTask();
// 输出:
// 添加延迟任务:task1,5秒后执行
// 添加延迟任务:task2,10秒后执行
// (5秒后)执行延迟任务:task1
// 任务处理完成:task1
// (再过5秒)执行延迟任务:task2
// 任务处理完成:task2
🎨 第七问:高级数据类型
Bitmap(位图)
使用场景: 签到、统计
/**
* 用户签到
*/
public void signIn(Long userId, LocalDate date) {
String key = "sign:" + userId + ":" + date.getYear() + ":" + date.getMonthValue();
int day = date.getDayOfMonth();
// 设置第day位为1
redisTemplate.opsForValue().setBit(key, day - 1, true);
}
/**
* 检查是否签到
*/
public boolean hasSignIn(Long userId, LocalDate date) {
String key = "sign:" + userId + ":" + date.getYear() + ":" + date.getMonthValue();
int day = date.getDayOfMonth();
return Boolean.TRUE.equals(redisTemplate.opsForValue().getBit(key, day - 1));
}
/**
* 获取本月签到次数
*/
public long getSignCount(Long userId, int year, int month) {
String key = "sign:" + userId + ":" + year + ":" + month;
// 统计1的个数
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.bitCount(key.getBytes()));
}
HyperLogLog(基数统计)
使用场景: UV统计(不精确,但省内存)
/**
* 记录用户访问
*/
public void recordVisit(String pageId, Long userId) {
String key = "uv:" + pageId;
redisTemplate.opsForHyperLogLog().add(key, userId.toString());
}
/**
* 获取UV(独立访客数)
*/
public Long getUV(String pageId) {
String key = "uv:" + pageId;
return redisTemplate.opsForHyperLogLog().size(key);
}
优势:
- 12KB内存统计2^64个不同元素
- 误差率0.81%
- 适合大数据量场景
GEO(地理位置)
使用场景: 附近的人、外卖配送
/**
* 添加位置
*/
public void addLocation(Long userId, double longitude, double latitude) {
String key = "location:users";
// 添加地理位置
redisTemplate.opsForGeo().add(key,
new Point(longitude, latitude),
userId.toString());
}
/**
* 获取附近的人
*/
public List<Long> getNearbyUsers(double longitude, double latitude, double radius) {
String key = "location:users";
// 以指定坐标为中心,查找半径范围内的用户
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(key,
new Circle(new Point(longitude, latitude),
new Distance(radius, Metrics.KILOMETERS)),
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance() // 包含距离
.sortAscending() // 按距离升序
.limit(10)); // 最多10个
List<Long> userIds = new ArrayList<>();
for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) {
userIds.add(Long.parseLong(result.getContent().getName()));
}
return userIds;
}
📊 第八问:数据类型选择指南
对比表格
| 数据类型 | 使用场景 | 优点 | 缺点 |
|---|---|---|---|
| String | 缓存对象、计数器、Session | 简单易用 | 修改整个值 |
| List | 消息队列、时间线、列表 | 有序、支持阻塞 | 查询慢 |
| Hash | 对象存储、购物车 | 节省内存、字段操作 | 不支持过期单个字段 |
| Set | 标签、去重、集合运算 | 去重、集合操作 | 无序 |
| ZSet | 排行榜、延迟队列 | 有序、范围查询 | 内存占用高 |
| Bitmap | 签到、在线状态 | 极省内存 | 只能存0/1 |
| HyperLogLog | UV统计 | 超省内存 | 有误差 |
| GEO | 附近的人、外卖配送 | 地理位置查询 | 功能单一 |
选择决策树
你的需求是?
│
├─ 简单的Key-Value缓存
│ └─ 选择:String
│
├─ 存储对象,需要修改单个字段
│ └─ 选择:Hash
│
├─ 消息队列、列表
│ └─ 选择:List
│
├─ 去重、标签、集合运算
│ └─ 选择:Set
│
├─ 排行榜、有序数据
│ └─ 选择:ZSet
│
├─ 签到、布尔值统计
│ └─ 选择:Bitmap
│
├─ 大数据量UV统计
│ └─ 选择:HyperLogLog
│
└─ 地理位置相关
└─ 选择:GEO
💡 知识点总结
Redis数据结构核心要点
✅ 5种基本类型
- String - 字符串(最基础)
- List - 列表(有序可重复)
- Hash - 哈希(对象存储)
- Set - 集合(去重)
- ZSet - 有序集合(排序)
✅ 3种高级类型
- Bitmap - 位图(省内存)
- HyperLogLog - 基数统计(省内存)
- GEO - 地理位置(LBS)
✅ 实战场景
- 缓存:String、Hash
- 排行榜:ZSet
- 消息队列:List
- 标签系统:Set
- 签到:Bitmap
- UV统计:HyperLogLog
- 附近的人:GEO
✅ 选择原则
- 根据业务场景选择
- 考虑内存占用
- 考虑操作复杂度
✅ 底层实现
- 每种类型有多种底层实现
- 自动选择最优实现
- 这是Redis高性能的秘密
记忆口诀
String最简单,缓存和计数。
List做队列,时间线排序。
Hash存对象,购物车最优。
Set去重复,标签和集合。
ZSet有顺序,排行榜首选。
Bitmap省内存,签到用它行。
HyperLogLog统计UV,
GEO附近找的人。
🤔 常见面试题
Q1: Redis有哪些数据类型?
A:
基本类型(5种):
1. String - 字符串
2. List - 列表
3. Hash - 哈希
4. Set - 集合
5. ZSet - 有序集合
高级类型(3种):
1. Bitmap - 位图
2. HyperLogLog - 基数统计
3. GEO - 地理位置
Q2: ZSet的底层实现是什么?
A:
ZSet底层使用两种数据结构:
1. 跳表(skiplist)
- 用于范围查询
- 时间复杂度O(logN)
2. 哈希表(hashtable)
- 用于通过member快速查分数
- 时间复杂度O(1)
两种结构同时维护,空间换时间。
Q3: 什么场景用Hash,什么场景用String?
A:
String:
- 简单的Key-Value
- 整个对象很少修改
- 不需要单独操作字段
Hash:
- 对象有多个字段
- 需要频繁修改单个字段
- 节省内存
例如:
用户基本信息 → Hash(name、age等字段可单独修改)
用户Token → String(整体存储,很少修改)
💬 写在最后
从String到ZSet,我们全面学习了Redis的8种数据结构:
- 💎 掌握了5种基本类型的使用
- 🚀 了解了3种高级类型的场景
- 💻 完成了多个实战案例
- 📊 学会了如何选择合适的类型
这篇文章,希望能让你对Redis数据结构有清晰的认识!
如果这篇文章对你有帮助,请:
- 👍 点赞支持
- ⭐ 收藏备用
- 🔄 转发分享
- 💬 评论交流
感谢阅读,下一篇我们聊Redis持久化! 👋