发布时间:2026/7/26 19:16:57
python# user_service.py - 原始版本(性能灾难)import psycopg2from datetime import datetime, timedeltadef get_user_browsing_history(user_id: int, limit: int = 50): """ 获取用户最近浏览记录 问题:未使用索引,且查询范围过大 """ conn = psycopg2.connect( host="db-prod", dbname="analytics", user="app_user", password="secret" ) cursor = conn.cursor() # 致命错误1:使用LIKE模糊查询用户ID(本应是精确匹配) # 致命错误2:未限制时间范围,导致扫描全表 query = """ SELECT product_id, view_time, category FROM browsing_history WHERE user_id LIKE '%{}%' -- 模糊匹配,无法使用索引 AND view_time > '{}' -- 硬编码30天前,但未使用参数化 ORDER BY view_time DESC LIMIT {} """.format(user_id, (datetime.now() - timedelta(days=30)).isoformat(), limit) cursor.execute(query) results = cursor.fetchall() cursor.close() conn.close() return results这段代码存在三个致命问题:1.模糊查询:LIKE '%{}%'导致数据库无法使用B-tree索引,必须全表扫描。2.未参数化查询:字符串格式化拼接SQL,不仅带来SQL注入风险,还让数据库无法缓存执行计划。3.无谓的全表扫描:即使只有100个活跃用户,每次查询也要扫描数百万行历史记录。## 第一次优化:索引与参数化我立刻修改了代码,并添加了数据库索引。但此时,告警仍在持续——虽然单个查询快了,但并发量太大,数据库连接池被耗尽。### 优化后的代码python# user_service.py - 第一次优化(解决SQL问题)import psycopg2from psycopg2 import poolfrom datetime import datetime, timedelta# 使用连接池,避免频繁创建连接connection_pool = psycopg2.pool.SimpleConnectionPool( 1, 20, # 最小1个连接,最大20个 host="db-prod", dbname="analytics", user="app_user", password="secret")def get_user_browsing_history_optimized(user_id: int, limit: int = 50): """ 优化版本:使用参数化查询和精确匹配 同时需要数据库添加复合索引: (user_id, view_time DESC) """ conn = connection_pool.getconn() try: cursor = conn.cursor() # 使用参数化查询,避免SQL注入,且数据库可缓存执行计划 # 精确匹配user_id,利用索引 # 限制时间范围,减少扫描行数 query = """ SELECT product_id, view_time, category FROM browsing_history WHERE user_id = %s -- 精确匹配,使用索引 AND view_time > %s -- 参数化时间范围 ORDER BY view_time DESC LIMIT %s """ # 只查询最近7天的数据,而不是30天 seven_days_ago = datetime.now() - timedelta(days=7) cursor.execute(query, (user_id, seven_days_ago, limit)) results = cursor.fetchall() return results finally: # 释放连接回连接池 cursor.close() connection_pool.putconn(conn)同时,我在数据库执行了以下索引创建:sqlCREATE INDEX CONCURRENTLY idx_user_view_time ON browsing_history (user_id, view_time DESC);## 深入分析:缓存与熔断的缺失SQL优化后,单次查询耗时从15秒降到50ms,但问题仍未完全解决。通过分析日志,我发现这个“用户画像聚合”函数,在高峰时期会被并发调用上千次。即使每个调用只花50ms,上千次并发也会耗尽数据库连接池,导致大量请求排队超时。更严重的是,这个函数没有任何缓存机制,每次用户刷新页面都要重新计算。而且,当数据库压力过大时,没有熔断机制,导致雪崩效应。### 重构:引入缓存与熔断python# user_profile_aggregator.py - 重构版本(缓存+熔断)import redisimport jsonfrom datetime import datetime, timedeltafrom functools import lru_cachefrom typing import Optionalclass UserProfileAggregator: """ 用户画像聚合器 使用多级缓存:本地缓存(LRU)+ 分布式缓存(Redis) 并实现熔断保护 """ def __init__(self, redis_client: redis.Redis): self.redis = redis_client # 本地缓存,最多缓存1000个用户,5分钟过期 self.local_cache = lru_cache(maxsize=1000) # 熔断器状态 self.circuit_breaker_state = { "failure_count": 0, "last_failure_time": None, "is_open": False } self.CIRCUIT_BREAKER_THRESHOLD = 5 # 连续5次失败则熔断 self.CIRCUIT_BREAKER_TIMEOUT = 30 # 熔断持续30秒 def get_user_profile(self, user_id: int) -> dict: """ 获取用户画像 优先从缓存读取,缓存未命中再从数据库加载 """ # 检查熔断器状态 if self._is_circuit_open(): # 熔断期间,返回降级数据(最近一次成功缓存的快照) degraded_data = self._get_degraded_data(user_id) if degraded_data: return degraded_data # 如果连降级数据都没有,返回空画像 return {"user_id": user_id, "profile": {}} # 尝试从缓存获取 cached_profile = self._get_from_cache(user_id) if cached_profile: return cached_profile # 缓存未命中,从数据库加载 try: profile = self._load_from_database(user_id) # 写入缓存,设置5分钟过期 self._set_to_cache(user_id, profile, ttl=300) # 重置熔断器计数 self._reset_circuit_breaker() return profile except Exception as e: # 记录失败 self._record_failure() # 尝试降级 degraded_data = self._get_degraded_data(user_id) if degraded_data: return degraded_data raise e def _get_from_cache(self, user_id: int) -> Optional[dict]: """从Redis缓存获取用户画像""" cache_key = f"user_profile:{user_id}" cached = self.redis.get(cache_key) if cached: return json.loads(cached) return None def _set_to_cache(self, user_id: int, profile: dict, ttl: int = 300): """写入Redis缓存""" cache_key = f"user_profile:{user_id}" self.redis.setex(cache_key, ttl, json.dumps(profile)) def _get_degraded_data(self, user_id: int) -> Optional[dict]: """获取降级数据(缓存中的历史快照)""" # 降级数据使用不同的key,避免被正常缓存覆盖 cache_key = f"user_profile_degraded:{user_id}" cached = self.redis.get(cache_key) if cached: return json.loads(cached) return None def _is_circuit_open(self) -> bool: """检查熔断器是否打开""" state = self.circuit_breaker_state if not state["is_open"]: return False # 检查熔断超时 if state["last_failure_time"] and \ (datetime.now() - state["last_failure_time"]).seconds > self.CIRCUIT_BREAKER_TIMEOUT: # 超时后自动半开 state["is_open"] = False state["failure_count"] = 0 return False return True def _record_failure(self): """记录一次失败""" state = self.circuit_breaker_state state["failure_count"] += 1 state["last_failure_time"] = datetime.now() if state["failure_count"] >= self.CIRCUIT_BREAKER_THRESHOLD: state["is_open"] = True def _reset_circuit_breaker(self): """重置熔断器""" self.circuit_breaker_state["failure_count"] = 0 self.circuit_breaker_state["is_open"] = False# 使用示例if __name__ == "__main__": # 初始化Redis客户端 redis_client = redis.Redis(host='redis-prod', port=6379, decode_responses=True) aggregator = UserProfileAggregator(redis_client) # 模拟并发请求 import threading def worker(user_id): try: profile = aggregator.get_user_profile(user_id) print(f"User {user_id} profile: {profile}") except Exception as e: print(f"Failed to get profile for {user_id}: {e}") threads = [] for i in range(100): # 100个并发请求 t = threading.Thread(target=worker, args=(i % 10,)) # 10个不同用户 threads.append(t) t.start() for t in threads: t.join()## 最终结果与反思经过以上优化:1.SQL优化:通过精确匹配和索引,单次查询从15秒降至50ms。2.连接池:避免频繁创建连接,降低数据库压力。3.多级缓存:本地缓存+Redis缓存,将99%的请求拦截在数据库之外。4.熔断机制:当数据库异常时,自动降级到缓存数据,防止雪崩。优化上线后,接口响应时间稳定在10ms以内,错误率降至0.01%,再也没有出现过凌晨告警。这次事故让我深刻体会到,性能优化不是单一维度的改进,而是需要从SQL、架构、缓存、容错等多个层面系统性地解决问题。## 总结生产事故是最好的老师。这次经历教会我三个核心原则:1.预防胜于治疗:上线前一定要做压力测试,特别是对数据库查询。2.缓存不是银弹:缓存能解决性能问题,但必须配合熔断和降级,否则缓存雪崩会引发更大的灾难。3.监控驱动优化:没有监控,你永远不知道问题是SQL慢、连接池不足,还是代码逻辑有bug。APM工具和日志分析是定位问题的利器。最后,记住一句真理:在分布式系统中,任何组件都可能失败。你的代码不仅要能处理成功路径,更要优雅地处理失败路径。