Redis作为高性能的键值存储系统,其核心优势在于丰富的数据结构支持。传统学习方式往往停留在命令记忆层面,而本文将带你通过构建简易博客系统,深入理解五大核心数据结构在真实场景中的应用。
String:最简单的数据结构,但功能远超你的想象。它不仅是存储文本的工具,更是实现计数器、缓存、分布式锁的基础。在博客系统中,String可完美胜任文章内容缓存、阅读量统计等场景。
Hash:字段映射表结构,特别适合存储对象。相比将整个对象序列化为String,Hash允许字段级操作,大幅提升效率。我们将用它存储用户资料和文章元数据。
List:双向链表结构,支持两端操作。在博客场景中,它既能实现最新评论列表,又能作为轻量级消息队列处理后台任务。
Set:无序唯一集合,提供高效的成员存在性检查。我们将利用其去重特性实现文章标签系统和用户关注关系。
ZSet:带分数的有序集合,是排行榜功能的天然解决方案。博客文章热度排行、用户贡献榜等场景都离不开它。
xml复制<!-- pom.xml关键依赖 -->
<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>
默认的RedisTemplate使用JDK序列化,可读性差且性能不佳。我们需要定制化配置:
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用String序列化key
template.setKeySerializer(RedisSerializer.string());
// 使用JSON序列化value
template.setValueSerializer(RedisSerializer.json());
// Hash结构序列化配置
template.setHashKeySerializer(RedisSerializer.string());
template.setHashValueSerializer(RedisSerializer.json());
template.afterPropertiesSet();
return template;
}
}
yaml复制# application.yml
spring:
redis:
host: localhost
port: 6379
lettuce:
pool:
max-active: 20 # 最大连接数
max-idle: 10 # 最大空闲连接
min-idle: 5 # 最小空闲连接
max-wait: 2000ms # 获取连接最大等待时间
阅读量统计:原子性操作保证计数准确
java复制public class ArticleService {
private final RedisTemplate<String, String> redisTemplate;
// 增加阅读量
public Long incrementViewCount(Long articleId) {
String key = "article:view:" + articleId;
return redisTemplate.opsForValue().increment(key);
}
// 获取阅读量
public Long getViewCount(Long articleId) {
String key = "article:view:" + articleId;
String count = redisTemplate.opsForValue().get(key);
return count != null ? Long.parseLong(count) : 0L;
}
}
文章内容缓存:缓解数据库压力
java复制public Article getArticleWithCache(Long id) {
String cacheKey = "article:content:" + id;
Article article = (Article) redisTemplate.opsForValue().get(cacheKey);
if (article == null) {
article = articleRepository.findById(id).orElse(null);
if (article != null) {
redisTemplate.opsForValue().set(
cacheKey,
article,
30, TimeUnit.MINUTES); // 缓存30分钟
}
}
return article;
}
用户资料存储:字段级操作提升效率
java复制public void saveUserProfile(User user) {
String key = "user:profile:" + user.getId();
Map<String, String> profileMap = new HashMap<>();
profileMap.put("username", user.getUsername());
profileMap.put("avatar", user.getAvatar());
profileMap.put("bio", user.getBio());
redisTemplate.opsForHash().putAll(key, profileMap);
}
public User getUserProfile(Long userId) {
String key = "user:profile:" + userId;
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
if (entries.isEmpty()) return null;
User user = new User();
user.setId(userId);
user.setUsername((String) entries.get("username"));
user.setAvatar((String) entries.get("avatar"));
user.setBio((String) entries.get("bio"));
return user;
}
最新评论列表:LPUSH实现时间序排列
java复制public void addComment(Comment comment) {
String key = "article:comments:" + comment.getArticleId();
redisTemplate.opsForList().leftPush(key, comment);
// 保持列表长度,只保留最新100条
redisTemplate.opsForList().trim(key, 0, 99);
}
public List<Comment> getRecentComments(Long articleId, int count) {
String key = "article:comments:" + articleId;
return redisTemplate.opsForList()
.range(key, 0, count - 1)
.stream()
.map(o -> (Comment) o)
.collect(Collectors.toList());
}
文章标签管理:集合运算实现复杂查询
java复制public void addTagsToArticle(Long articleId, Set<String> tags) {
String key = "article:tags:" + articleId;
redisTemplate.opsForSet().add(key, tags.toArray());
}
public Set<String> getCommonTags(Long articleId1, Long articleId2) {
String key1 = "article:tags:" + articleId1;
String key2 = "article:tags:" + articleId2;
return redisTemplate.opsForSet()
.intersect(key1, key2)
.stream()
.map(o -> (String) o)
.collect(Collectors.toSet());
}
热度排行榜:分数自动排序
java复制public void incrementArticleScore(Long articleId, double delta) {
String key = "article:hot";
redisTemplate.opsForZSet().incrementScore(key, articleId.toString(), delta);
}
public List<Article> getTopArticles(int count) {
String key = "article:hot";
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, count - 1);
return tuples.stream()
.map(tuple -> {
Article article = articleRepository.findById(Long.parseLong(tuple.getValue()))
.orElse(null);
if (article != null) {
article.setHotScore(tuple.getScore());
}
return article;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
java复制public boolean tryLock(String lockKey, String requestId, long expireTime) {
return redisTemplate.opsForValue().setIfAbsent(
lockKey,
requestId,
expireTime,
TimeUnit.MILLISECONDS
);
}
public boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
requestId
);
return result != null && result == 1;
}
java复制public void delayTask(String taskId, long delayTime) {
String key = "delay:queue";
redisTemplate.opsForZSet().add(
key,
taskId,
System.currentTimeMillis() + delayTime
);
}
public void processDelayedTasks() {
String key = "delay:queue";
long now = System.currentTimeMillis();
Set<String> taskIds = redisTemplate.opsForZSet()
.rangeByScore(key, 0, now);
if (!taskIds.isEmpty()) {
redisTemplate.opsForZSet().removeRangeByScore(key, 0, now);
taskIds.forEach(this::processTask);
}
}
java复制public void batchUpdateViewCount(Map<Long, Long> articleViews) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
StringRedisConnection stringConn = (StringRedisConnection) connection;
articleViews.forEach((articleId, count) -> {
String key = "article:view:" + articleId;
stringConn.set(key, count.toString());
});
return null;
});
}
java复制public boolean updateIfGreater(String key, long newValue) {
String script =
"local current = tonumber(redis.call('get', KEYS[1]) or 0)\n" +
"if newValue > current then\n" +
" redis.call('set', KEYS[1], ARGV[1])\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
String.valueOf(newValue)
);
return result != null && result == 1;
}
java复制public Article getArticleWithNullCache(Long id) {
String cacheKey = "article:" + id;
Article article = (Article) redisTemplate.opsForValue().get(cacheKey);
// 特殊值缓存标识空结果
if ("NULL".equals(article)) {
return null;
}
if (article == null) {
article = articleRepository.findById(id).orElse(null);
if (article == null) {
// 缓存空结果,短时间过期
redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(cacheKey, article, 30, TimeUnit.MINUTES);
}
}
return article;
}
java复制// 使用Redis的monitor命令分析热点Key
// 或实现简单的访问计数
public Object handleHotKey(String key, Supplier<Object> loader) {
String counterKey = "hotkey:counter:" + key;
Long count = redisTemplate.opsForValue().increment(counterKey);
// 重置计数器
redisTemplate.expire(counterKey, 1, TimeUnit.MINUTES);
if (count != null && count > 1000) { // 阈值判断
// 触发热点Key处理逻辑
return getFromLocalCache(key, loader);
}
return loader.get();
}
| 指标名称 | 监控频率 | 告警阈值 | 应对措施 |
|---|---|---|---|
| 内存使用率 | 每分钟 | >80% | 扩容或清理无用Key |
| 连接数 | 每分钟 | >最大连接数80% | 检查连接泄漏或扩容 |
| 命中率 | 每小时 | <90% | 检查缓存策略有效性 |
| 网络输入/输出流量 | 每分钟 | 持续高流量 | 分析是否遭受攻击 |
bash复制# 内存分析
redis-cli --bigkeys
redis-cli --memkeys
# 慢查询分析
redis-cli slowlog get 10
# 客户端连接分析
redis-cli client list
在实际开发中,Redis数据结构的选择往往需要权衡多种因素。曾经在实现用户动态时间线功能时,最初考虑使用List存储,但在用户量激增后发现内存占用过高。最终改用ZSet结合分数排序,不仅节省了30%内存,还获得了按时间范围查询的额外能力。
另一个教训来自缓存雪崩事故。某次大促期间,大量Key同时过期导致数据库瞬时压力暴增。现在我们采用基础过期时间加随机偏移量的策略,如:30 + random.nextInt(10)分钟,有效分散了缓存重建压力。