一、需求背景
基于缓存实现一个商品最近搜索记录功能,需求背景:存在一个商品搜索功能,用户每次搜索商品需要把搜索过的商品记录缓存起来,缓存信息包括商品ID、商品名称,注意:一次搜索可以批量搜索多个商品,考虑到占用Redis
内存空间问题,每个用户存储商品key
的大小不超过10个。
关键点包括:
- 使用
Java
和Redis
实现最近搜索记录。 - 每次搜索可能涉及多个商品,需要批量处理。
- 每个用户的记录不超过10个,避免内存过大。
- 需要存储商品ID和名称,可能涉及数据结构的选择。
二、需求分析
最新搜索过的商品需要排在前面,基于这个前提可以使用Redis
的Sorted Set(ZSet)
来存储浏览记录,并结合时间戳排序。这在搜索结果中也有类似案例,比如使用ZSet
存储用户ID和商品ID,通过时间戳作为score
来实现排序,这种方法可以自动维护有序性,方便管理最近的数据。
接下来考虑数据结构的选择。每个用户的搜索记录需要存储商品ID和名称,可能需要使用Hash
结构来存储商品详情,而使用ZSet
来维护最近搜索的顺序。例如,用户A的最近搜索商品ID存储在ZSet
中,score
为时间戳,而商品的具体信息存在Hash
中,这样避免重复存储名称,节省内存。
关于限制每个用户的记录数量,可以在每次添加后检查ZSet
的长度,如果超过10个,就移除最旧的记录(即score
最小的)。这可以通过ZREMRANGEBYRANK
命令来实现,保留最新的10个。
对于批量搜索的处理,需要将多个商品一次性添加到ZSet
中,并更新它们的score
为当前时间。同时,需要确保重复的商品不会被多次存储,或者在添加时移除旧记录再添加新的,保持唯一性。
获取最近记录时,从ZSet
中取出最新的10个ID,再从Hash
中获取详细信息。还需要考虑并发情况,比如同一用户同时进行多次搜索,可能需要加锁或者使用原子操作来保证数据一致性。不过,Redis本身是单线程的,大部分命令是原子性的,可能不需要额外处理。
三、技术方案设计
-
数据结构选择
- 使用
ZSET
结构存储用户搜索记录(用户ID为key
,商品ID为value
,时间戳为score
) - 使用
HASH
结构存储商品详情(商品ID为key,商品名称等为field
)
- 使用
-
核心逻辑
- 每次搜索时更新
ZSET
中的时间戳 - 控制
ZSET
最大长度不超过10 - 批量处理商品ID时保证原子性操作
- 每次搜索时更新
总结实现步骤:
- 使用
ZSet
存储用户最近搜索的商品ID,按时间排序。 - 使用
Hash
存储商品ID和名称的映射。 - 每次搜索批量添加商品,更新
ZSet
的score
为当前时间。 - 维护
ZSet
长度不超过10,移除旧记录。 - 提供获取最近搜索记录的方法,从
ZSet
和Hash
中联合查询。
四、代码实现
RedisTemplate配置
arduino
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
服务层实现
typescript
@Slf4j
@Component
public class LatestSearchUtils {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 存储搜索记录(批量)
public void addSearchHistory(String userId, List<ZbProduct> zbProducts) {
String zsetKey = "lastest:search:history:" + userId;
String hashKey = "zbproduct:info";
// 批量更新ZSET和HASH
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
long now = System.currentTimeMillis();
for (ZbProduct product : zbProducts) {
// 更新ZSET(时间倒序)
redisTemplate.opsForZSet().add(zsetKey, product.getId(), now--);
// 存储商品详情
redisTemplate.opsForHash().put(hashKey, product.getId(), product.getName());
}
// 修剪ZSET保留最新10条
redisTemplate.opsForZSet().removeRange(zsetKey, 0, -11);
return null;
});
}
// 获取最近搜索记录
public List<ZbProduct> getRecentHistory(String userId) {
String zsetKey = "lastest:search:history:" + userId;
String hashKey = "zbproduct:info";
Set<Object> productIds = redisTemplate.opsForZSet().reverseRange(zsetKey, 0, 9);
return redisTemplate.executePipelined((RedisCallback<ZbProduct>) connection -> {
for (Object id : productIds) {
String name = (String) redisTemplate.opsForHash().get(hashKey, id);
if(name != null) {
return new ZbProduct(id.toString(), name);
}
}
return null;
});
}
}
// 商品实体类
@Data
@AllArgsConstructor
class ZbProduct {
private String id;
private String name;
}
五、关键实现逻辑说明
-
ZSET时间戳倒序
- 使用递减时间戳(
now--
)实现相同批次商品按输入顺序存储。 reverseRange
方法实现从新到旧排序读取。
- 使用递减时间戳(
-
内存控制策略
removeRange
修剪超过10条的旧记录。- 商品详情统一存储HASH,避免重复存储名称。
-
性能优化
- 使用管道(
pipelined
)提升批量操作性能。 - 商品详情分离存储,支持多业务复用。
- 使用管道(