开发系统时---如何高效缓存呢?
下文笔者讲述开发系统时,高效缓存的方法及示例分享,如下所示
避免缓存大对象
例:反例
@Cacheable(value = "user", key = "#id")
public User getUser(Long id) {
return userDao.findWithAllRelations(id);
}
这里一次查询出了用户及其所有关联对象,然后添加到内存缓存中
当使用id查询用户信息的请求量非常大,会导致频繁的GC
正确使用示例
缓存平凡的小对象
@Cacheable(value = "user_base", key = "#id")
public UserBase getBaseInfo(Long id) { /*...*/ }
@Cacheable(value = "user_detail", key = "#id")
public UserDetail getDetailInfo(Long id) { /*...*/ }
这种情况,需要拆分缓存对象,比如:将用户基本信息和用户详细信息分开缓存
永远设置过期时间
所有的缓存信息,都应设置过期时间
正确配置:
@Cacheable(value = "config", key = "#key",
unless = "#result == null",
cacheManager = "redisCacheManager")
public String getConfig(String key) {
return configDao.get(key);
}
Redis配置如下:
spring.cache.redis.time-to-live=300000 // 5分钟
spring.cache.redis.cache-null-values=false
需要指定key的存活时间,比如:time-to-live设置成5分钟。
TTL设置公式:
最优TTL = 平均数据变更周期 × 0.3
并不是所有的缓存都按照这种公式设置,
我们应采用动态设置缓存时间,如:商品详情页,设置30分钟的缓存时间
空值缓存
在用户请求并发量大的业务场景种
我们需要把空值缓存起来。
防止大批量在系统中不存在的用户id,没有命中缓存,
而直接查询数据库的情况。
例:
public Product getProduct(Long id) {
String key = "product:" + id;
Product product = redis.get(key);
if (product != null) {
if (product.isEmpty()) { // 空对象标识
return null;
}
return product;
}
product = productDao.findById(id);
if (product == null) {
redis.setex(key, 300, "empty"); // 缓存空值5分钟
return null;
}
redis.setex(key, 3600, product);
return product;
}
分布式锁用Redisson
Redisson分布式锁实现:
public Product getProduct(Long id) {
String key = "product:" + id;
Product product = redis.get(key);
if (product == null) {
RLock lock = redisson.getLock("lock:" + key);
try {
if (lock.tryLock(3, 30, TimeUnit.SECONDS)) {
product = productDao.findById(id);
redis.setex(key, 3600, product);
}
} finally {
lock.unlock();
}
}
return product;
}
延迟双删策略
在保证数据库和缓存双写数据一致性的业务场景种, 可使用延迟双删的策略
例
@Transactional
public void updateProduct(Product product) {
// 1. 先删缓存
redis.delete("product:" + product.getId());
// 2. 更新数据库
productDao.update(product);
// 3. 延时再删
executor.schedule(() -> {
redis.delete("product:" + product.getId());
}, 500, TimeUnit.MILLISECONDS);
}
热点数据预加载
对经常使用的热点数据 我们可以提前做数据的预加载
实时监控方案:
// 使用Redis HyperLogLog统计访问频率
public void recordAccess(Long productId) {
String key = "access:product:" + productId;
redis.pfadd(key, UUID.randomUUID().toString());
redis.expire(key, 60); // 统计最近60秒
}
// 定时任务检测热点
@Scheduled(fixedRate = 10000)
public void detectHotKeys() {
Set<String> keys = redis.keys("access:product:*");
keys.forEach(key -> {
long count = redis.pfcount(key);
if (count > 1000) { // 阈值
Long productId = extractId(key);
preloadProduct(productId);
}
});
}
定时任务检测热点,并且更新到缓存中
选择合适的数据结构
反例:
使用String类型存储用户信息
错误用String存储对象:
redis.set("user:123", JSON.toJSONString(user));
每次更新单个字段都需要反序列化整个对象。
导致:
序列化/反序列化开销大
更新单个字段需读写整个对象
内存占用高
正确做法
//使用Hash存储
redis.opsForHash().putAll("user:123", userToMap(user));
//局部更新
redis.opsForHash().put("user:123", "age", "25");
优秀数据结构选择
1.String
计数器
redis.opsForValue().increment("article:123:views");
分布式锁
redis.opsForValue().set("lock:order:456", "1", "NX", "EX", 30);
2.Hash
存储商品信息
Map<String, String> productMap = new HashMap<>();
productMap.put("name", "maomao");
productMap.put("price", "888");
redis.opsForHash().putAll("product:6666", productMap);
部分更新
redis.opsForHash().put("product:6666", "stock", "100");
3.list
消息队列
redis.opsForList().leftPush("queue:payment", orderJson);
最新N条记录
redis.opsForList().trim("user:123:logs", 0, 99);
4.Set
标签系统
redis.opsForSet().add("article:123:tags", "科技", "数码");
共同好友
redis.opsForSet().intersect("user:123:friends", "user:456:friends");
5.ZSet
排行榜
redis.opsForZSet().add("leaderboard", "player1", 2500);
redis.opsForZSet().reverseRange("leaderboard", 0, 9);
延迟队列
redis.opsForZSet().add("delay:queue", "task1", System.currentTimeMillis() + 5000);
版权声明
本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。


