Redis缓存优化外卖平台店铺状态管理实践 1. 项目背景与核心需求最近在开发一个外卖平台项目苍穹外卖在Day05的开发中遇到了两个关键技术点Redis缓存的应用和店铺营业状态管理。这两个功能看似独立实则紧密关联——营业状态需要高频读取但低频修改正是Redis的典型应用场景。作为后端开发者我发现在实际业务中店铺营业状态这类基础数据会被订单系统、推荐系统、用户端展示等多个模块频繁查询。如果每次都走数据库不仅性能堪忧还会给数据库带来不必要的压力。而Redis作为内存数据库其读取速度是传统关系型数据库的100倍以上特别适合这种读多写少的场景。2. Redis技术选型与配置2.1 为什么选择Redis在技术选型时我们对比了几种缓存方案Memcached简单但功能单一缺乏持久化和丰富的数据结构Ehcache本地缓存集群环境下同步困难Redis支持持久化、多种数据结构、原子操作还有丰富的集群方案最终选择Redis 6.2版本主要基于以下考虑支持String/Hash/Set等多种数据结构营业状态适合用String类型存储提供原子性操作避免多线程下的状态不一致问题支持RDB和AOF两种持久化方式保证数据安全官方提供Cluster模式方便后续扩展2.2 Redis配置要点在application.yml中的关键配置spring: redis: host: 127.0.0.1 port: 6379 password: database: 0 lettuce: pool: max-active: 8 # 根据服务器核心数设置 max-wait: -1ms # 无限等待 max-idle: 8 min-idle: 0重要提示生产环境一定要配置密码和合理的连接池参数。我们曾经因为连接泄漏导致服务不可用后来通过max-active限制才解决。3. 营业状态功能实现3.1 数据库设计首先在MySQL创建店铺表CREATE TABLE shop ( id bigint NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, status tinyint DEFAULT 0 COMMENT 0-休息中 1-营业中, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 双写一致性方案为了保证Redis和MySQL的数据一致我们采用先更新数据库再删除缓存的策略public void updateShopStatus(Long shopId, Integer status) { // 1. 更新数据库 shopMapper.updateStatus(shopId, status); // 2. 删除缓存 redisTemplate.delete(shop:status: shopId); // 3. 记录操作日志 logService.recordShopStatusChange(shopId, status); }踩坑记录曾经尝试先删缓存再更新数据库结果在高并发下出现了缓存击穿问题。现在的方案虽然可能有短暂不一致但对业务影响更小。3.3 缓存查询优化获取营业状态的典型代码public Integer getShopStatus(Long shopId) { // 1. 先查缓存 String cacheKey shop:status: shopId; Object status redisTemplate.opsForValue().get(cacheKey); if (status ! null) { return (Integer)status; } // 2. 缓存不存在则查数据库 Shop shop shopMapper.selectById(shopId); if (shop null) { throw new BusinessException(店铺不存在); } // 3. 写入缓存并设置TTL redisTemplate.opsForValue().set(cacheKey, shop.getStatus(), 1, TimeUnit.HOURS); return shop.getStatus(); }这里有几个优化点使用shop:status:作为key前缀避免与其他业务冲突设置1小时过期时间防止长期不更新使用opsForValue()简化String类型操作4. 高并发场景下的问题处理4.1 缓存击穿解决方案当某个店铺状态缓存过期同时有大量请求涌入时会导致所有请求都打到数据库。我们通过互斥锁解决public Integer getShopStatusWithLock(Long shopId) { String cacheKey shop:status: shopId; // 第一次尝试获取缓存 Object status redisTemplate.opsForValue().get(cacheKey); if (status ! null) { return (Integer)status; } // 获取锁 String lockKey lock:shop:status: shopId; boolean locked false; try { locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS); if (locked) { // 二次检查缓存 status redisTemplate.opsForValue().get(cacheKey); if (status ! null) { return (Integer)status; } // 查询数据库 Shop shop shopMapper.selectById(shopId); if (shop null) { throw new BusinessException(店铺不存在); } // 写入缓存 redisTemplate.opsForValue().set(cacheKey, shop.getStatus(), 1, TimeUnit.HOURS); return shop.getStatus(); } else { // 未获取到锁短暂休眠后重试 Thread.sleep(50); return getShopStatusWithLock(shopId); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BusinessException(获取店铺状态异常); } finally { if (locked) { redisTemplate.delete(lockKey); } } }4.2 批量查询优化在首页需要展示多个店铺状态时我们使用pipeline批量操作public MapLong, Integer batchGetShopStatus(ListLong shopIds) { // 1. 构建缓存key列表 ListString cacheKeys shopIds.stream() .map(id - shop:status: id) .collect(Collectors.toList()); // 2. 使用pipeline批量获取 ListObject statusList redisTemplate.executePipelined((RedisCallbackObject) connection - { for (String key : cacheKeys) { connection.stringCommands().get(key.getBytes()); } return null; }); // 3. 处理结果 MapLong, Integer result new HashMap(); ListLong missingIds new ArrayList(); for (int i 0; i shopIds.size(); i) { Long shopId shopIds.get(i); Object status statusList.get(i); if (status ! null) { result.put(shopId, (Integer)status); } else { missingIds.add(shopId); } } // 4. 处理缓存未命中的情况 if (!missingIds.isEmpty()) { ListShop shops shopMapper.selectBatchIds(missingIds); for (Shop shop : shops) { result.put(shop.getId(), shop.getStatus()); redisTemplate.opsForValue().set( shop:status: shop.getId(), shop.getStatus(), 1, TimeUnit.HOURS ); } } return result; }5. 监控与运维实践5.1 Redis监控指标我们使用PrometheusGrafana监控以下关键指标缓存命中率keyspace_hits / (keyspace_hits keyspace_misses)内存使用率used_memory / maxmemory命令延迟redis_command_latency_seconds连接数connected_clients5.2 常见问题排查问题1缓存命中率低可能原因TTL设置过短或缓存key设计不合理解决方案调整过期时间分析热点数据问题2Redis内存持续增长可能原因缓存无过期时间或内存泄漏解决方案检查所有key的TTL设置使用MEMORY USAGE命令分析大key问题3连接数过高可能原因连接未正确关闭或连接池配置不合理解决方案检查代码中的连接释放逻辑调整连接池参数6. 扩展思考与优化方向在实际项目中我们还考虑了以下优化点多级缓存在应用本地增加Caffeine缓存形成本地缓存RedisMySQL的三级架构热点数据预热在营业高峰前提前加载热门店铺的状态到缓存异步更新将状态变更事件发送到消息队列由消费者异步更新缓存分布式锁优化使用Redisson实现的分布式锁替代原生Redis命令这个项目让我深刻体会到看似简单的功能背后往往隐藏着复杂的技术考量。特别是缓存系统需要在一致性、可用性和性能之间找到平衡点。在实际开发中建议先实现基础功能再逐步引入缓存优化避免过早优化带来的复杂性。