一、Redis 概述
- 定义:Redis(Remote Dictionary Server)是开源的高性能键值对存储数据库,以内存为主要存储介质。
- 核心特点:
- 高性能:单线程模型 + IO 多路复用,支持每秒数十万次操作;
- 多数据结构:原生支持 String、Hash、List、Set、Sorted Set(ZSet)5 种基本类型,及 Bitmap 等扩展类型;
- 原子操作:所有命令原子性,支持事务和 Lua 脚本;
- 丰富功能:过期时间、发布订阅、分布式锁、地理空间等。
- 应用场景:缓存、会话存储、消息队列、排行榜等,是分布式系统核心组件。
二、Redis 官方资源
- 官方网站:https://redis.io/
- Spring Data Redis 官方文档:https://docs.spring.io/spring-data/redis/docs/current/reference/html/
三、Spring Data Redis 依赖(Maven)
<!-- Spring 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>
四、Redis 5 种基本类型详解
1. String(字符串)
- 数据结构:二进制安全,可存储文本、数字、二进制数据(最大 512MB),底层基于简单动态字符串(SDS)。
- 应用场景:缓存简单数据(如用户信息 JSON)、计数器(文章阅读量)、分布式锁、限流等。
- 原生命令示例:
命令 | 说明 | 例子 | 执行后结果 |
SET key value [EX seconds] | 设置键值对(可选过期时间) | SET article:1001 "Redis 教程" EX 3600 | article:1001 → "Redis 教程"(1 小时过期) |
GET key | 获取键值 | GET article:1001 | 返回 "Redis 教程" |
INCR key | 数值自增 1(原子操作) | INCR view:1001 | 从 0→1 |
DECR key | 数值自减 1 | DECR stock:phone | 从 10→9 |
GETSET key value | 先获取旧值,再设新值 | GETSET counter:1 0 | 返回旧值 5,设为 0 |
- Spring Data Redis 实现:
public class StringDemo { private StringRedisTemplate stringRedisTemplate; // 缓存用户信息(带过期时间) public void cacheUserInfo(String userId, String userJson, int expireSeconds) { stringRedisTemplate.opsForValue().set("user:" + userId, userJson, expireSeconds, TimeUnit.SECONDS); // 结果:user:100 → "{\"name\":\"张三\"}"(3600秒后过期) } // 文章阅读量自增 public Long incrementArticleView(String articleId) { return stringRedisTemplate.opsForValue().increment("view:" + articleId); // 结果:原数值+1 } // 获取缓存的用户信息 public String getUserInfo(String userId) { return stringRedisTemplate.opsForValue().get("user:" + userId); // 结果:如"{\"name\":\"张三\"}" } }
2. Hash(哈希)
- 数据结构:类似 HashMap,由 field-value 键值对组成,适合存储对象。小数据用压缩列表(ziplist),大数据用哈希表(hashtable)。
- 应用场景:存储对象型数据(用户信息、商品详情),支持字段级操作(避免全量序列化)。
- 原生命令示例:
命令 | 说明 | 例子 | 执行后结果 |
HSET key field value | 设置哈希字段值 | HSET user:100 name "张三" age "25" | user:100 → {name: "张三", age: "25"} |
HGET key field | 获取哈希字段值 | HGET user:100 name | 返回 "张三" |
HMGET key field1 field2 | 批量获取字段值 | HMGET user:100 name age | 返回 ["张三", "25"] |
HGETALL key | 获取所有字段和值 | HGETALL user:100 | 返回 ["name", "张三", "age", "25"] |
HDEL key field | 删除哈希字段 | HDEL user:100 age | 剩余 {name: "张三"} |
- Spring Data Redis 实现:
public class HashDemo { private StringRedisTemplate stringRedisTemplate; // 存储用户信息(多字段) public void saveUser(String userId) { HashOperations<String, String, String> hashOps = stringRedisTemplate.opsForHash(); Map<String, String> userMap = new HashMap<>(); userMap.put("name", "张三"); userMap.put("age", "25"); hashOps.putAll("user:" + userId, userMap); // 结果:user:100 → {name: "张三", age: "25"} } // 获取用户姓名 public String getUserName(String userId) { return stringRedisTemplate.opsForHash().get("user:" + userId, "name"); // 返回"张三" } // 获取用户所有信息 public Map<String, String> getUserAll(String userId) { return stringRedisTemplate.opsForHash().entries("user:" + userId); // 返回所有字段和值 } }
3. List(列表)
- 数据结构:有序可重复元素集合,类似双向链表,支持两端插入 / 删除。小数据用压缩列表(ziplist),大数据用双向链表(linkedlist)。
- 应用场景:消息队列(LPUSH+RPOP)、最新列表(如最新文章)、有限长度排行榜。
- 原生命令示例:
命令 | 说明 | 例子 | 执行后结果 |
LPUSH key value1 [value2] | 左侧插入元素 | LPUSH msg:queue "消息 1" "消息 2" | msg:queue → ["消息 2", "消息 1"] |
RPUSH key value1 [value2] | 右侧插入元素 | RPUSH article:new "ID1" "ID2" | article:new → ["ID1", "ID2"] |
LPOP key | 左侧弹出元素 | LPOP msg:queue | 返回 "消息 2",剩余 ["消息 1"] |
RPOP key | 右侧弹出元素 | RPOP article:new | 返回 "ID2",剩余 ["ID1"] |
LRANGE key start end | 获取范围元素 | LRANGE article:new 0 1 | 若列表为 ["ID1","ID2","ID3"],返回 ["ID1","ID2"] |
- Spring Data Redis 实现:
public class ListDemo { private StringRedisTemplate stringRedisTemplate; // 发送消息(左侧插入,模拟队列) public Long sendMessage(String queueName, String message) { Long newSize = stringRedisTemplate.opsForList().leftPush(queueName, message); // 结果:msg:queue → [新消息, 原元素...] return newSize; } // 消费消息(右侧弹出) public String consumeMessage(String queueName) { return stringRedisTemplate.opsForList().rightPop(queueName); // 返回弹出的消息 } // 获取最新5条文章ID public List<String> getLatestArticles(String listKey) { return stringRedisTemplate.opsForList().range(listKey, 0, 4); // 返回前5条 } }
4. Set(集合)
- 数据结构:无序、不可重复元素集合,支持交集、并集、差集。小数据用压缩列表(ziplist),大数据用哈希表(hashtable)。
- 应用场景:去重存储(用户标签)、共同好友计算、随机抽奖等。
- 原生命令示例:
命令 | 说明 | 例子 | 执行后结果 |
SADD key member1 [member2] | 添加元素(自动去重) | SADD user:100:tags "足球" "音乐" "足球" | user:100:tags → {"足球", "音乐"} |
SMEMBERS key | 获取所有元素 | SMEMBERS user:100:tags | 返回 ["足球", "音乐"](顺序随机) |
SINTER key1 key2 | 计算交集 | SINTER user:100:tags user:200:tags(user200 含 {"音乐","阅读"}) | 返回 ["音乐"] |
SUNION key1 key2 | 计算并集 | SUNION user:100:tags user:200:tags | 返回 ["足球", "音乐", "阅读"] |
SREM key member | 移除元素 | SREM user:100:tags "音乐" | 剩余 {"足球"} |
- Spring Data Redis 实现:
public class SetDemo { private StringRedisTemplate stringRedisTemplate; // 给用户添加标签(自动去重) public Long addUserTags(String userId, String... tags) { return stringRedisTemplate.opsForSet().add("user:" + userId + ":tags", tags); } // 获取用户所有标签 public Set<String> getUserTags(String userId) { return stringRedisTemplate.opsForSet().members("user:" + userId + ":tags"); } // 计算两个用户的共同标签(交集) public Set<String> getCommonTags(String userId1, String userId2) { return stringRedisTemplate.opsForSet().intersect( "user:" + userId1 + ":tags", "user:" + userId2 + ":tags" ); } }
5. Sorted Set(ZSet,有序集合)
- 数据结构:元素关联 score(分数),按分数排序(分数可重复,元素不可重复)。小数据用压缩列表(ziplist),大数据用跳表(skiplist)+ 哈希表。
- 应用场景:游戏排行榜(按分数排序)、延迟队列(按时间戳为 score)、TOP N 统计。
- 原生命令示例:
命令 | 说明 | 例子 | 执行后结果 |
ZADD key score member | 添加元素及分数 | ZADD game:rank 95 "用户 A" 88 "用户 B" | 按 score 升序存储:{("用户 B",88), ("用户 A",95)} |
ZREVRANGE key start end [WITHSCORES] | 按 score 降序取元素 | ZREVRANGE game:rank 0 1 WITHSCORES | 返回 ["用户 A", "95", "用户 B", "88"] |
ZSCORE key member | 获取元素分数 | ZSCORE game:rank "用户 A" | 返回 "95" |
ZINCRBY key increment member | 增加元素分数 | ZINCRBY game:rank 5 "用户 B" | 用户 B 分数从 88→93 |
- Spring Data Redis 实现:
public class ZSetDemo { private StringRedisTemplate stringRedisTemplate; // 添加用户到排行榜(初始分数) public Boolean addToRank(String userName, double score) { return stringRedisTemplate.opsForZSet().add("game:rank", userName, score); } // 增加用户分数 public Double incrementScore(String userName, double increment) { return stringRedisTemplate.opsForZSet().incrementScore("game:rank", userName, increment); } // 获取排行榜前三名(降序,带分数) public Set<ZSetOperations.TypedTuple<String>> getTop3() { return stringRedisTemplate.opsForZSet().reverseRangeWithScores("game:rank", 0, 2); } }
五、缓存问题解决方案(穿透、击穿、雪崩)
在使用 Redis 缓存时,需注意解决以下常见问题,避免对数据库或系统造成压力:
1. 缓存穿透
- 问题描述:查询不存在的数据(如 ID 为 -1 的用户),导致请求绕过缓存直接访问数据库,大量请求可能压垮数据库。
- 解决方案:缓存空值(设置较短过期时间)+ 布隆过滤器(提前拦截不存在的 key)。
- Spring Data Redis 实现示例(缓存空值):
public class CachePenetrationDemo { private StringRedisTemplate stringRedisTemplate; private UserDao userDao; // 假设存在用户DAO层 // 查询用户信息,解决缓存穿透 public String getUserById(String userId) { String key = "user:" + userId; String userJson = stringRedisTemplate.opsForValue().get(key); // 缓存命中(包括空值) if (userJson != null) { // 返回空值标记时,实际返回null return "NULL".equals(userJson) ? null : userJson; } // 缓存未命中,查询数据库 User user = userDao.selectById(userId); if (user == null) { // 缓存空值,设置短过期时间(如5分钟),避免缓存穿透 stringRedisTemplate.opsForValue().set(key, "NULL", 300, TimeUnit.SECONDS); return null; } // 数据库存在,缓存用户信息(正常过期时间,如1小时) userJson = JSON.toJSONString(user); // 假设使用FastJSON stringRedisTemplate.opsForValue().set(key, userJson, 3600, TimeUnit.SECONDS); return userJson; } }
2. 缓存击穿
- 问题描述:热点 key 过期瞬间,大量请求同时访问,击穿缓存直接访问数据库,导致数据库压力骤增。
- 解决方案:互斥锁(同一时间只允许一个请求更新缓存)+ 热点 key 永不过期。
- Spring Data Redis 实现示例(基于分布式锁):
public class CacheBreakdownDemo { private StringRedisTemplate stringRedisTemplate; private ProductDao productDao; // 假设存在商品DAO层 // 查询商品详情,解决缓存击穿 public String getProductById(String productId) { String key = "product:" + productId; String productJson = stringRedisTemplate.opsForValue().get(key); // 缓存命中,直接返回 if (productJson != null) { return productJson; } // 缓存未命中,尝试获取分布式锁 String lockKey = "lock:product:" + productId; try { // 使用setIfAbsent实现简单分布式锁,过期时间10秒(避免死锁) Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { // 获取锁成功,查询数据库并更新缓存 Product product = productDao.selectById(productId); if (product == null) { // 缓存空值(同穿透处理) stringRedisTemplate.opsForValue().set(key, "NULL", 300, TimeUnit.SECONDS); return null; } // 缓存商品信息(正常过期时间) productJson = JSON.toJSONString(product); stringRedisTemplate.opsForValue().set(key, productJson, 3600, TimeUnit.SECONDS); return productJson; } else { // 获取锁失败,休眠后重试(如50ms) Thread.sleep(50); return getProductById(productId); // 递归重试 } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { // 释放锁(仅持有锁的线程可释放) stringRedisTemplate.delete(lockKey); } } }
3. 缓存雪崩
- 问题描述:大量缓存 key 同时过期,或 Redis 服务宕机,导致请求瞬间全部压向数据库,引发数据库崩溃。
- 解决方案:过期时间随机化(避免集中过期)+ 缓存集群(高可用)+ 服务熔断降级。
- Spring Data Redis 实现示例(过期时间随机化):
public class CacheAvalancheDemo { private StringRedisTemplate stringRedisTemplate; // 缓存数据时添加随机过期时间,解决缓存雪崩 public void cacheDataWithRandomExpire(String key, String value) { // 基础过期时间(如1小时)+ 随机时间(0-30分钟),避免大量key同时过期 int baseExpire = 3600; int randomExpire = new Random().nextInt(1800); // 0-1800秒 stringRedisTemplate.opsForValue().set(key, value, baseExpire + randomExpire, TimeUnit.SECONDS); } }
六、总结
- String:简单键值对,适合缓存和计数;
- Hash:对象存储,支持字段级操作;
- List:有序可重复,适合队列和最新列表;
- Set:无序去重,适合交集计算和去重;
- ZSet:有序带权重,适合排序和排行榜。
- 缓存问题需通过穿透(缓存空值)、击穿(互斥锁)、雪崩(随机过期)等方案解决,保障系统稳定性。