ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Redis缓存优化外卖平台店铺状态管理实践

Redis缓存优化外卖平台店铺状态管理实践

1. 项目背景与核心需求

最近在开发一个外卖平台项目"苍穹外卖",在Day05的开发中遇到了两个关键技术点:Redis缓存的应用和店铺营业状态管理。这两个功能看似独立,实则紧密关联——营业状态需要高频读取但低频修改,正是Redis的典型应用场景。

作为后端开发者,我发现在实际业务中,店铺营业状态这类基础数据会被订单系统、推荐系统、用户端展示等多个模块频繁查询。如果每次都走数据库,不仅性能堪忧,还会给数据库带来不必要的压力。而Redis作为内存数据库,其读取速度是传统关系型数据库的100倍以上,特别适合这种读多写少的场景。

2. Redis技术选型与配置

2.1 为什么选择Redis

在技术选型时,我们对比了几种缓存方案:

  • Memcached:简单但功能单一,缺乏持久化和丰富的数据结构
  • Ehcache:本地缓存,集群环境下同步困难
  • Redis:支持持久化、多种数据结构、原子操作,还有丰富的集群方案

最终选择Redis 6.2版本,主要基于以下考虑:

  1. 支持String/Hash/Set等多种数据结构,营业状态适合用String类型存储
  2. 提供原子性操作,避免多线程下的状态不一致问题
  3. 支持RDB和AOF两种持久化方式,保证数据安全
  4. 官方提供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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

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(); }

这里有几个优化点:

  1. 使用shop:status:作为key前缀,避免与其他业务冲突
  2. 设置1小时过期时间,防止长期不更新
  3. 使用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 Map<Long, Integer> batchGetShopStatus(List<Long> shopIds) { // 1. 构建缓存key列表 List<String> cacheKeys = shopIds.stream() .map(id -> "shop:status:" + id) .collect(Collectors.toList()); // 2. 使用pipeline批量获取 List<Object> statusList = redisTemplate.executePipelined((RedisCallback<Object>) connection -> { for (String key : cacheKeys) { connection.stringCommands().get(key.getBytes()); } return null; }); // 3. 处理结果 Map<Long, Integer> result = new HashMap<>(); List<Long> 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()) { List<Shop> 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监控指标

我们使用Prometheus+Grafana监控以下关键指标:

  1. 缓存命中率:keyspace_hits / (keyspace_hits + keyspace_misses)
  2. 内存使用率:used_memory / maxmemory
  3. 命令延迟:redis_command_latency_seconds
  4. 连接数:connected_clients

5.2 常见问题排查

问题1:缓存命中率低

  • 可能原因:TTL设置过短或缓存key设计不合理
  • 解决方案:调整过期时间,分析热点数据

问题2:Redis内存持续增长

  • 可能原因:缓存无过期时间或内存泄漏
  • 解决方案:检查所有key的TTL设置,使用MEMORY USAGE命令分析大key

问题3:连接数过高

  • 可能原因:连接未正确关闭或连接池配置不合理
  • 解决方案:检查代码中的连接释放逻辑,调整连接池参数

6. 扩展思考与优化方向

在实际项目中,我们还考虑了以下优化点:

  1. 多级缓存:在应用本地增加Caffeine缓存,形成本地缓存+Redis+MySQL的三级架构
  2. 热点数据预热:在营业高峰前,提前加载热门店铺的状态到缓存
  3. 异步更新:将状态变更事件发送到消息队列,由消费者异步更新缓存
  4. 分布式锁优化:使用Redisson实现的分布式锁,替代原生Redis命令

这个项目让我深刻体会到,看似简单的功能背后往往隐藏着复杂的技术考量。特别是缓存系统,需要在一致性、可用性和性能之间找到平衡点。在实际开发中,建议先实现基础功能,再逐步引入缓存优化,避免过早优化带来的复杂性。

返回列表