背景
在现代分布式系统中,缓存是提升系统性能的重要手段。传统的本地缓存(如Java的LinkedHashMap)虽然性能优秀,但在分布式环境下存在数据一致性和共享性问题。而Redis作为高性能的内存数据库,天然支持分布式部署,成为了实现分布式缓存的理想选择。
然而,内存资源是有限的,当数据量超过物理内存容量时,就必须通过某种机制来释放空间,这就是内存淘汰策略存在的根本原因。没有智能的内存淘汰机制,Redis可能会因为内存不足而频繁崩溃或性能急剧下降,进而影响整个系统的稳定性和响应速度。 目前Redis主要支持三种内存淘汰策略:LRU(最近最少使用)、LFU(最不经常使用)和TTL(过期时间优先)。
业务开发中用的最多的场景就是TTL。Redis可以设置键值的过期时间,对于长时间没用的键,在有效期时间之后会自动删除,但是对于ZSet,Hash等结构,只能对整个键设置过期时间,里面的元素是没法设置过期时间的,而随着时间的增长,ZSet和Hash里面的元素越来越多,很多业务场景下只需要访问最近的元素数据(比如记录用户最近行为记录,最近历史会话记录,用户最近发送的消息,商品推荐和最近商品浏览历史等),如果存储所有数据会造成内存空间的浪费,因此可以利用LRU算法将redis ZSet/hash里面的非最近元素给淘汰掉。
技术设计
核心设计思路
这里我们以Redis的**有序集合(Sorted Set)**作为底层数据结构来实现LRU算法:
- 成员(Member):存储序列化后的缓存对象JSON字符串
- 分数(Score):使用时间戳作为排序依据,分数越大表示越新
架构设计
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Application │───▶│ RedisLruCache │───▶│ Redis │
│ │ │ │ │ Sorted Set │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌──────────────┐
│ ObjectMapper │
│ (JSON序列化) │
└──────────────┘
主要特性
- 泛型支持:支持任意类型的对象缓存
- 自动淘汰:当缓存达到最大容量时,自动移除最旧的数据
- 事务保证:使用Redis事务确保添加和淘汰操作的原子性
关键代码
1. 核心类结构
@Slf4j
public class RedisLruCache<T> {
private final RedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
public RedisLruCache(RedisTemplate redisTemplate, ObjectMapper objectMapper) {
this.redisTemplate = redisTemplate;
this.objectMapper = objectMapper;
}
}
2. 添加元素到LRU缓存
public void addToLruCache(String cacheKey, T item, int maxSize, Function<T, Double> scoreExtractor) {
try {
Double score = scoreExtractor.apply(item);
String itemJson = objectMapper.writeValueAsString(item);
// 先获取当前大小(在事务外)
Long currentSize = redisTemplate.opsForZSet().zCard(cacheKey);
// 使用Redis事务确保原子性
redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.multi();
// 添加新元素
operations.opsForZSet().add(cacheKey, itemJson, score);
// 如果当前大小+1超过最大大小,移除最旧的元素
if (currentSize != null && currentSize >= maxSize) {
long removeCount = currentSize + 1 - maxSize;
operations.opsForZSet().removeRange(cacheKey, 0, removeCount - 1);
}
// 设置过期时间(可选)
operations.expire(cacheKey, Duration.ofDays(7));
return operations.exec();
}
});
} catch (Exception ex) {
log.error("添加数据到redis LRU cache异常,cacheKey: {},异常信息: {}", cacheKey, ex.getMessage(), ex);
throw new RuntimeException("添加数据到redis LRU cache失败", ex);
}
}
其中scoreExtractor用于计算item元素的Score的方法。
3. 获取最近的N条记录
public List<T> getRecentItems(String cacheKey, int count, Class<T> clazz) {
try {
// 获取最新的count条记录(按分数倒序)
Set<Object> objItems = redisTemplate.opsForZSet().reverseRange(cacheKey, 0, count - 1);
if (CollectionUtils.isEmpty(objItems)) {
return Collections.emptyList();
}
Set<String> items = objItems.stream()
.map(ConvertUtils::obj2String)
.collect(Collectors.toSet());
return items.stream()
.map(itemJson -> {
try {
return objectMapper.readValue(itemJson, clazz);
} catch (Exception e) {
log.error("Error deserializing item from cache: {}", itemJson, e);
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (Exception e) {
log.error("Error getting recent items from cache: {}", cacheKey, e);
return Collections.emptyList();
}
}
使用示例
1. 配置和初始化
@Configuration
public class CacheConfig {
@Bean
public RedisLruCache<UserActivity> userActivityCache(
RedisTemplate<String, Object> redisTemplate,
ObjectMapper objectMapper) {
return new RedisLruCache<>(redisTemplate, objectMapper);
}
}
2. 用户活动缓存示例
@Service
public class UserActivityService {
@Autowired
private RedisLruCache<UserActivity> userActivityCache;
// 记录用户活动
public void recordUserActivity(Long userId, UserActivity activity) {
String cacheKey = "user:activity:" + userId;
// 使用活动时间戳作为分数
userActivityCache.addToLruCache(
cacheKey,
activity,
100, // 最多保存100条活动记录
UserActivity::getTimestamp // 分数提取器
);
}
// 获取用户最近的活动
public List<UserActivity> getRecentActivities(Long userId, int count) {
String cacheKey = "user:activity:" + userId;
return userActivityCache.getRecentItems(cacheKey, count, UserActivity.class);
}
}
3. 商品浏览历史示例
@Service
public class ProductViewService {
@Autowired
private RedisLruCache<ProductView> productViewCache;
// 记录商品浏览
public void recordProductView(Long userId, Long productId) {
String cacheKey = "user:product:view:" + userId;
ProductView view = new ProductView(productId, System.currentTimeMillis());
productViewCache.addToLruCache(
cacheKey,
view,
50, // 最多保存50个浏览记录
ProductView::getViewTime
);
}
// 获取用户浏览历史
public List<ProductView> getUserViewHistory(Long userId) {
String cacheKey = "user:product:view:" + userId;
return productViewCache.getRecentItems(cacheKey, 20, ProductView.class);
}
}
4. 实体类定义
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserActivity {
private Long userId;
private String activityType;
private String description;
private Long timestamp;
// timestamp作为LRU排序的分数
public Double getTimestamp() {
return timestamp.doubleValue();
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProductView {
private Long productId;
private Long viewTime;
public Double getViewTime() {
return viewTime.doubleValue();
}
}
优化建议
- 这里的LRU使用的是固定元素数量淘汰的方法,还可以使用最近多长时间淘汰的方法;
- 业务开发中还会面临LFU算法的使用场景。