去年双十一,我们团队负责的电商平台在流量高峰时段遭遇了数据库雪崩,当时的情况是这样的:凌晨0点刚过,促销活动开始,瞬时流量暴涨50倍,数据库连接池瞬间耗尽,查询响应时间从平时的50ms飙升到5秒以上,整个商品详情页几乎瘫痪,运维团队紧急扩容了数据库节点,但效果有限——因为问题的核心不在数据库处理能力,而在于大量重复查询直接穿透到了数据库层。
这次惨痛经历让我们意识到:在高并发场景下,没有合理的缓存架构,再强大的数据库也会成为系统瓶颈,经过半年多的优化实践,我们最终用Redis构建了一套高性能查询方案,在今年的618大促中平稳支撑了峰值QPS 12万的流量,下面我就分享这套方案的设计思路和实现细节。
我们最早犯的错误就是没有处理好缓存穿透问题,当用户查询一个不存在的商品ID时,请求会直接落到数据库上,如果遭遇恶意攻击或爬虫,大量这类请求会导致数据库不堪重负。
解决方案:
def get_product(product_id): # 先检查布隆过滤器 if not bloom_filter.might_contain(product_id): return None # 尝试从Redis获取 cache_key = f"product:{product_id}" data = redis.get(cache_key) # 处理缓存命中 if data is not None: return json.loads(data) if data != "NULL" else None # 查询数据库 product = db.query("SELECT * FROM products WHERE id = %s", product_id) # 写入缓存 if product: redis.setex(cache_key, 3600, json.dumps(product)) # 缓存1小时 else: redis.setex(cache_key, 30, "NULL") # 空值缓存30秒 return product
另一个教训是缓存集中失效导致的雪崩效应,我们最初给所有商品数据设置了相同的1小时过期时间,结果在流量高峰时段大量缓存同时失效,数据库瞬间被打满。
优化方案:
// 设置带随机抖动的过期时间 int baseExpire = 3600; // 基础过期时间1小时 int randomExpire = baseExpire + ThreadLocalRandom.current().nextInt(-300, 300); // ±5分钟随机 redisTemplate.opsForValue().set(cacheKey, value, randomExpire, TimeUnit.SECONDS);
我们最终采用的缓存架构分为三层:
type MultiLevelCache struct { localCache *caffeine.Cache redis *redis.Client db *gorm.DB } func (c *MultiLevelCache) GetProduct(id string) (*Product, error) { // 1. 检查本地缓存 if item, ok := c.localCache.Get(id); ok { return item.(*Product), nil } // 2. 检查Redis cacheKey := fmt.Sprintf("product:%s", id) data, err := c.redis.Get(cacheKey).Bytes() if err == nil { product := &Product{} if string(data) != "NULL" { json.Unmarshal(data, product) c.localCache.Set(id, product, time.Minute*5) // 回填本地缓存 return product, nil } return nil, nil // 空值情况 } // 3. 查询数据库 var product Product if err := c.db.Where("id = ?", id).First(&product).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // 缓存空值防止穿透 c.redis.Set(cacheKey, "NULL", time.Second*30) return nil, nil } return nil, err } // 回填缓存 jsonData, _ := json.Marshal(product) c.redis.Set(cacheKey, jsonData, time.Hour+time.Duration(rand.Intn(600))*time.Second) c.localCache.Set(id, &product, time.Minute*10) return &product, nil }
通过监控分析,我们发现5%的商品承载了80%的流量,针对这些热点商品,我们做了特殊优化:
-- 库存扣减Lua脚本 local key = KEYS[1] local quantity = tonumber(ARGV[1]) local current = tonumber(redis.call('GET', key)) if current >= quantity then redis.call('DECRBY', key, quantity) return 1 -- 成功 else return 0 -- 库存不足 end
商品列表页需要查询数十个商品信息,最初我们采用循环单查的方式,产生了大量网络往返,优化后使用Redis的pipeline批量查询:
// 批量查询优化示例 List<String> productIds = Arrays.asList("1001", "1002", "1003", "1004"); List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> { for (String id : productIds) { connection.stringCommands().get(("product:" + id).getBytes()); } return null; }); List<Product> products = results.stream() .map(data -> data != null ? parseProduct((byte[]) data) : null) .collect(Collectors.toList());
对于商品搜索这类复杂查询,我们采用"查询条件+结果ID列表"的缓存方式:
def search_products(keyword, page=1, size=20): cache_key = f"search:{keyword}" # 先尝试获取ID列表 total, id_list = redis.zscan(cache_key, 0, count=page*size) if not id_list: # 数据库查询 results = db.query("SELECT id FROM products WHERE title LIKE %s ORDER BY sales DESC", f"%{keyword}%") # 写入Redis有序集合,score为销量 pipe = redis.pipeline() for product in results: pipe.zadd(cache_key, {product['id']: product['sales']}) pipe.expire(cache_key, 300) # 5分钟过期 pipe.execute() id_list = [x[0] for x in results] # 获取当前页ID page_ids = id_list[(page-1)*size : page*size] # 批量获取商品详情 products = batch_get_products(page_ids) return { 'total': len(id_list), 'items': products, 'page': page }
随着缓存数据增长,我们遇到了内存不足的问题,通过以下优化节省了40%内存:
# Redis配置示例 hash-max-ziplist-entries 512 # 哈希元素少于512时使用紧凑存储 hash-max-ziplist-value 64 # 哈希值小于64字节时使用紧凑存储 activerehashing yes # 启用主动rehash
当单实例无法承受流量压力时,我们采用了Redis Cluster方案,关键点包括:
redis-cli --hotkeys
定期分析访问模式为保证缓存数据安全,我们配置了:
# 持久化配置 appendonly yes appendfsync everysec save 86400 1 # 24小时后至少1个变更则触发bgsave
我们建立了完整的监控体系,重点关注:
# 获取关键指标 redis-cli info stats | grep -E "instantaneous_ops_per_sec|total_connections_received" redis-cli info memory | grep used_memory_human
案例:缓存更新导致集群抖动
我们曾遇到Redis集群在整点出现周期性延迟飙升的问题,经排查发现是多个服务的缓存过期时间设置过于集中(都是1小时整倍数),导致整点时大量缓存同时失效重建。
解决方案:
def refresh_cache(key, callback, lock_timeout=10): # 尝试获取分布式锁 lock_key = f"lock:{key}" if redis.set(lock_key, 1, nx=True, ex=lock_timeout): try: # 执行缓存重建 new_data = callback() redis.setex(key, 3600 + random.randint(0, 300), json.dumps(new_data)) finally: redis.delete(lock_key)
通过这套Redis缓存优化方案,我们的系统在今年的618大促中表现出色:
未来我们计划在以下方向继续优化:
缓存设计没有银弹,需要根据业务特点不断调整,希望我们的实践经验能给面临类似挑战的团队提供参考,好的缓存策略不是一蹴而就的,而是在持续监控和迭代中逐渐完善的。
本文由 行鸿轩 于2025-08-01发表在【云服务器提供商】,文中图片由(行鸿轩)上传,本平台仅提供信息存储服务;作者观点、意见不代表本站立场,如有侵权,请联系我们删除;若有图片侵权,请您准备原始证明材料和公证书后联系我方删除!
本文链接:https://vps.7tqx.com/wenda/504904.html
发表评论