在SpringCloud构建的微服务体系中,Redis作为高性能缓存中间件被广泛应用。然而,随着系统规模扩大和访问量增长,Redis缓存面临着诸如缓存穿透、雪崩、一致性等一系列挑战。本文将深入分析这些问题的成因,并结合具体代码示例给出解决方案,同时探讨Redis在云原生环境下的最新实践。
缓存穿透指查询一个不存在的数据,导致请求直接穿透缓存访问数据库,在高并发下可能压垮数据库。常见场景包括:
public User getUser(Long id) {
// 从Redis获取数据
String key = "user:" + id;
ValueOperations<String, String> ops = redisTemplate.opsForValue();
// 使用RedisTemplate的execute方法处理异常
String userJson = redisTemplate.execute((RedisCallback<String>) connection -> {
try {
return ops.get(key);
} catch (Exception e) {
log.error("Failed to get data from Redis", e);
// 发生异常时返回null,走数据库查询
return null;
}
});
// 缓存为空
if (userJson == null) {
// 分布式锁防止缓存击穿
String lockKey = "lock:user:" + id;
try {
if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS)) {
// 双重检查
userJson = ops.get(key);
if (userJson == null) {
// 从数据库查询
User user = userDao.selectById(id);
// 即使数据为空也缓存,设置较短过期时间
if (user == null) {
ops.set(key, "null", 5, TimeUnit.MINUTES);
} else {
ops.set(key, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
}
return user;
}
} else {
// 未获取到锁,等待后重试
Thread.sleep(100);
return getUser(id);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Interrupted while waiting for lock", e);
return null;
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
// 处理缓存中的空值
return "null".equals(userJson) ? null : JSON.parseObject(userJson, User.class);
}
// 使用Redisson实现分布式布隆过滤器
@Service
public class BloomFilterService {
@Autowired
private RedissonClient redissonClient;
private RBloomFilter<Long> userBloomFilter;
@PostConstruct
public void init() {
// 初始化布隆过滤器,预计元素数量100万,误判率0.01%
userBloomFilter = redissonClient.getBloomFilter("userBloomFilter");
userBloomFilter.tryInit(1000000, 0.0001);
}
public void addUserId(Long userId) {
userBloomFilter.add(userId);
}
public boolean mightContain(Long userId) {
return userBloomFilter.contains(userId);
}
}
// 在查询服务中使用布隆过滤器
public User getUser(Long id) {
// 布隆过滤器校验
if (!bloomFilterService.mightContain(id)) {
log.warn("Bloom filter rejected request for non-existent user: {}", id);
return null;
}
// 正常缓存查询流程
// ...
}
缓存雪崩指大量缓存在同一时间过期,导致请求全部转向数据库,引发数据库压力骤增甚至崩溃。常见诱因包括:
public void setCache(String key, Object value, long baseExpire, long randomRange, TimeUnit timeUnit) {
// 生成随机过期时间,基础时间 + 随机增量
long randomExpire = ThreadLocalRandom.current().nextLong(0, randomRange);
long totalExpire = baseExpire + randomExpire;
// 使用Redis的SETEX命令设置带过期时间的缓存
redisTemplate.execute((RedisCallback<Void>) connection -> {
byte[] keyBytes = redisTemplate.getKeySerializer().serialize(key);
byte[] valueBytes = redisTemplate.getValueSerializer().serialize(value);
connection.setEx(keyBytes, timeUnit.toSeconds(totalExpire), valueBytes);
return null;
});
}
// 使用示例
public void cacheUser(User user) {
// 基础30分钟,随机增加0-10分钟
setCache("user:" + user.getId(), user, 30, 10, TimeUnit.MINUTES);
}
// Caffeine本地缓存 + Redis分布式缓存
@Service
public class UserService {
private final LoadingCache<Long, User> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(this::loadUserFromRedis);
private User loadUserFromRedis(Long id) {
String key = "user:" + id;
String userJson = redisTemplate.opsForValue