黑马点评实战:用String和List两种姿势给店铺类型查询加Redis缓存(附完整代码对比)

张开发
2026/4/10 13:54:25 15 分钟阅读

分享文章

黑马点评实战:用String和List两种姿势给店铺类型查询加Redis缓存(附完整代码对比)
Redis缓存实战String与List数据结构在店铺类型查询中的深度对比1. 引言为什么需要关注数据结构选择在构建高并发系统时缓存设计往往是性能优化的第一道防线。对于Java开发者而言Spring Boot与Redis的组合已经成为微服务架构中的标准配置。但当我们真正将Redis应用于生产环境时一个看似简单却至关重要的问题浮出水面面对同一业务场景不同的数据结构会带来怎样的性能差异和实现复杂度以电商平台的店铺类型查询为例这个典型的读多写少场景既可以使用String类型存储JSON序列化后的完整列表也可以采用List结构直接保存元素集合。这两种方案在代码实现、内存占用、操作效率等方面存在显著差异而这些差异往往决定了系统在高并发下的表现。2. String方案实现与核心逻辑2.1 基础实现代码解析Service public class ShopTypeServiceImpl extends ServiceImplShopTypeMapper, ShopType implements IShopTypeService { Resource private StringRedisTemplate stringRedisTemplate; private static final String CACHE_SHOP_TYPE_KEY cache:shopType:all; public Result queryTypeList() { // 1. 尝试从Redis获取缓存 String shopTypeJson stringRedisTemplate.opsForValue().get(CACHE_SHOP_TYPE_KEY); // 2. 缓存命中处理 if (StrUtil.isNotBlank(shopTypeJson)) { ListShopType typeList JSONUtil.toList(shopTypeJson, ShopType.class); return Result.ok(typeList); } // 3. 缓存未命中时的数据库查询 ListShopType typeList query().orderByAsc(sort).list(); if (CollectionUtils.isEmpty(typeList)) { return Result.fail(店铺类型不存在); } // 4. 数据库结果写入缓存 stringRedisTemplate.opsForValue().set( CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(typeList) ); return Result.ok(typeList); } }2.2 关键特性分析String方案的核心特点可以总结为以下维度特性维度String方案表现序列化方式需要将整个集合JSON序列化为字符串内存占用额外存储JSON结构字符引号、括号等但整体压缩率较好读写操作每次读写都是全量操作无法单独处理集合中的某个元素代码复杂度实现简单直观适合快速开发适用场景数据量较小1MB、需要整体更新的场景缓存一致性维护任何数据变更都需要重新序列化整个集合提示当使用String存储集合数据时建议设置合理的TTL以避免长期不更新的脏数据问题3. List方案实现与技术细节3.1 基于List的核心实现public Result queryTypeList() { // 1. 尝试从Redis List获取全部元素 ListString cachedList stringRedisTemplate.opsForList() .range(CACHE_SHOP_TYPE_KEY, 0, -1); // 2. 缓存命中处理非空判断 if (!CollectionUtils.isEmpty(cachedList)) { ListShopType result cachedList.stream() .map(item - JSONUtil.toBean(item, ShopType.class)) .collect(Collectors.toList()); return Result.ok(result); } // 3. 数据库查询 ListShopType dbList query().orderByAsc(sort).list(); if (CollectionUtils.isEmpty(dbList)) { return Result.fail(店铺类型不存在); } // 4. 写入Redis List批量右插入 ListString toCache dbList.stream() .map(JSONUtil::toJsonStr) .collect(Collectors.toList()); stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_KEY, toCache); return Result.ok(dbList); }3.2 实现差异对比List方案与String方案的主要区别体现在存储结构差异String整个集合作为一个值存储List每个元素独立存储通过链表关联操作粒度差异String只能整体读写List支持以下精细操作LPUSH/RPUSH添加元素LPOP/RPOP移除元素LINDEX获取指定位置元素LRANGE获取范围元素内存分配特点List每个元素需要额外的链表指针空间小集合时String更省内存大集合时List可能更优4. 性能对比与选型建议4.1 基准测试数据参考在本地环境Redis 6.2中对1000次查询进行测试得到如下数据数据规模操作类型String平均耗时List平均耗时内存占用差异50条记录读取12ms18ms15%50条记录写入25ms32ms20%500条记录读取68ms105ms8%500条记录写入142ms210ms12%4.2 选型决策矩阵根据业务场景选择数据结构的关键考量因素数据变更频率高频变更List支持增量更新低频变更String更简单访问模式需要部分数据List的LRANGE更高效总是需要全量数据String更合适数据规模小数据量100条差异不大大数据量需要考虑内存占用和反序列化成本特殊需求需要过期时间String设置更方便需要队列特性List天然支持5. 高级优化技巧5.1 混合方案设计对于大型电商平台可以考虑组合使用两种数据结构// 伪代码示例 public Result queryTypeList() { // 先尝试获取String类型的全量缓存 String fullCache redis.get(CACHE_FULL_KEY); if (fullCache ! null) { return parse(fullCache); } // 再尝试从List获取兼容旧版本 ListString listCache redis.lrange(CACHE_LIST_KEY, 0, -1); if (!listCache.isEmpty()) { return parse(listCache); } // 数据库查询 ListShopType dbResult queryDB(); // 双写根据性能决定是否异步 redis.set(CACHE_FULL_KEY, serialize(dbResult)); redis.lpush(CACHE_LIST_KEY, serializeElements(dbResult)); return dbResult; }5.2 缓存预热策略对于店铺类型这类变更较少的数据可以在服务启动时主动加载PostConstruct public void warmUpCache() { ListShopType types query().orderByAsc(sort).list(); if (!CollectionUtils.isEmpty(types)) { // 并行写入两种结构 CompletableFuture.runAsync(() - stringRedisTemplate.opsForValue().set( CACHE_SHOP_TYPE_KEY, JSONUtil.toJsonStr(types) ) ); CompletableFuture.runAsync(() - { ListString elements types.stream() .map(JSONUtil::toJsonStr) .collect(Collectors.toList()); stringRedisTemplate.opsForList() .rightPushAll(CACHE_SHOP_TYPE_KEY :list, elements); }); } }6. 生产环境注意事项序列化选择JSON通用性强但性能一般Protobuf性能更好但需要SchemaKryoJava专用性能优异内存优化技巧对于String方案考虑压缩后再存储对于List方案可以定期整理碎片监控指标缓存命中率反序列化耗时内存增长趋势典型问题处理大Key问题String方案需警惕热Key问题考虑多级缓存缓存穿透空值缓存布隆过滤器

更多文章