Redis缓存应用全面指南:从基础应用到问题解决的完整方案
Redis缓存基础概念
什么是缓存?
缓存就像是我们生活中的便利店。想象一下,如果每次想买瓶水都要跑到几公里外的超市,那会非常麻烦。便利店就是把常用商品放在离我们更近的地方,方便快速获取。
在计算机系统中:
- 数据库:像大型超市,数据全面但访问慢
- 缓存:像便利店,数据有限但访问快
Redis作为缓存的优势
1. 高性能:
数据库查询:平均100ms
Redis查询: 平均1ms
性能提升: 100倍
2. 丰富的数据结构:
// String: 简单键值对缓存
redisTemplate.opsForValue().set("user:123", userJson);
// Hash: 对象属性缓存
redisTemplate.opsForHash().put("user:123", "name", "John");
// List: 列表数据缓存
redisTemplate.opsForList().rightPush("recent:articles", articleId);
// Zset: 排行榜缓存
redisTemplate.opsForZSet().add("leaderboard", userId, score);
3. 过期机制:
// 自动过期,无需手动清理
redisTemplate.opsForValue().set("session:abc123", sessionData, Duration.ofMinutes(30));
Redis缓存应用场景
1. 数据库查询缓存
这是最常见的缓存应用场景,用于减少数据库查询压力。
用户信息缓存:
@Service
public class UserCacheService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate<String, String> redisTemplate;
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
// 1. 先查缓存
String userJson = redisTemplate.opsForValue().get(cacheKey);
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 2. 缓存未命中,查数据库
User user = userRepository.findById(userId);
if (user != null) {
// 3. 写入缓存,30分钟过期
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user),
Duration.ofMinutes(30));
}
return user;
}
// 更新用户信息
public void updateUser(User user) {
// 1. 更新数据库
userRepository.save(user);
// 2. 删除缓存,下次查询时重新加载
String cacheKey = "user:" + user.getId();
redisTemplate.delete(cacheKey);
}
}
商品信息缓存:
@Service
public class ProductCacheService {
// 商品详情缓存(长时间缓存)
public Product getProductDetail(Long productId) {
String cacheKey = "product:detail:" + productId;
String productJson = redisTemplate.opsForValue().get(cacheKey);
if (productJson != null) {
return JSON.parseObject(productJson, Product.class);
}
Product product = productRepository.findById(productId);
if (product != null) {
// 商品信息变化不频繁,缓存1小时
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(product),
Duration.ofHours(1));
}
return product;
}
// 商品库存缓存(短时间缓存,数据变化频繁)
public Integer getProductStock(Long productId) {
String cacheKey = "product:stock:" + productId;
String stockStr = redisTemplate.opsForValue().get(cacheKey);
if (stockStr != null) {
return Integer.valueOf(stockStr);
}
Integer stock = inventoryService.getStock(productId);
if (stock != null) {
// 库存变化频繁,只缓存30秒
redisTemplate.opsForValue().set(cacheKey, stock.toString(),
Duration.ofSeconds(30));
}
return stock;
}
}
2. 会话缓存
用于存储用户会话信息,支持分布式session。
@Component
public class SessionCacheService {
// 保存用户会话
public void saveSession(String sessionId, UserSession session) {
String key = "session:" + sessionId;
// 使用Hash结构存储会话的多个属性
Map<String, String> sessionMap = new HashMap<>();
sessionMap.put("userId", String.valueOf(session.getUserId()));
sessionMap.put("username", session.getUsername());
sessionMap.put("loginTime", String.valueOf(session.getLoginTime()));
sessionMap.put("lastAccessTime", String.valueOf(System.currentTimeMillis()));
redisTemplate.opsForHash().putAll(key, sessionMap);
redisTemplate.expire(key, Duration.ofHours(2)); // 2小时过期
}
// 获取用户会话
public UserSession getSession(String sessionId) {
String key = "session:" + sessionId;
Map<Object, Object> sessionMap = redisTemplate.opsForHash().entries(key);
if (sessionMap.isEmpty()) {
return null;
}
// 更新最后访问时间并续期
redisTemplate.opsForHash().put(key, "lastAccessTime",
String.valueOf(System.currentTimeMillis()));
redisTemplate.expire(key, Duration.ofHours(2));
// 构建会话对象
UserSession session = new UserSession();
session.setUserId(Long.valueOf((String) sessionMap.get("userId")));
session.setUsername((String) sessionMap.get("username"));
session.setLoginTime(Long.valueOf((String) sessionMap.get("loginTime")));
return session;
}
// 删除会话(用户登出)
public void removeSession(String sessionId) {
redisTemplate.delete("session:" + sessionId);
}
}
3. 计算结果缓存
缓存复杂计算的结果,避免重复计算。
@Service
public class StatisticsCacheService {
// 用户统计数据缓存
public UserStatistics getUserStatistics(Long userId, String date) {
String key = String.format("stats:user:%d:%s", userId, date);
String statsJson = redisTemplate.opsForValue().get(key);
if (statsJson != null) {
return JSON.parseObject(statsJson, UserStatistics.class);
}
// 复杂的统计计算
UserStatistics stats = calculateUserStatistics(userId, date);
if (stats != null) {
// 统计数据相对稳定,缓存6小时
redisTemplate.opsForValue().set(key, JSON.toJSONString(stats),
Duration.ofHours(6));
}
return stats;
}
private UserStatistics calculateUserStatistics(Long userId, String date) {
UserStatistics stats = new UserStatistics();
// 复杂的数据库查询和计算
stats.setPageViews(calculatePageViews(userId, date));
stats.setOrderCount(calculateOrderCount(userId, date));
stats.setTotalAmount(calculateTotalAmount(userId, date));
return stats;
}
}
4. 排行榜缓存
利用Redis的Sorted Set实现排行榜功能。
@Service
public class LeaderboardService {
// 更新用户分数
public void updateUserScore(Long userId, double score) {
String key = "leaderboard:daily:" + LocalDate.now();
redisTemplate.opsForZSet().add(key, "user:" + userId, score);
// 设置排行榜过期时间
redisTemplate.expire(key, Duration.ofDays(7));
}
// 获取排行榜前N名
public List<UserRank> getTopRanking(int limit) {
String key = "leaderboard:daily:" + LocalDate.now();
Set<ZSetOperations.TypedTuple<String>> results =
redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, limit - 1);
return results.stream()
.map(tuple -> new UserRank(tuple.getValue(), tuple.getScore()))
.collect(Collectors.toList());
}
// 获取用户排名
public Long getUserRank(Long userId) {
String key = "leaderboard:daily:" + LocalDate.now();
Long rank = redisTemplate.opsForZSet().reverseRank(key, "user:" + userId);
return rank != null ? rank + 1 : null; // 排名从1开始
}
}
5. 热点数据缓存
对于访问频率极高的数据,使用专门的热点缓存策略。
@Service
public class HotDataCacheService {
// 热点商品缓存
public List<Product> getHotProducts(String category) {
String key = "hot:products:" + category;
String productsJson = redisTemplate.opsForValue().get(key);
if (productsJson != null) {
return JSON.parseArray(productsJson, Product.class);
}
List<Product> hotProducts = productService.getHotProductsByCategory(category);
if (!hotProducts.isEmpty()) {
// 热点数据缓存时间短,但会预热刷新
redisTemplate.opsForValue().set(key, JSON.toJSONString(hotProducts),
Duration.ofMinutes(10));
}
return hotProducts;
}
// 预热热点数据
@Scheduled(fixedRate = 300000) // 每5分钟执行
public void preheatHotData() {
List<String> categories = Arrays.asList("electronics", "clothing", "books");
categories.parallelStream().forEach(category -> {
try {
List<Product> hotProducts = productService.getHotProductsByCategory(category);
String key = "hot:products:" + category;
redisTemplate.opsForValue().set(key, JSON.toJSONString(hotProducts),
Duration.ofMinutes(10));
} catch (Exception e) {
log.error("预热热点数据失败: {}", category, e);
}
});
}
}
缓存常见问题及解决方案
1. 缓存穿透
问题定义:缓存中没有,数据库中也没有的数据被大量查询。
典型场景:
// 恶意查询不存在的用户ID
for (int i = 0; i < 10000; i++) {
getUserById(-i); // 负数ID不存在,每次都查数据库
}
解决方案一:布隆过滤器
@Component
public class BloomFilterService {
private final BloomFilter<Long> userBloomFilter;
public BloomFilterService() {
// 创建布隆过滤器:预期100万元素,1%误判率
this.userBloomFilter = BloomFilter.create(
Funnels.longFunnel(), 1000000, 0.01);
// 初始化:将所有存在的用户ID加入过滤器
initUserBloomFilter();
}
public User getUserById(Long userId) {
// 先检查布隆过滤器
if (!userBloomFilter.mightContain(userId)) {
// 肯定不存在,直接返回null
return null;
}
// 可能存在,继续正常查询流程
return queryUserWithCache(userId);
}
private void initUserBloomFilter() {
List<Long> allUserIds = userRepository.findAllUserIds();
allUserIds.forEach(userBloomFilter::put);
}
}
解决方案二:缓存空值
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
String userJson = redisTemplate.opsForValue().get(cacheKey);
// 检查是否是缓存的空值
if ("NULL".equals(userJson)) {
return null;
}
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 查询数据库
User user = userRepository.findById(userId);
if (user != null) {
// 缓存正常数据
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user),
Duration.ofMinutes(30));
} else {
// 缓存空值,但过期时间要短
redisTemplate.opsForValue().set(cacheKey, "NULL",
Duration.ofMinutes(2));
}
return user;
}
2. 缓存击穿
问题定义:热点数据缓存过期,大量请求同时查询数据库。
解决方案:分布式锁
@Service
public class CacheBreakdownSolution {
public Product getProductById(Long productId) {
String cacheKey = "product:" + productId;
String lockKey = "lock:product:" + productId;
// 1. 查询缓存
String productJson = redisTemplate.opsForValue().get(cacheKey);
if (productJson != null) {
return JSON.parseObject(productJson, Product.class);
}
// 2. 缓存未命中,尝试获取分布式锁
String lockValue = UUID.randomUUID().toString();
Boolean lockAcquired = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
if (lockAcquired) {
try {
// 3. 获得锁,再次检查缓存
productJson = redisTemplate.opsForValue().get(cacheKey);
if (productJson != null) {
return JSON.parseObject(productJson, Product.class);
}
// 4. 查询数据库并缓存
Product product = productRepository.findById(productId);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey,
JSON.toJSONString(product), Duration.ofMinutes(30));
}
return product;
} finally {
// 5. 释放锁
releaseLock(lockKey, lockValue);
}
} else {
// 6. 未获得锁,等待后重试
try {
Thread.sleep(50);
return getProductById(productId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
}
private void releaseLock(String lockKey, String lockValue) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), lockValue);
}
}
3. 缓存雪崩
问题定义:大量缓存同时失效,请求都打到数据库。
解决方案一:随机过期时间
public void cacheUserWithRandomExpire(User user) {
String cacheKey = "user:" + user.getId();
// 基础过期时间30分钟,随机增加0-10分钟
int baseExpireMinutes = 30;
int randomMinutes = new Random().nextInt(10);
int totalExpireMinutes = baseExpireMinutes + randomMinutes;
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user),
Duration.ofMinutes(totalExpireMinutes));
}
解决方案二:多级缓存
@Component
public class MultiLevelCacheService {
// L1缓存:本地缓存
private final LoadingCache<String, String> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(Duration.ofMinutes(5))
.build(key -> null);
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 查询本地缓存
String productJson = localCache.getIfPresent(cacheKey);
if (productJson != null) {
return JSON.parseObject(productJson, Product.class);
}
// 2. 查询Redis缓存
productJson = redisTemplate.opsForValue().get(cacheKey);
if (productJson != null) {
localCache.put(cacheKey, productJson);
return JSON.parseObject(productJson, Product.class);
}
// 3. 查询数据库
Product product = productRepository.findById(productId);
if (product != null) {
productJson = JSON.toJSONString(product);
// 写入两级缓存
redisTemplate.opsForValue().set(cacheKey, productJson,
Duration.ofMinutes(30));
localCache.put(cacheKey, productJson);
}
return product;
}
}
4. 缓存更新策略
策略一:先更新数据库,再删除缓存
@Transactional
public void updateUser(User user) {
// 1. 更新数据库
userRepository.save(user);
// 2. 删除缓存
String cacheKey = "user:" + user.getId();
redisTemplate.delete(cacheKey);
}
策略二:基于消息队列的异步更新
@Component
public class CacheUpdateService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Transactional
public void updateUserData(User user) {
// 1. 更新数据库
userRepository.save(user);
// 2. 发送缓存更新消息
CacheUpdateMessage message = new CacheUpdateMessage();
message.setKey("user:" + user.getId());
message.setOperation("UPDATE");
rabbitTemplate.convertAndSend("cache.update", message);
}
@RabbitListener(queues = "cache.update")
public void handleCacheUpdate(CacheUpdateMessage message) {
// 删除缓存,让下次查询时重新加载
redisTemplate.delete(message.getKey());
}
}
高级缓存应用
1. 分布式锁
@Component
public class RedisDistributedLock {
public boolean tryLock(String lockKey, String lockValue, long expireTime) {
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, Duration.ofSeconds(expireTime));
return Boolean.TRUE.equals(result);
}
public void releaseLock(String lockKey, String lockValue) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), lockValue);
}
// 使用示例:防止重复提交
@PostMapping("/order")
public ResponseEntity<String> createOrder(@RequestBody OrderRequest request,
HttpServletRequest httpRequest) {
String userId = getCurrentUserId();
String lockKey = "order:create:" + userId;
String lockValue = UUID.randomUUID().toString();
if (tryLock(lockKey, lockValue, 10)) {
try {
// 创建订单的业务逻辑
orderService.createOrder(request);
return ResponseEntity.ok("订单创建成功");
} finally {
releaseLock(lockKey, lockValue);
}
} else {
return ResponseEntity.badRequest().body("请勿重复提交");
}
}
}
2. 限流器
@Component
public class RateLimiter {
// 滑动窗口限流
public boolean isAllowed(String key, int limit, int windowSeconds) {
long now = System.currentTimeMillis();
long windowStart = now - windowSeconds * 1000L;
String script =
"redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +
"local count = redis.call('zcard', KEYS[1]) " +
"if count < tonumber(ARGV[2]) then " +
" redis.call('zadd', KEYS[1], ARGV[3], ARGV[3]) " +
" redis.call('expire', KEYS[1], ARGV[4]) " +
" return 1 " +
"else " +
" return 0 " +
"end";
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
String.valueOf(windowStart),
String.valueOf(limit),
String.valueOf(now),
String.valueOf(windowSeconds)
);
return Long.valueOf(1).equals(result);
}
}
// 使用示例
@RestController
public class ApiController {
@GetMapping("/api/data")
public ResponseEntity<Object> getData(HttpServletRequest request) {
String clientIp = getClientIp(request);
String rateLimitKey = "rate_limit:" + clientIp;
// 每分钟最多100次请求
if (!rateLimiter.isAllowed(rateLimitKey, 100, 60)) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body("请求过于频繁,请稍后再试");
}
return ResponseEntity.ok(businessService.getData());
}
}
缓存监控和运维
1. 缓存命中率监控
@Component
public class CacheMetrics {
private final AtomicLong hitCount = new AtomicLong(0);
private final AtomicLong missCount = new AtomicLong(0);
public void recordHit() {
hitCount.incrementAndGet();
}
public void recordMiss() {
missCount.incrementAndGet();
}
public double getHitRate() {
long hits = hitCount.get();
long misses = missCount.get();
long total = hits + misses;
return total == 0 ? 0.0 : (double) hits / total;
}
// 定期输出监控指标
@Scheduled(fixedRate = 60000) // 每分钟
public void reportMetrics() {
double hitRate = getHitRate();
log.info("缓存命中率: {:.2f}%, 命中次数: {}, 未命中次数: {}",
hitRate * 100, hitCount.get(), missCount.get());
}
}
2. 缓存健康检查
@Component
public class CacheHealthCheck {
@EventListener
@Async
public void checkCacheHealth() {
try {
// 测试Redis连接
String testKey = "health:check:" + System.currentTimeMillis();
redisTemplate.opsForValue().set(testKey, "OK", Duration.ofSeconds(10));
String result = redisTemplate.opsForValue().get(testKey);
redisTemplate.delete(testKey);
if ("OK".equals(result)) {
log.info("缓存健康检查通过");
} else {
log.error("缓存健康检查失败:读写测试不通过");
// 发送告警
alertService.sendAlert("Redis缓存读写测试失败");
}
} catch (Exception e) {
log.error("缓存健康检查异常", e);
alertService.sendAlert("Redis缓存连接异常: " + e.getMessage());
}
}
}
3. 缓存清理策略
@Component
public class CacheCleanupService {
@Scheduled(fixedDelay = 300000) // 每5分钟
public void cleanupExpiredCache() {
try {
// 清理过期的临时缓存
cleanupTempCache();
// 清理过期的统计缓存
cleanupStatisticsCache();
} catch (Exception e) {
log.error("缓存清理失败", e);
}
}
private void cleanupTempCache() {
Set<String> tempKeys = redisTemplate.keys("temp:*");
if (tempKeys != null && !tempKeys.isEmpty()) {
// 检查哪些key已经过期但还没被Redis清理
for (String key : tempKeys) {
Long ttl = redisTemplate.getExpire(key);
if (ttl != null && ttl < 0) {
redisTemplate.delete(key);
}
}
}
}
// 手动清理指定模式的缓存
public int cleanupCacheByPattern(String pattern) {
Set<String> keys = redisTemplate.keys(pattern);
if (keys != null && !keys.isEmpty()) {
Long deleted = redisTemplate.delete(keys);
log.info("清理缓存完成,模式: {}, 清理数量: {}", pattern, deleted);
return deleted.intValue();
}
return 0;
}
}
缓存最佳实践
1. 缓存设计原则
选择合适的过期时间:
// 根据数据特性设置不同的过期时间
public class CacheExpireStrategy {
// 用户基本信息:变化不频繁,缓存时间长
public static final Duration USER_INFO_EXPIRE = Duration.ofHours(2);
// 商品库存:变化频繁,缓存时间短
public static final Duration PRODUCT_STOCK_EXPIRE = Duration.ofSeconds(30);
// 配置信息:很少变化,缓存时间很长
public static final Duration CONFIG_EXPIRE = Duration.ofDays(1);
// 统计数据:定时计算,缓存到下次计算时间
public static final Duration STATISTICS_EXPIRE = Duration.ofHours(6);
}
合理的键命名规范:
public class CacheKeyBuilder {
private static final String SEPARATOR = ":";
public static String userKey(Long userId) {
return "user" + SEPARATOR + userId;
}
public static String userProfileKey(Long userId) {
return "user" + SEPARATOR + "profile" + SEPARATOR + userId;
}
public static String productKey(Long productId) {
return "product" + SEPARATOR + productId;
}
public static String sessionKey(String sessionId) {
return "session" + SEPARATOR + sessionId;
}
}
2. 性能优化建议
批量操作:
// 批量获取用户信息
public Map<Long, User> batchGetUsers(List<Long> userIds) {
// 1. 批量构建缓存key
List<String> cacheKeys = userIds.stream()
.map(id -> "user:" + id)
.collect(Collectors.toList());
// 2. 批量从Redis获取
List<String> cachedValues = redisTemplate.opsForValue().multiGet(cacheKeys);
Map<Long, User> result = new HashMap<>();
List<Long> missedIds = new ArrayList<>();
// 3. 处理缓存结果
for (int i = 0; i < userIds.size(); i++) {
String cachedValue = cachedValues.get(i);
if (cachedValue != null) {
result.put(userIds.get(i), JSON.parseObject(cachedValue, User.class));
} else {
missedIds.add(userIds.get(i));
}
}
// 4. 批量查询未命中的数据
if (!missedIds.isEmpty()) {
List<User> users = userRepository.findByIdIn(missedIds);
// 5. 批量写入缓存
Map<String, String> cacheData = new HashMap<>();
for (User user : users) {
cacheData.put("user:" + user.getId(), JSON.toJSONString(user));
result.put(user.getId(), user);
}
if (!cacheData.isEmpty()) {
redisTemplate.opsForValue().multiSet(cacheData);
// 批量设置过期时间
cacheData.keySet().forEach(key ->
redisTemplate.expire(key, Duration.ofMinutes(30)));
}
}
return result;
}
Pipeline使用:
public void batchUpdateUserScores(Map<Long, Double> userScores) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (Map.Entry<Long, Double> entry : userScores.entrySet()) {
String key = "leaderboard:daily";
String member = "user:" + entry.getKey();
connection.zAdd(key.getBytes(), entry.getValue(), member.getBytes());
}
return null;
});
}
总结
Redis作为缓存的应用是现代高性能系统的核心组件:
核心应用场景:
- 数据库查询缓存:减少数据库压力,提升查询性能
- 会话缓存:支持分布式session,提升用户体验
- 计算结果缓存:避免重复计算,节省CPU资源
- 排行榜缓存:实时排序功能,支持高并发访问
关键问题解决:
- 缓存穿透:布隆过滤器 + 缓存空值 + 参数校验
- 缓存击穿:分布式锁 + 预热机制 + 永不过期策略
- 缓存雪崩:随机过期 + 多级缓存 + 集群部署
最佳实践要点:
- 根据数据特性设置合适的过期时间
- 建立规范的键命名体系
- 实施完善的监控和告警机制
- 使用批量操作提升性能
- 设计合理的缓存更新策略
通过合理应用Redis缓存,可以显著提升系统性能,改善用户体验,为高并发业务提供强有力的支撑。