Redis 从入门到精通:Spring Boot 实战三部曲(一)------ 基础核心与快速上手
专题导读:本系列共三篇,从基础到高级,带你系统掌握 Redis 在 Spring Boot 项目中的实战应用。
- 第一篇:基础核心与快速上手(本文)
- 第二篇:进阶原理与高可用架构
- 第三篇:高级特性与性能优化
📖 前言
Redis 作为当前最流行的内存数据库,以其高性能、丰富的数据结构和可靠性著称。在当今的互联网应用中,Redis 几乎成为了标配技术栈。
本文将从 Redis 的基础概念出发,结合 Spring Boot 项目,带你快速上手 Redis 开发,掌握五大核心数据结构的实际应用。
学完本文你将掌握:
- ✅ Redis 的核心概念与应用场景
- ✅ 五大数据结构的使用方法与最佳实践
- ✅ Spring Boot 集成 Redis 的完整配置
- ✅ 实际业务场景中的缓存实现
- ✅ 常见问题的解决方案
一、Redis 是什么?为什么需要它?
1.1 Redis 简介
Redis(Remote Dictionary Server)是一个开源的、基于内存的数据结构存储系统,可以用作数据库、缓存和消息中间件。
核心特点:
- 🚀 高性能:读写速度可达 10万+ QPS
- 💾 丰富的数据结构:String、List、Hash、Set、ZSet
- 🔧 持久化支持:RDB 快照 + AOF 日志
- 🔄 主从复制:支持数据备份和读写分离
- ⚡ 原子操作:单线程模型保证操作的原子性
1.2 典型应用场景
┌─────────────────────────────────────────────┐
│ Redis 应用场景 │
├──────────────┬──────────────────────────────┤
│ 缓存 │ 热点数据缓存、Session存储 │
│ 计数器 │ 点赞数、浏览量、排行榜 │
│ 消息队列 │ 异步任务、事件通知 │
│ 分布式锁 │ 防止超卖、幂等性控制 │
│ 社交功能 │ 共同好友、关注列表 │
│ 实时系统 │ 在线用户、实时统计 │
└──────────────┴──────────────────────────────┘
1.3 与传统数据库对比
| 特性 | Redis | MySQL |
|---|---|---|
| 数据存储 | 内存 | 磁盘 |
| 读写速度 | 微秒级 | 毫秒级 |
| 数据类型 | 丰富结构化 | 关系型表格 |
| 持久化 | 可选 | 必须 |
| 适用场景 | 缓存、高速读写 | 持久化存储、复杂查询 |
最佳实践:Redis 不是要替代 MySQL,而是与之配合使用,形成多级存储架构。
二、环境搭建与快速开始
2.1 Redis 安装
Windows 安装
bash
# 下载 Redis for Windows
# 访问:https://github.com/microsoftarchive/redis/releases
# 启动 Redis
redis-server.exe redis.windows.conf
# 测试连接
redis-cli.exe
127.0.0.1:6379> PING
PONG
Linux 安装
bash
# Ubuntu/Debian
sudo apt-get install redis-server
# CentOS/RHEL
sudo yum install redis
# 启动服务
sudo systemctl start redis
sudo systemctl enable redis
# 测试
redis-cli ping
Docker 安装(推荐)
bash
docker run -d --name redis \
-p 6379:6379 \
-v /data/redis:/data \
redis:7.0 \
redis-server --appendonly yes
2.2 常用命令行工具
bash
# 连接 Redis
redis-cli -h 127.0.0.1 -p 6379
# 基本测试
127.0.0.1:6379> PING
PONG
# 查看服务器信息
127.0.0.1:6379> INFO
# 查看数据库大小
127.0.0.1:6379> DBSIZE
# 清空当前数据库
127.0.0.1:6379> FLUSHDB
三、五大核心数据结构详解
3.1 String(字符串)
最基本的数据类型,二进制安全,可以存储任何数据。
常用命令
bash
# 设置值
SET name "张三"
# 获取值
GET name
# 设置带过期时间的值(秒)
SETEX code 300 "123456"
# 数值自增
INCR views
INCRBY score 10
# 批量操作
MSET key1 value1 key2 value2
MGET key1 key2
Spring Boot 实战案例
场景 1:用户 Session 管理
java
@Service
public class SessionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String SESSION_PREFIX = "session:";
private static final long SESSION_TIMEOUT = 30; // 分钟
/**
* 创建会话
*/
public String createSession(Long userId, String userInfo) {
String sessionId = UUID.randomUUID().toString();
String key = SESSION_PREFIX + sessionId;
// 存储用户信息,30分钟过期
redisTemplate.opsForValue().set(key, userInfo, SESSION_TIMEOUT, TimeUnit.MINUTES);
return sessionId;
}
/**
* 获取会话信息
*/
public String getSession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
return (String) redisTemplate.opsForValue().get(key);
}
/**
* 刷新会话(延长过期时间)
*/
public void refreshSession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
redisTemplate.expire(key, SESSION_TIMEOUT, TimeUnit.MINUTES);
}
/**
* 销毁会话
*/
public void destroySession(String sessionId) {
String key = SESSION_PREFIX + sessionId;
redisTemplate.delete(key);
}
}
场景 2:短信验证码
java
@Service
public class VerificationCodeService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String CODE_PREFIX = "sms:code:";
private static final long CODE_EXPIRE = 5; // 分钟
private static final int MAX_RETRY = 3; // 最大重试次数
/**
* 发送验证码
*/
public void sendCode(String phone) {
String code = generateCode();
String key = CODE_PREFIX + phone;
// 检查是否频繁发送
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if (ttl != null && ttl > 60) {
throw new RuntimeException("请勿频繁发送验证码");
}
// 存储验证码,5分钟过期
redisTemplate.opsForValue().set(key, code, CODE_EXPIRE, TimeUnit.MINUTES);
// TODO: 调用短信服务发送
System.out.println("验证码:" + code);
}
/**
* 验证验证码
*/
public boolean verifyCode(String phone, String code) {
String key = CODE_PREFIX + phone;
String storedCode = (String) redisTemplate.opsForValue().get(key);
if (storedCode != null && storedCode.equals(code)) {
// 验证成功后删除
redisTemplate.delete(key);
return true;
}
return false;
}
private String generateCode() {
return String.valueOf((int)((Math.random() * 9 + 1) * 100000));
}
}
场景 3:文章浏览量统计
java
@Service
public class ArticleViewService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String VIEW_KEY = "article:view:";
/**
* 增加浏览量
*/
public void incrementView(Long articleId) {
String key = VIEW_KEY + articleId;
redisTemplate.opsForValue().increment(key);
}
/**
* 获取浏览量
*/
public Long getViewCount(Long articleId) {
String key = VIEW_KEY + articleId;
Object count = redisTemplate.opsForValue().get(key);
return count != null ? Long.parseLong(count.toString()) : 0L;
}
/**
* 批量获取浏览量
*/
public Map<Long, Long> batchGetViews(List<Long> articleIds) {
List<String> keys = articleIds.stream()
.map(id -> VIEW_KEY + id)
.collect(Collectors.toList());
List<Object> values = redisTemplate.opsForValue().multiGet(keys);
Map<Long, Long> result = new HashMap<>();
for (int i = 0; i < articleIds.size(); i++) {
Long count = values.get(i) != null ?
Long.parseLong(values.get(i).toString()) : 0L;
result.put(articleIds.get(i), count);
}
return result;
}
}
3.2 Hash(哈希)
适合存储对象,比 String 更节省内存。
常用命令
bash
# 设置字段
HSET user:1001 name "张三" age 25 email "zhangsan@example.com"
# 获取单个字段
HGET user:1001 name
# 获取所有字段
HGETALL user:1001
# 获取多个字段
HMGET user:1001 name age
# 删除字段
HDEL user:1001 email
# 判断字段是否存在
HEXISTS user:1001 name
# 获取字段数量
HLEN user:1001
Spring Boot 实战案例
场景:用户信息管理
java
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String USER_HASH_KEY = "user:info:";
/**
* 保存用户信息
*/
public void saveUser(User user) {
String key = USER_HASH_KEY + user.getId();
Map<String, Object> userMap = new HashMap<>();
userMap.put("id", user.getId());
userMap.put("name", user.getName());
userMap.put("age", user.getAge());
userMap.put("email", user.getEmail());
userMap.put("phone", user.getPhone());
userMap.put("createTime", System.currentTimeMillis());
redisTemplate.opsForHash().putAll(key, userMap);
// 设置过期时间(可选)
redisTemplate.expire(key, 24, TimeUnit.HOURS);
}
/**
* 获取用户信息
*/
public User getUser(Long userId) {
String key = USER_HASH_KEY + userId;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
if (entries.isEmpty()) {
return null;
}
User user = new User();
user.setId(Long.parseLong(entries.get("id").toString()));
user.setName(entries.get("name").toString());
user.setAge(Integer.parseInt(entries.get("age").toString()));
user.setEmail(entries.get("email").toString());
user.setPhone(entries.get("phone").toString());
return user;
}
/**
* 更新用户部分信息
*/
public void updateUserField(Long userId, String field, Object value) {
String key = USER_HASH_KEY + userId;
redisTemplate.opsForHash().put(key, field, value);
}
/**
* 批量获取用户信息
*/
public List<User> batchGetUsers(List<Long> userIds) {
List<User> users = new ArrayList<>();
for (Long userId : userIds) {
User user = getUser(userId);
if (user != null) {
users.add(user);
}
}
return users;
}
/**
* 删除用户
*/
public void deleteUser(Long userId) {
String key = USER_HASH_KEY + userId;
redisTemplate.delete(key);
}
}
优势分析:
- ✅ 相比 String 序列化整个对象,Hash 可以单独更新某个字段
- ✅ 节省内存(特别是字段较多时)
- ✅ 更适合存储结构化数据
3.3 List(列表)
双向链表结构,支持从两端插入和弹出。
常用命令
bash
# 左侧推入
LPUSH tasks "task1" "task2" "task3"
# 右侧推入
RPUSH messages "msg1" "msg2"
# 左侧弹出
LPOP tasks
# 右侧弹出
RPOP messages
# 获取列表长度
LLEN tasks
# 获取指定范围
LRANGE messages 0 10
# 阻塞式弹出(超时时间秒)
BRPOP tasks 10
Spring Boot 实战案例
场景 1:简单消息队列
java
@Service
@Slf4j
public class MessageQueueService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String QUEUE_KEY = "queue:messages";
/**
* 生产消息
*/
public void produceMessage(String message) {
redisTemplate.opsForList().leftPush(QUEUE_KEY, message);
log.info("消息已入队: {}", message);
}
/**
* 消费消息(非阻塞)
*/
public String consumeMessage() {
Object message = redisTemplate.opsForList().rightPop(QUEUE_KEY);
return message != null ? message.toString() : null;
}
/**
* 消费消息(阻塞式,推荐)
*/
public String consumeMessageBlocking(long timeout, TimeUnit unit) {
Object message = redisTemplate.opsForList()
.rightPop(QUEUE_KEY, timeout, unit);
return message != null ? message.toString() : null;
}
/**
* 获取队列长度
*/
public Long getQueueSize() {
return redisTemplate.opsForList().size(QUEUE_KEY);
}
/**
* 批量生产消息
*/
public void batchProduceMessages(List<String> messages) {
redisTemplate.opsForList().leftPushAll(QUEUE_KEY, messages.toArray());
log.info("批量消息已入队,数量: {}", messages.size());
}
}
消费者示例:
java
@Component
@Slf4j
public class MessageConsumer {
@Autowired
private MessageQueueService queueService;
@PostConstruct
public void startConsuming() {
// 启动后台线程持续消费
new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 阻塞式消费,超时10秒
String message = queueService.consumeMessageBlocking(10, TimeUnit.SECONDS);
if (message != null) {
log.info("处理消息: {}", message);
// TODO: 处理业务逻辑
processMessage(message);
}
} catch (Exception e) {
log.error("消息处理失败", e);
}
}
}, "message-consumer-thread").start();
}
private void processMessage(String message) {
// 业务处理逻辑
System.out.println("Processing: " + message);
}
}
场景 2:最新动态列表
java
@Service
public class FeedService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String FEED_KEY = "user:feed:";
private static final int MAX_FEED_SIZE = 100; // 最多保留100条
/**
* 发布动态
*/
public void publishFeed(Long userId, String content) {
String key = FEED_KEY + userId;
Map<String, Object> feed = new HashMap<>();
feed.put("content", content);
feed.put("timestamp", System.currentTimeMillis());
feed.put("id", UUID.randomUUID().toString());
// 左侧推入(最新的在前面)
redisTemplate.opsForList().leftPush(key, feed);
// 限制列表长度,删除多余的
redisTemplate.opsForList().trim(key, 0, MAX_FEED_SIZE - 1);
}
/**
* 获取用户动态列表
*/
public List<Object> getUserFeeds(Long userId, int page, int pageSize) {
String key = FEED_KEY + userId;
long start = (long) (page - 1) * pageSize;
long end = start + pageSize - 1;
return redisTemplate.opsForList().range(key, start, end);
}
/**
* 获取最新动态N条
*/
public List<Object> getLatestFeeds(Long userId, int count) {
String key = FEED_KEY + userId;
return redisTemplate.opsForList().range(key, 0, count - 1);
}
}
3.4 Set(集合)
无序不重复集合,支持交集、并集、差集运算。
常用命令
bash
# 添加成员
SADD tags:article:1 "Java" "Redis" "Spring"
# 获取所有成员
SMEMBERS tags:article:1
# 判断成员是否存在
SISMEMBER tags:article:1 "Java"
# 删除成员
SREM tags:article:1 "Redis"
# 获取集合大小
SCARD tags:article:1
# 交集
SINTER tags:article:1 tags:article:2
# 并集
SUNION tags:article:1 tags:article:2
# 差集
SDIFF tags:article:1 tags:article:2
# 随机获取一个成员
SRANDMEMBER tags:article:1
# 随机弹出一个成员
SPOP tags:article:1
Spring Boot 实战案例
场景 1:文章标签系统
java
@Service
public class ArticleTagService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String ARTICLE_TAGS_KEY = "tags:article:";
private static final String TAG_ARTICLES_KEY = "articles:tag:";
/**
* 为文章添加标签
*/
public void addTags(Long articleId, Set<String> tags) {
String articleKey = ARTICLE_TAGS_KEY + articleId;
// 文章的标签集合
redisTemplate.opsForSet().add(articleKey, tags.toArray());
// 反向索引:标签下的文章集合
for (String tag : tags) {
String tagKey = TAG_ARTICLES_KEY + tag;
redisTemplate.opsForSet().add(tagKey, articleId);
}
}
/**
* 获取文章的标签
*/
public Set<Object> getArticleTags(Long articleId) {
String key = ARTICLE_TAGS_KEY + articleId;
return redisTemplate.opsForSet().members(key);
}
/**
* 获取标签下的文章
*/
public Set<Object> getArticlesByTag(String tag) {
String key = TAG_ARTICLES_KEY + tag;
return redisTemplate.opsForSet().members(key);
}
/**
* 查找同时包含多个标签的文章(交集)
*/
public Set<Object> findArticlesWithTags(String... tags) {
if (tags.length == 0) {
return Collections.emptySet();
}
String[] keys = Arrays.stream(tags)
.map(tag -> TAG_ARTICLES_KEY + tag)
.toArray(String[]::new);
return redisTemplate.opsForSet().intersect(keys);
}
/**
* 查找包含任一标签的文章(并集)
*/
public Set<Object> findArticlesWithAnyTag(String... tags) {
if (tags.length == 0) {
return Collections.emptySet();
}
String[] keys = Arrays.stream(tags)
.map(tag -> TAG_ARTICLES_KEY + tag)
.toArray(String[]::new);
return redisTemplate.opsForSet().union(keys);
}
/**
* 移除文章的标签
*/
public void removeTag(Long articleId, String tag) {
String articleKey = ARTICLE_TAGS_KEY + articleId;
String tagKey = TAG_ARTICLES_KEY + tag;
redisTemplate.opsForSet().remove(articleKey, tag);
redisTemplate.opsForSet().remove(tagKey, articleId);
}
}
场景 2:共同好友功能
java
@Service
public class FriendService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String FRIENDS_KEY = "user:friends:";
/**
* 添加好友
*/
public void addFriend(Long userId, Long friendId) {
String key = FRIENDS_KEY + userId;
redisTemplate.opsForSet().add(key, friendId);
}
/**
* 获取用户的好友列表
*/
public Set<Object> getFriends(Long userId) {
String key = FRIENDS_KEY + userId;
return redisTemplate.opsForSet().members(key);
}
/**
* 查找共同好友
*/
public Set<Object> findMutualFriends(Long userId1, Long userId2) {
String key1 = FRIENDS_KEY + userId1;
String key2 = FRIENDS_KEY + userId2;
return redisTemplate.opsForSet().intersect(key1, key2);
}
/**
* 推荐好友(好友的好友,排除自己和已有好友)
*/
public Set<Object> recommendFriends(Long userId) {
Set<Object> myFriends = getFriends(userId);
Set<Object> recommended = new HashSet<>();
// 遍历我的每个好友
for (Object friendId : myFriends) {
Set<Object> friendOfFriend = getFriends(Long.parseLong(friendId.toString()));
// 排除自己和已有好友
friendOfFriend.removeAll(myFriends);
friendOfFriend.remove(userId);
recommended.addAll(friendOfFriend);
}
return recommended;
}
/**
* 删除好友
*/
public void removeFriend(Long userId, Long friendId) {
String key = FRIENDS_KEY + userId;
redisTemplate.opsForSet().remove(key, friendId);
}
}
3.5 ZSet(有序集合)
带分数的集合,自动排序,适合排行榜场景。
常用命令
bash
# 添加成员(带分数)
ZADD leaderboard 100 "player1" 200 "player2" 150 "player3"
# 获取排名(从小到大,0-based)
ZRANK leaderboard "player1"
# 获取排名(从大到小)
ZREVRANK leaderboard "player1"
# 获取指定范围的成员(从小到大)
ZRANGE leaderboard 0 9
# 获取指定范围的成员(从大到小)
ZREVRANGE leaderboard 0 9 WITHSCORES
# 获取成员的分数
ZSCORE leaderboard "player1"
# 增加分数
ZINCRBY leaderboard 10 "player1"
# 获取集合大小
ZCARD leaderboard
# 删除成员
ZREM leaderboard "player1"
# 获取分数范围内的成员
ZRANGEBYSCORE leaderboard 100 200
Spring Boot 实战案例
场景 1:游戏排行榜
java
@Service
public class LeaderboardService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String LEADERBOARD_KEY = "game:leaderboard";
/**
* 更新玩家分数
*/
public void updateScore(Long playerId, double score) {
redisTemplate.opsForZSet().add(LEADERBOARD_KEY, playerId, score);
}
/**
* 增加分数
*/
public void incrementScore(Long playerId, double increment) {
redisTemplate.opsForZSet().incrementScore(LEADERBOARD_KEY, playerId, increment);
}
/**
* 获取 Top N 玩家
*/
public Set<Object> getTopN(int n) {
// 从大到小排序,获取前N个
return redisTemplate.opsForZSet()
.reverseRange(LEADERBOARD_KEY, 0, n - 1);
}
/**
* 获取 Top N 玩家(带分数)
*/
public Set<ZSetOperations.TypedTuple<Object>> getTopNWithScores(int n) {
return redisTemplate.opsForZSet()
.reverseRangeWithScores(LEADERBOARD_KEY, 0, n - 1);
}
/**
* 获取玩家排名
*/
public Long getPlayerRank(Long playerId) {
Long rank = redisTemplate.opsForZSet()
.reverseRank(LEADERBOARD_KEY, playerId);
// 排名从0开始,转换为从1开始
return rank != null ? rank + 1 : null;
}
/**
* 获取玩家分数
*/
public Double getPlayerScore(Long playerId) {
return redisTemplate.opsForZSet()
.score(LEADERBOARD_KEY, playerId);
}
/**
* 获取玩家的排名和分数
*/
public Map<String, Object> getPlayerInfo(Long playerId) {
Map<String, Object> info = new HashMap<>();
info.put("playerId", playerId);
info.put("rank", getPlayerRank(playerId));
info.put("score", getPlayerScore(playerId));
return info;
}
/**
* 获取分数段内的玩家
*/
public Set<Object> getPlayersByScoreRange(double minScore, double maxScore) {
return redisTemplate.opsForZSet()
.rangeByScore(LEADERBOARD_KEY, minScore, maxScore);
}
/**
* 删除玩家
*/
public void removePlayer(Long playerId) {
redisTemplate.opsForZSet().remove(LEADERBOARD_KEY, playerId);
}
/**
* 获取排行榜总人数
*/
public Long getTotalPlayers() {
return redisTemplate.opsForZSet().zCard(LEADERBOARD_KEY);
}
}
Controller 层示例:
java
@RestController
@RequestMapping("/api/leaderboard")
public class LeaderboardController {
@Autowired
private LeaderboardService leaderboardService;
/**
* 更新分数
*/
@PostMapping("/score")
public ResponseEntity<Void> updateScore(
@RequestParam Long playerId,
@RequestParam Double score) {
leaderboardService.updateScore(playerId, score);
return ResponseEntity.ok().build();
}
/**
* 获取 Top 10
*/
@GetMapping("/top10")
public ResponseEntity<Set<Object>> getTop10() {
return ResponseEntity.ok(leaderboardService.getTopN(10));
}
/**
* 获取我的排名
*/
@GetMapping("/my-rank")
public ResponseEntity<Map<String, Object>> getMyRank(
@RequestParam Long playerId) {
return ResponseEntity.ok(leaderboardService.getPlayerInfo(playerId));
}
}
场景 2:延迟队列
java
@Service
@Slf4j
public class DelayQueueService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String DELAY_QUEUE_KEY = "delay:queue";
/**
* 添加延迟任务
*
* @param taskId 任务ID
* @param delaySeconds 延迟秒数
*/
public void addDelayTask(String taskId, long delaySeconds) {
long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
// 分数为执行时间戳
redisTemplate.opsForZSet().add(DELAY_QUEUE_KEY, taskId, executeTime);
log.info("添加延迟任务: {}, 执行时间: {}", taskId, new Date(executeTime));
}
/**
* 获取到期任务
*/
public Set<Object> getDueTasks() {
long now = System.currentTimeMillis();
// 获取分数小于等于当前时间的任务
return redisTemplate.opsForZSet()
.rangeByScore(DELAY_QUEUE_KEY, 0, now);
}
/**
* 处理到期任务
*/
public void processDueTasks() {
Set<Object> dueTasks = getDueTasks();
for (Object taskId : dueTasks) {
// 尝试移除任务(保证只处理一次)
Boolean removed = redisTemplate.opsForZSet()
.remove(DELAY_QUEUE_KEY, taskId);
if (Boolean.TRUE.equals(removed)) {
log.info("处理到期任务: {}", taskId);
// TODO: 执行任务逻辑
executeTask(taskId.toString());
}
}
}
private void executeTask(String taskId) {
// 任务执行逻辑
System.out.println("Executing task: " + taskId);
}
}
定时任务处理器:
java
@Component
@Slf4j
public class DelayTaskProcessor {
@Autowired
private DelayQueueService delayQueueService;
/**
* 每秒检查一次到期任务
*/
@Scheduled(fixedRate = 1000)
public void processTasks() {
try {
delayQueueService.processDueTasks();
} catch (Exception e) {
log.error("处理延迟任务失败", e);
}
}
}
四、Spring Boot 集成 Redis 完整配置
4.1 Maven 依赖
xml
<dependencies>
<!-- Spring Boot Starter Data Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 连接池支持 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
4.2 application.yml 配置
yaml
spring:
redis:
host: localhost
port: 6379
password: # 如果有密码则填写
database: 0
timeout: 3000ms
lettuce:
pool:
max-active: 20 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 3000ms # 连接超时时间
4.3 Redis 配置类
java
@Configuration
public class RedisConfig {
/**
* 配置 RedisTemplate
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// Key 使用 String 序列化
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 使用 JSON 序列化
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
4.4 通用 Redis 工具类
java
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// ==================== String 操作 ====================
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
public Long expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.getExpire(key, unit);
}
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
public Long increment(String key) {
return redisTemplate.opsForValue().increment(key);
}
// ==================== Hash 操作 ====================
public void hashSet(String key, String field, Object value) {
redisTemplate.opsForHash().put(key, field, value);
}
public Object hashGet(String key, String field) {
return redisTemplate.opsForHash().get(key, field);
}
public Map<Object, Object> hashGetAll(String key) {
return redisTemplate.opsForHash().entries(key);
}
// ==================== List 操作 ====================
public Long leftPush(String key, Object value) {
return redisTemplate.opsForList().leftPush(key, value);
}
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
public List<Object> range(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
// ==================== Set 操作 ====================
public Long setAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
public Set<Object> setMembers(String key) {
return redisTemplate.opsForSet().members(key);
}
public Boolean setIsMember(String key, Object value) {
return redisTemplate.opsForSet().isMember(key, value);
}
public Set<Object> setIntersect(String... keys) {
return redisTemplate.opsForSet().intersect(keys);
}
// ==================== ZSet 操作 ====================
public Boolean zSetAdd(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
public Set<Object> zSetReverseRange(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRange(key, start, end);
}
public Long zSetRank(String key, Object value) {
return redisTemplate.opsForZSet().reverseRank(key, value);
}
public Double zSetScore(String key, Object value) {
return redisTemplate.opsForZSet().score(key, value);
}
}
五、实战项目:简易博客系统
5.1 项目结构
blog-system/
├── controller/
│ ├── ArticleController.java
│ └── UserController.java
├── service/
│ ├── ArticleService.java
│ ├── UserService.java
│ └── ViewService.java
├── model/
│ ├── Article.java
│ └── User.java
└── config/
└── RedisConfig.java
5.2 核心功能实现
文章服务(综合应用多种数据结构):
java
@Service
public class ArticleService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private ArticleMapper articleMapper;
private static final String ARTICLE_KEY = "article:";
private static final String ARTICLE_LIST_KEY = "articles:latest";
private static final String ARTICLE_VIEW_KEY = "article:view:";
/**
* 发布文章
*/
@Transactional
public Long publishArticle(Article article) {
// 1. 保存到数据库
articleMapper.insert(article);
Long articleId = article.getId();
// 2. 缓存文章详情(Hash)
cacheArticleDetail(article);
// 3. 添加到最新文章列表(List)
redisUtil.leftPush(ARTICLE_LIST_KEY, articleId);
// 只保留最近100篇
redisUtil.trimList(ARTICLE_LIST_KEY, 0, 99);
// 4. 添加文章标签(Set)
if (article.getTags() != null && !article.getTags().isEmpty()) {
redisUtil.setAdd(ARTICLE_KEY + articleId + ":tags",
article.getTags().toArray());
}
return articleId;
}
/**
* 获取文章详情
*/
public Article getArticle(Long articleId) {
// 1. 尝试从缓存获取
Article article = getCachedArticle(articleId);
if (article != null) {
return article;
}
// 2. 从数据库查询
article = articleMapper.selectById(articleId);
if (article != null) {
// 3. 写入缓存
cacheArticleDetail(article);
}
return article;
}
/**
* 记录浏览量
*/
public void recordView(Long articleId) {
// 使用 String 类型的 INCR
redisUtil.increment(ARTICLE_VIEW_KEY + articleId);
}
/**
* 获取浏览量
*/
public Long getViewCount(Long articleId) {
Object count = redisUtil.get(ARTICLE_VIEW_KEY + articleId);
return count != null ? Long.parseLong(count.toString()) : 0L;
}
/**
* 获取最新文章列表
*/
public List<Article> getLatestArticles(int page, int pageSize) {
long start = (long) (page - 1) * pageSize;
long end = start + pageSize - 1;
List<Object> articleIds = redisUtil.range(ARTICLE_LIST_KEY, start, end);
List<Article> articles = new ArrayList<>();
for (Object id : articleIds) {
Article article = getCachedArticle(Long.parseLong(id.toString()));
if (article != null) {
articles.add(article);
}
}
return articles;
}
/**
* 缓存文章详情
*/
private void cacheArticleDetail(Article article) {
String key = ARTICLE_KEY + article.getId();
redisUtil.hashSet(key, "id", article.getId());
redisUtil.hashSet(key, "title", article.getTitle());
redisUtil.hashSet(key, "content", article.getContent());
redisUtil.hashSet(key, "authorId", article.getAuthorId());
redisUtil.hashSet(key, "createTime", article.getCreateTime());
// 设置24小时过期
redisUtil.expire(key, 24, TimeUnit.HOURS);
}
/**
* 从缓存获取文章
*/
private Article getCachedArticle(Long articleId) {
String key = ARTICLE_KEY + articleId;
Map<Object, Object> entries = redisUtil.hashGetAll(key);
if (entries.isEmpty()) {
return null;
}
Article article = new Article();
article.setId(Long.parseLong(entries.get("id").toString()));
article.setTitle(entries.get("title").toString());
article.setContent(entries.get("content").toString());
article.setAuthorId(Long.parseLong(entries.get("authorId").toString()));
article.setCreateTime(Long.parseLong(entries.get("createTime").toString()));
return article;
}
}
六、常见问题与解决方案
6.1 缓存穿透
问题:查询不存在的数据,请求直接打到数据库。
解决方案:
java
public User getUser(Long userId) {
String key = "user:" + userId;
User user = (User) redisUtil.get(key);
if (user != null) {
// 如果是空对象,说明数据库中也不存在
if (user instanceof NullObject) {
return null;
}
return user;
}
// 查询数据库
user = userMapper.selectById(userId);
if (user != null) {
redisUtil.set(key, user, 30, TimeUnit.MINUTES);
} else {
// 缓存空值,防止穿透
redisUtil.set(key, new NullObject(), 5, TimeUnit.MINUTES);
}
return user;
}
6.2 缓存击穿
问题:热点 key 过期,大量请求同时访问数据库。
解决方案:使用互斥锁
java
public User getUserWithLock(Long userId) {
String key = "user:" + userId;
User user = (User) redisUtil.get(key);
if (user != null) {
return user;
}
// 加锁
String lockKey = "lock:user:" + userId;
String requestId = UUID.randomUUID().toString();
try {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 3, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 双重检查
user = (User) redisUtil.get(key);
if (user != null) {
return user;
}
// 查询数据库并缓存
user = userMapper.selectById(userId);
if (user != null) {
redisUtil.set(key, user, 30, TimeUnit.MINUTES);
}
} else {
// 等待后重试
Thread.sleep(50);
return getUserWithLock(userId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
return user;
}
6.3 缓存雪崩
问题:大量 key 同时过期,导致数据库压力骤增。
解决方案:设置随机过期时间
java
public void setWithRandomTtl(String key, Object value, long baseTtl) {
// 基础时间 ± 20% 随机波动
long randomOffset = (long) (baseTtl * 0.2 * Math.random());
long ttl = baseTtl + (Math.random() > 0.5 ? randomOffset : -randomOffset);
redisUtil.set(key, value, ttl, TimeUnit.SECONDS);
}
七、总结与展望
7.1 本文要点回顾
✅ Redis 基础概念 :理解 Redis 的特点和应用场景
✅ 五大数据结构 :掌握 String、Hash、List、Set、ZSet 的使用
✅ Spring Boot 集成 :完成 Redis 的配置和工具类封装
✅ 实战案例 :通过博客系统综合运用所学知识
✅ 常见问题:了解缓存穿透、击穿、雪崩的解决方案
7.2 下篇预告
在下一篇文章《Redis 从入门到精通:Spring Boot 实战三部曲(二)------ 进阶原理与高可用架构》中,我们将深入探讨:
- 🔍 Redis 持久化机制(RDB & AOF)深度解析
- 🔄 主从复制与哨兵模式原理
- 🔒 事务与 Lua 脚本实战
- 🛡️ 分布式锁的高级实现
- 📊 性能监控与调优技巧
7.3 学习建议
- 动手实践:不要只看代码,一定要亲手敲一遍
- 理解原理:知其然更要知其所以然
- 循序渐进:先掌握基础,再深入学习高级特性
- 关注官方文档:获取最新的技术信息
📚 参考资料
- Redis 官方文档:https://redis.io/documentation
- Spring Data Redis:https://spring.io/projects/spring-data-redis
- 《Redis 设计与实现》- 黄健宏
觉得有用?欢迎点赞、收藏、转发!
下一篇更精彩,敬请期待! 🚀
系列文章:
- 第一篇 Redis 从入门到精通:Spring Boot 实战三部曲(一)------ 基础核心与快速上手
- 第二篇 Redis 从入门到精通:Spring Boot 实战三部曲(二)------ 进阶原理与高可用架构
- 第三篇 Redis 从入门到精通:Spring Boot 实战三部曲(三)------ 高级特性与性能优化