Redis基础:数据类型及使用场景
1. 总体概述
Redis是内存数据库,通常被误解为“仅用于缓存”。实际上Redis提供多种数据类型,每种类型都适合特定的问题场景。
这篇文章将帮你掌握Redis最常用的5种数据类型、如何通过Spring Data Redis使用它们,更重要的是:何时应该选择哪种类型。读完后,你将了解如何避免经典错误——“把String用于所有情况”,然后在更新数据时自找麻烦。
2. 环境准备
在项目中添加Spring Data Redis依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.yml中声明连接配置:
spring:
data:
redis:
host: localhost
port: 6379
使用Docker快速运行Redis:
docker run -d --name redis-dev -p 6379:6379 redis:7-alpine
下面的示例使用Spring Boot自动配置的RedisTemplate<String, String>,你无需手动声明bean。
3. String:最简单的类型
String是基本类型,将一个值关联到一个key。值可以是文本、数字或序列化的JSON。
@Service
public class SessionTokenService {
private final RedisTemplate<String, String> redisTemplate;
public SessionTokenService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 保存token
public void saveToken(String userId, String token) {
redisTemplate.opsForValue()
.set("session:" + userId, token, Duration.ofMinutes(30));
}
// 获取token
public String getToken(String userId) {
return redisTemplate.opsForValue().get("session:" + userId);
}
}
set(key, value, ttl):保存token并设置30分钟的生存时间,Redis在过期时自动删除。get(key):如果key不存在或已过期,返回null。
调用getToken获取已过期token时的输出:
null
何时使用: session token、缓存已序列化的对象(JSON)、简单计数器(INCR)、特性开关。String也支持原子递增:
Long visits = redisTemplate.opsForValue().increment("page:views:home");
注意: 不要为了更新1个字段而保存一个大对象并序列化/反序列化整个对象——这表明你应该转向Hash。
4. Hash:具有多个字段的对象
Hash类似于在一个key内的Map<String, String>,允许独立更新每个字段,无需读取/写入整个对象。
@Service
public class UserProfileCache {
private final RedisTemplate<String, String> redisTemplate;
public UserProfileCache(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void saveProfile(String userId, Map<String, String> fields) {
redisTemplate.opsForHash().putAll("user:" + userId, fields);
}
// 更新电子邮件
public void updateEmail(String userId, String newEmail) {
redisTemplate.opsForHash().put("user:" + userId, "email", newEmail);
}
public Map<Object, Object> getProfile(String userId) {
return redisTemplate.opsForHash().entries("user:" + userId);
}
}
putAll:同时写入多个字段,类似HSET。put一个字段:仅更新email字段,其他字段(name、age等)不受影响。entries:获取hash的所有field-value。
与String比较:如果使用String + JSON,每次更改电子邮件时,你必须GET整个JSON、解析、修改、重新序列化,然后SET。使用Hash,只需一个HSET命令。
何时使用: 具有多个字段或独立更新的对象——用户配置文件、产品详情、按租户配置。
5. List:队列和有序历史
List是有序列表,支持在两端进行push/pop,适合队列或保存最近N个项目。
@Service
public class RecentActivityService {
private final RedisTemplate<String, String> redisTemplate;
public RecentActivityService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void logActivity(String userId, String activity) {
String key = "activity:" + userId;
redisTemplate.opsForList().leftPush(key, activity);
// 修剪列表,仅保留前10个元素
redisTemplate.opsForList().trim(key, 0, 9);
}
// 获取最近活动
public List<String> getRecentActivities(String userId) {
return redisTemplate.opsForList().range("activity:" + userId, 0, -1);
}
}
leftPush:将新活动添加到列表开头。trim(key, 0, 9):修剪列表,仅保留前10个元素,防止列表无限增长。range(0, -1):获取所有元素,索引-1表示最后一个元素。
何时使用: 最近的活动时间线、简单的任务队列(结合BLPOP让消费者等待)、撤销堆栈。
注意: 如果你需要按值搜索或频繁随机访问,List不适合——对于长列表,按索引访问的时间复杂度为O(n)。
6. Set:不重复的无序集合
Set存储唯一值,支持快速的集合操作(并集、交集、差集)。
@Service
public class TagService {
private final RedisTemplate<String, String> redisTemplate;
public TagService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void addTags(String articleId, String... tags) {
redisTemplate.opsForSet().add("article:tags:" + articleId, tags);
}
public boolean hasTag(String articleId, String tag) {
return redisTemplate.opsForSet()
.isMember("article:tags:" + articleId, tag);
}
// 共同标签
public Set<String> commonTags(String articleId1, String articleId2) {
return redisTemplate.opsForSet()
.intersect("article:tags:" + articleId1, "article:tags:" + articleId2);
}
}
add:添加多个标签,Redis自动去除重复。isMember:检查存在性,时间复杂度为O(1),比遍历List快得多。intersect:找到两篇文章的共同标签——用于推荐相关文章。
何时使用: 标签/分类、按日期的唯一访问者列表(SADD visitors:2026-10-05 userId)、快速成员检查(例如:用户是否已注册事件)。
7. Sorted Set:按分数排序
Sorted Set(ZSet)类似Set,但每个成员都有一个score用于排序。这是排行榜问题的最强大类型。
@Service
public class LeaderboardService {
private final RedisTemplate<String, String> redisTemplate;
public LeaderboardService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void addScore(String playerId, double score) {
redisTemplate.opsForZSet().add("leaderboard:weekly", playerId, score);
}
// 获取排名
public Long getRank(String playerId) {
// 从0开始计数,不存在时返回null
return redisTemplate.opsForZSet().reverseRank("leaderboard:weekly", playerId);
}
public Set<String> getTop10() {
return redisTemplate.opsForZSet()
.reverseRange("leaderboard:weekly", 0, 9);
}
}
add(key, member, score):添加或更新玩家分数。reverseRank:按分数降序获取排名(最高排名是最高分)。reverseRange(0, 9):获取前10,Redis已自动排序,无需在应用程序中排序。
调用getTop10()时的示例输出:
["playerA", "playerC", "playerB"]
何时使用: 排行榜、优先队列(score = priority)、时间窗口速率限制(score = timestamp)。
8. 使用JUnit 5和Testcontainers进行行为测试
要在不影响真实数据的情况下测试与Redis相关的逻辑,使用Testcontainers。
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class LeaderboardServiceTest {
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@DynamicPropertySource
static void redisProps(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
}
@Autowired
private LeaderboardService leaderboardService;
@Test
void shouldReturnTopPlayersInOrder() {
leaderboardService.addScore("playerA", 100);
leaderboardService.addScore("playerB", 50);
leaderboardService.addScore("playerC", 80);
Set<String> top = leaderboardService.getTop10();
assertThat(top).containsExactly("playerA", "playerC", "playerB");
}
}
@Container为每个测试类在Docker中初始化真实Redis。@DynamicPropertySource覆盖host/port,使Spring Boot连接到正确的容器。containsExactly检查确切顺序——Sorted Set保证结果始终按分数降序排列。
9. 快速对比表
| 类型 | 结构 | 使用场景 |
|---|---|---|
| String | key → value | 缓存对象、计数器、session token |
| Hash | key → field-value map | 多字段对象、部分更新 |
| List | key → 有序列表 | 队列、最近项目、时间线 |
| Set | key → 不重复集合 | 标签、成员检查、集合操作 |
| Sorted Set | key → 带分数成员 | 排行榜、速率限制、优先队列 |
10. 常见错误
- 对频繁部分更新的数据使用String:导致多个线程同时读-改-写JSON时的竞态条件。改用Hash可以原子更新单个字段。
- List不修剪:列表无限增长,浪费RAM,
LRANGE命令随时间变慢。 - 忘记设置TTL:Redis默认不自动删除key,垃圾数据会不断积累。始终为临时数据(如session、cache)考虑
expire。 - 对需要顺序的数据使用Set:Set不保证迭代顺序,如果需要排序,使用Sorted Set。
注意: Redis是内存数据库——所有数据都在RAM中。不要将大型、长期或需要复杂ACID事务的数据(例如:银行账户余额)存储在Redis中作为唯一的真实源。
11. 结论
Redis不是“大缓存”,而是灵活的数据结构工具集。选择正确的数据类型——String用于单个值、Hash用于多字段对象、List用于队列、Set用于成员关系、Sorted Set用于排名——有助于代码更简洁、更少的命令,并利用Redis的原子操作的强大力量,而不是在应用层自己处理这些逻辑。