尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

电商平台Redis多级缓存架构设计与实践

电商平台Redis多级缓存架构设计与实践 1. 项目背景与核心需求烘焙坊电商平台的后端系统在用户量达到日均1万UV后开始面临明显的性能瓶颈。特别是在促销活动期间商品详情页的数据库查询QPS峰值突破800次/秒导致MySQL实例CPU持续保持在90%以上。同时购物车功能的并发写入冲突问题使得部分用户在高峰期出现数据丢失情况。这个阶段我们需要解决两个核心问题高频访问数据的读取性能优化购物车这类写密集场景的数据一致性保障缓存系统的引入成为必然选择。但具体实施方案需要权衡多个因素缓存穿透的风险、数据一致性的保证、缓存层级的划分等。我们的技术选型最终确定为Redis作为主要缓存介质配合本地Caffeine缓存构成二级缓存体系。2. 缓存架构设计与实现2.1 多级缓存方案选型经过压力测试对比我们最终采用的分层缓存结构如下缓存层级技术实现响应时间适用场景数据一致性策略一级缓存Caffeine1ms热点数据定时过期二级缓存Redis2-5ms全量数据主动更新三级存储MySQL10-50ms持久化-这种架构的优势在于本地缓存解决超热点数据访问的极致性能需求Redis作为分布式缓存保证数据一致性通过合理的过期策略平衡性能与数据准确性2.2 Redis数据结构设计针对不同业务场景我们设计了差异化的数据结构商品信息缓存// Key设计 String productKey product: productId :v version; // Value结构采用Hash存储 MapString, String productMap new HashMap(); productMap.put(name, product.getName()); productMap.put(price, product.getPrice().toString()); productMap.put(stock, String.valueOf(product.getStock())); redisTemplate.opsForHash().putAll(productKey, productMap);购物车缓存// 使用Hash结构存储用户购物车 String cartKey cart: userId; redisTemplate.opsForHash().put( cartKey, productId.toString(), JSON.toJSONString(cartItem) ); // 设置合理的TTL7天 redisTemplate.expire(cartKey, 7, TimeUnit.DAYS);2.3 缓存一致性保障我们采用先更新数据库再删除缓存的策略配合消息队列实现最终一致性Transactional public void updateProduct(Product product) { // 1. 更新数据库 productMapper.updateById(product); // 2. 发送缓存删除事件 String cacheKey product: product.getId(); rabbitTemplate.convertAndSend( cache.events, new CacheEvictMessage(cacheKey) ); } // 消费者端处理 RabbitListener(queues cache.queue) public void handleCacheEvict(CacheEvictMessage message) { try { redisTemplate.delete(message.getKey()); localCache.invalidate(message.getKey()); } catch (Exception e) { // 失败重试逻辑 retryTemplate.execute(context - { redisTemplate.delete(message.getKey()); return null; }); } }关键提示在分布式环境下即使这样仍然可能出现短暂的数据不一致。对于商品价格等关键数据我们额外增加了版本号校验机制。3. 购物车功能深度实现3.1 并发写入解决方案购物车面临的核心挑战是并发修改问题。我们采用Redis的HASH结构配合Lua脚本实现原子化操作-- cart_update.lua local userId KEYS[1] local productId KEYS[2] local quantity tonumber(ARGV[1]) local maxLimit tonumber(ARGV[2]) -- 获取当前数量 local current redis.call(HGET, cart:..userId, productId) local currentQty current and tonumber(cjson.decode(current).quantity) or 0 -- 校验并更新 local newQty currentQty quantity if newQty 0 then redis.call(HDEL, cart:..userId, productId) return 0 elseif newQty maxLimit then return -1 else local item { productId productId, quantity newQty, updatedAt os.time() } redis.call(HSET, cart:..userId, productId, cjson.encode(item)) return newQty endJava调用方式Long result redisTemplate.execute( new DefaultRedisScript(luaScript, Long.class), Arrays.asList(userId.toString(), productId.toString()), String.valueOf(changeQuantity), String.valueOf(MAX_CART_ITEMS) );3.2 购物车过期策略购物车数据的过期管理采用分层策略基础TTL所有购物车Key默认7天过期访问续期每次读写操作时延长TTL定期清理每周执行一次扫描清理超过30天未更新的购物车// 读写时续期示例 public CartItem getCartItem(Long userId, Long productId) { String cartKey cart: userId; String itemJson (String) redisTemplate.opsForHash().get(cartKey, productId.toString()); // 续期操作 redisTemplate.expire(cartKey, 7, TimeUnit.DAYS); return parseCartItem(itemJson); }3.3 购物车与库存的预扣减在秒杀场景下我们实现了购物车到订单的平滑过渡public boolean reserveInventory(Long userId, ListCartItem items) { // 使用Redis的WATCH/MULTI实现乐观锁 return redisTemplate.execute(new SessionCallbackBoolean() { Override public Boolean execute(RedisOperations operations) throws DataAccessException { try { operations.watch(items.stream() .map(i - product: i.getProductId()) .toArray(String[]::new)); // 检查库存 MapString, Integer stocks new HashMap(); for (CartItem item : items) { String key product: item.getProductId(); Integer stock (Integer) operations.opsForHash().get(key, stock); if (stock null || stock item.getQuantity()) { operations.unwatch(); return false; } stocks.put(key, stock - item.getQuantity()); } // 扣减库存 operations.multi(); stocks.forEach((key, value) - { operations.opsForHash().put(key, stock, value.toString()); }); operations.exec(); return true; } catch (Exception e) { operations.discard(); throw e; } } }); }4. 性能优化与问题排查4.1 缓存穿透防护方案针对商品详情页的缓存穿透问题我们实现了多级防护布隆过滤器前置校验空值缓存缓存null值设置短TTL互斥锁重建缓存public Product getProductWithCache(Long productId) { // 1. 布隆过滤器检查 if (!bloomFilter.mightContain(productId)) { return null; } // 2. 尝试从缓存获取 String cacheKey product: productId; Product product cacheService.get(cacheKey); if (product ! null) { return product instanceof NullProduct ? null : product; } // 3. 获取分布式锁 String lockKey lock:product: productId; try { boolean locked redisLock.tryLock(lockKey, 3, TimeUnit.SECONDS); if (!locked) { // 短暂等待后重试 Thread.sleep(100); return getProductWithCache(productId); } // 4. 二次检查缓存防止并发重复查询 product cacheService.get(cacheKey); if (product ! null) { return product instanceof NullProduct ? null : product; } // 5. 查询数据库 product productMapper.selectById(productId); // 6. 写入缓存 if (product null) { cacheService.put(cacheKey, new NullProduct(), 1, TimeUnit.MINUTES); } else { cacheService.put(cacheKey, product, 30, TimeUnit.MINUTES); } return product; } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(Interrupted while getting product, e); } finally { redisLock.unlock(lockKey); } }4.2 热点Key发现与处理我们开发了实时热点监控系统主要流程包括Redis监控数据采集使用MONITOR命令采样滑动窗口统计5分钟窗口1分钟滑动热点Key自动识别超过阈值自动报警动态负载均衡对热点Key进行本地缓存// 热点Key监控示例 public class HotKeyDetector { private final MapString, AtomicLong counterMap new ConcurrentHashMap(); private final ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); public void start() { scheduler.scheduleAtFixedRate(this::analyzeHotKeys, 1, 1, TimeUnit.MINUTES); // 订阅Redis的MONITOR命令输出 redisTemplate.execute(new RedisCallbackVoid() { Override public Void doInRedis(RedisConnection connection) throws DataAccessException { connection.monitor(new MessageListener() { Override public void onMessage(Message message) { String cmd new String(message.getBody()); if (cmd.startsWith(GET ) || cmd.startsWith(HGET )) { String[] parts cmd.split( ); if (parts.length 2) { String key parts[1]; counterMap.computeIfAbsent(key, k - new AtomicLong()).incrementAndGet(); } } } }); return null; } }); } private void analyzeHotKeys() { long threshold 1000; // 每分钟1000次访问 ListString hotKeys counterMap.entrySet().stream() .filter(e - e.getValue().get() threshold) .map(Map.Entry::getKey) .collect(Collectors.toList()); if (!hotKeys.isEmpty()) { alertAndHandleHotKeys(hotKeys); } counterMap.clear(); } }4.3 典型问题排查记录问题1缓存雪崩导致服务不可用现象凌晨3点Redis连接数暴增大量请求直接打到数据库原因分析多个重要Key设置了相同的过期时间同时失效解决方案对基础数据缓存设置随机过期时间基础时间±随机偏移实现缓存预热定时任务增加熔断降级机制问题2购物车数据偶尔丢失现象部分用户反映购物车商品突然清空原因追踪Redis内存达到maxmemory触发自动淘汰解决方案调整Redis内存淘汰策略为volatile-lru对购物车Key设置明确的TTL增加Redis内存监控告警问题3高并发下库存超卖现象秒杀活动出现库存减为负数根本原因Redis与数据库库存不一致最终方案引入分布式锁控制库存扣减实现库存扣减的幂等操作增加库存校验的异步对账任务5. 监控与运维实践5.1 关键指标监控体系我们建立了完整的缓存监控看板核心指标包括指标类别具体指标报警阈值监控工具性能指标Redis平均响应时间50msPrometheus缓存命中率85%Grafana资源指标Redis内存使用率80%CloudWatch连接数使用率70%Datadog业务指标购物车操作成功率99.9%自定义监控商品详情页加载时间P99500msNewRelic5.2 缓存治理策略定期扫描与清理public void cleanExpiredCaches() { // 使用SCAN命令避免阻塞 String pattern product:*; Cursorbyte[] cursor redisTemplate.scan( ScanOptions.scanOptions().match(pattern).count(100).build()); int cleaned 0; while (cursor.hasNext()) { byte[] key cursor.next(); if (redisTemplate.getExpire(key) -1) { // 没有设置TTL的Key redisTemplate.expire(key, 24, TimeUnit.HOURS); cleaned; } } logger.info(Cleaned {} caches without TTL, cleaned); }大Key分析与优化使用redis-rdb-tools定期分析RDB文件对超过10KB的Hash进行分片存储对超过1MB的String类型数据转为Hash存储热Key自动迁移实时监控热点Key分布对极端热点Key启用本地缓存使用Redis Cluster的迁移命令平衡负载在实际运行中这套缓存系统成功将商品详情页的P99响应时间从320ms降低到45ms购物车操作的并发处理能力提升了8倍。特别是在618大促期间系统平稳支撑了平时5倍的流量高峰。
返回列表