如何基于缓存设计实现一个商品最近搜索记录功能

一、需求背景

基于缓存实现一个商品最近搜索记录功能,需求背景:存在一个商品搜索功能,用户每次搜索商品需要把搜索过的商品记录缓存起来,缓存信息包括商品ID、商品名称,注意:一次搜索可以批量搜索多个商品,考虑到占用Redis内存空间问题,每个用户存储商品key的大小不超过10个。

关键点包括:

  • 使用JavaRedis实现最近搜索记录。
  • 每次搜索可能涉及多个商品,需要批量处理。
  • 每个用户的记录不超过10个,避免内存过大。
  • 需要存储商品ID和名称,可能涉及数据结构的选择。

二、需求分析

最新搜索过的商品需要排在前面,基于这个前提可以使用RedisSorted 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时保证原子性操作

总结实现步骤:

  1. 使用ZSet存储用户最近搜索的商品ID,按时间排序。
  2. 使用Hash存储商品ID和名称的映射。
  3. 每次搜索批量添加商品,更新ZSetscore为当前时间。
  4. 维护ZSet长度不超过10,移除旧记录。
  5. 提供获取最近搜索记录的方法,从ZSetHash中联合查询。

四、代码实现

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)提升批量操作性能。
    • 商品详情分离存储,支持多业务复用。
相关推荐
Vacant Seat11 分钟前
图论-实现Trie(前缀树)
java·开发语言·数据结构·图论
工一木子25 分钟前
【HeadFirst系列之HeadFirstJava】第16天之深入解析 Java 集合与泛型:高效管理数据的终极指南!(含代码实战)
java·集合·泛型
Yuanymoon1 小时前
【由技及道】统一封装API返回结果后String返回报错文件解决原理--Spring 消息转换器的层次图解与规则说明【人工智障AI2077的开发问题日志002】
java·spring
听风说雨的人儿1 小时前
ES6 class的继承概念
java·前端·es6
l_tian_tian_1 小时前
JavaWeb——Mybatis、JDBC、数据库连接池、lombok
java·数据库·mybatis
臣妾写不来啊1 小时前
使用dify的api连接外部知识库,dify连接ragflow的知识库(附java代码)
java·开发语言·spring boot
深思慎考2 小时前
Linux——进程间通信初解(匿名管道与命名管道)
java·linux·服务器
Seven972 小时前
【设计模式】使用解释器模式简化复杂的语法规则
java·后端·设计模式
yyueshen2 小时前
JVM中是如何定位一个对象的
java·jvm
异常驯兽师2 小时前
《Java三剑客:JDK、JRE、JVM的“塑料友情”》
java·开发语言·jvm