MySQL 慢查询排查:从发现到解决

线上接口突然变慢,大概率是数据库查询出了问题。这篇文章梳理一下排查慢查询的完整流程。 开启慢查询日志 -- 查看当前配置 SHOW VARIABLES LIKE 'slow_query%'; SHOW VARIABLES LIKE 'long_query_time'; -- 临时开启(重启失效) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; -- 超过 1 秒的查询记录 SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log'; -- 永久配置(写入 my.cnf) [mysqld] slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 log_queries_not_using_indexes = 1 # 记录未使用索引的查询 用 pt-query-digest 分析 手动看慢查询日志很费时,pt-query-digest(Percona Toolkit)可以聚合分析: pt-query-digest /var/log/mysql/slow.log | head -100 输出会按响应时间降序列出各类查询,告诉你哪些查询最值得优化: # Query 1: 0.50 QPS, 2.50x concurrency, ID 0xABC123 # This item is included in the report because it matches --limit. # pct total min max avg 95% stddev median # Count 15 1000 # Exec time 80% 200s 0.1s 5s 0.2s 0.5s 0.3s 0.15s # Rows sent 5% 500 0 10 0 1 1 0 # Rows examine 90% 900k 100 5000 900 2000 800 600 重点关注 Rows examine(扫描行数)和 Exec time(执行时间)。 ...

2022-04-10 · 2 min · Kada Liao

FastAPI 入门:用 Python 快速构建现代 Web API

Django REST Framework 太重,Flask 太裸。FastAPI 在这两者之间找到了一个很好的平衡点。 为什么是 FastAPI 快:基于 Starlette,性能接近 Node.js 自动文档:自动生成 Swagger UI 和 ReDoc 类型驱动:用 Pydantic 做数据校验,写一次模型,校验和文档都有了 原生异步:完整支持 async/await 5 分钟上手 uv pip install fastapi uvicorn # main.py from fastapi import FastAPI app = FastAPI(title="我的 API", version="1.0.0") @app.get("/") def read_root(): return {"message": "Hello, World!"} @app.get("/users/{user_id}") def get_user(user_id: int, include_orders: bool = False): return {"user_id": user_id, "include_orders": include_orders} uvicorn main:app --reload # 访问 http://localhost:8000/docs 看自动生成的文档 Pydantic 模型:请求/响应的核心 from pydantic import BaseModel, EmailStr, Field from datetime import datetime class UserCreate(BaseModel): name: str = Field(..., min_length=2, max_length=50) email: EmailStr age: int = Field(..., ge=0, le=150) class UserResponse(BaseModel): id: int name: str email: str created_at: datetime class Config: from_attributes = True # 支持从 ORM 对象创建 @app.post("/users", response_model=UserResponse, status_code=201) def create_user(user: UserCreate): # FastAPI 自动解析请求体、校验字段、返回时过滤多余字段 db_user = save_to_db(user) return db_user Pydantic 的 Field 支持的校验相当丰富:字符串长度、数字范围、正则表达式……不需要手写校验逻辑。 ...

2022-03-28 · 2 min · Kada Liao

Python 虚拟环境管理:从 venv 到 uv

Python 的包管理历史是一部混乱史。这篇文章梳理一下各种工具的演变,以及我现在的实践选择。 为什么需要虚拟环境 不同项目依赖不同版本的库。不用虚拟环境,所有包都装在系统 Python 里,版本冲突是迟早的事: 项目 A:需要 Django 3.2 项目 B:需要 Django 4.2 → 只能装一个,两个项目不能同时开发 虚拟环境为每个项目创建独立的 Python 环境,依赖互不干扰。 各种工具的演变 venv(Python 3.3+ 内置) python -m venv .venv source .venv/bin/activate # macOS/Linux .venv\Scripts\activate # Windows pip install requests pip freeze > requirements.txt 最基础,不需要额外安装,但功能简单,requirements.txt 不区分开发依赖和生产依赖。 virtualenv + pip-tools pip install pip-tools # requirements.in(只写直接依赖) requests>=2.28 flask>=2.0 # 生成锁定版本的 requirements.txt pip-compile requirements.in # 安装 pip-sync requirements.txt pip-compile 解决了依赖锁定的问题,是很长一段时间内的最佳实践。 Poetry poetry new my-project poetry add requests poetry add pytest --group dev poetry install # pyproject.toml 管理所有配置 Poetry 把依赖管理、打包、发布整合在一起,pyproject.toml 是单一配置文件。问题是速度慢,解析依赖时间长。 ...

2021-10-05 · 1 min · Kada Liao

RESTful API 设计规范:我总结的一些原则

在做了几年后端开发之后,我发现 API 设计的好坏对前后端协作效率影响很大。这篇文章把我总结的一些原则写下来。 URL 设计 用名词,不用动词 # 不好 GET /getUser POST /createOrder DELETE /deleteProduct?id=1 # 好 GET /users/{id} POST /orders DELETE /products/{id} 层级关系用路径表达 GET /users/{userId}/orders # 某个用户的所有订单 GET /users/{userId}/orders/{id} # 某个用户的某个订单 用复数名词 GET /users # 不是 /user GET /products # 不是 /product HTTP 方法的语义 方法 语义 幂等 GET 读取资源 是 POST 创建资源 否 PUT 全量替换资源 是 PATCH 部分更新资源 否 DELETE 删除资源 是 幂等意味着重复调用和调用一次效果相同,这对网络重试很重要。 状态码要用对 200 OK - 成功 201 Created - 创建成功(POST 之后返回) 204 No Content - 成功但无响应体(DELETE 常用) 400 Bad Request - 客户端参数错误 401 Unauthorized - 未认证(没登录) 403 Forbidden - 无权限(登录了但没权限) 404 Not Found - 资源不存在 409 Conflict - 资源冲突(如重复创建) 422 Unprocessable Entity - 参数格式正确但业务校验失败 500 Internal Server Error - 服务器内部错误 最常见的错误:把所有错误都返回 200,在响应体里用 code 区分。这让客户端必须解析响应体才能判断是否成功,无法用 HTTP 层面的工具(代理、监控)做处理。 ...

2021-07-14 · 1 min · Kada Liao

Python 生成器与迭代器:惰性求值的力量

处理大文件或数据流时,把所有数据加载进内存是不现实的。生成器提供了一种"按需生成"的方式,解决了这个问题。 迭代器协议 Python 的 for 循环本质上是在调用迭代器协议: # for 循环等价于这段代码 it = iter(my_list) # 调用 __iter__,返回迭代器 while True: try: item = next(it) # 调用 __next__,取下一个元素 # 执行循环体 except StopIteration: break 实现了 __iter__ 和 __next__ 的对象就是迭代器。 生成器函数 用 yield 的函数就是生成器函数,调用它返回一个生成器对象: def count_up(start, end): current = start while current <= end: yield current # 暂停,返回 current,等待下次调用 current += 1 for n in count_up(1, 5): print(n) # 1 2 3 4 5 关键在于暂停和恢复:每次 next() 调用,函数从上次 yield 的地方继续执行,直到下一个 yield。 实际用处:处理大文件 # 不好:一次性加载所有行到内存 def read_all_lines(filename): with open(filename) as f: return f.readlines() # 10GB 文件直接 OOM # 好:逐行生成,内存始终只有一行 def read_lines(filename): with open(filename) as f: for line in f: yield line.strip() # 使用 for line in read_lines("huge_file.csv"): process(line) 生成器表达式 列表推导式的"惰性版本": ...

2021-06-30 · 2 min · Kada Liao

Python 类型注解实战:让代码更可维护

Python 是动态语言,但这不意味着不需要类型。类型注解让 IDE 提示更准确、代码意图更清晰、重构更安全。 基础语法 # 变量注解 name: str = "Kada" age: int = 30 scores: list[float] = [9.5, 8.8, 9.2] # Python 3.9+ # 函数注解 def greet(name: str, times: int = 1) -> str: return f"Hello, {name}! " * times # 返回 None def log(message: str) -> None: print(message) 常用类型 from typing import Optional, Union, List, Dict, Tuple, Any # Optional:可以是 None def find_user(user_id: int) -> Optional[str]: ... # Union:多种类型之一(Python 3.10 可以用 X | Y) def process(data: Union[str, bytes]) -> str: ... # Python 3.10+ def process(data: str | bytes) -> str: ... # 字典和列表(Python 3.9+ 可以直接用小写) def get_config() -> dict[str, Any]: ... # Tuple:固定长度和类型 def get_coordinates() -> tuple[float, float]: ... TypedDict:给字典加类型 JSON 数据经常用字典传递,TypedDict 让字典有类型检查: from typing import TypedDict class UserInfo(TypedDict): id: int name: str email: str age: int | None # 可选字段 def create_user(info: UserInfo) -> None: print(info["name"]) # IDE 有提示,且知道是 str 类型 dataclass:更结构化的数据类 比 TypedDict 更进一步,dataclass 是带类型的数据容器: ...

2021-04-08 · 2 min · Kada Liao

Python with 语句与上下文管理器

with 语句在 Python 里随处可见,但很多人只会用,不了解它的工作原理。这篇文章把上下文管理器讲清楚。 with 解决了什么问题 最典型的场景是文件操作: # 没有 with:如果中间报错,文件不会被关闭 f = open("data.txt") data = f.read() f.close() # 有 with:无论是否报错,退出 with 块时一定会关闭文件 with open("data.txt") as f: data = f.read() with 保证了"进入时执行某些操作,退出时一定执行另一些操作",无论退出是正常还是异常。 底层原理:enter 和 exit 实现了 __enter__ 和 __exit__ 的对象就是上下文管理器: class ManagedFile: def __init__(self, filename): self.filename = filename def __enter__(self): self.file = open(self.filename) return self.file # as 后面的变量就是这个返回值 def __exit__(self, exc_type, exc_val, exc_tb): self.file.close() # 返回 True 表示吞掉异常,False 或 None 表示继续传播异常 return False with ManagedFile("data.txt") as f: data = f.read() __exit__ 接收三个参数:异常类型、异常值、traceback。如果没有异常,三者都是 None。 更简单的写法:contextmanager 装饰器 用类实现太繁琐,contextlib.contextmanager 让你用生成器函数实现上下文管理器: from contextlib import contextmanager @contextmanager def managed_file(filename): f = open(filename) try: yield f # yield 之前是 __enter__,yield 的值是 as 后面的变量 finally: f.close() # yield 之后是 __exit__,finally 保证一定执行 with managed_file("data.txt") as f: data = f.read() yield 把函数分成两半:之前是进入逻辑,之后是退出逻辑。 ...

2021-01-22 · 2 min · Kada Liao

MySQL 事务、锁与死锁排查

事务和锁是 MySQL 里最复杂也最容易出问题的部分。这篇文章从实际问题出发,梳理核心概念和排查思路。 事务隔离级别 MySQL 有四种隔离级别,对应不同的并发问题: 隔离级别 脏读 不可重复读 幻读 READ UNCOMMITTED ✓ 可能 ✓ 可能 ✓ 可能 READ COMMITTED ✗ 不会 ✓ 可能 ✓ 可能 REPEATABLE READ(默认) ✗ 不会 ✗ 不会 部分解决 SERIALIZABLE ✗ 不会 ✗ 不会 ✗ 不会 InnoDB 默认是 REPEATABLE READ,并且通过 MVCC + Gap Lock 解决了大部分幻读问题。 InnoDB 的锁类型 行锁(最常用): 共享锁(S 锁):SELECT ... LOCK IN SHARE MODE,允许多个事务同时读 排他锁(X 锁):SELECT ... FOR UPDATE 或增删改,同一时间只有一个事务能持有 意向锁:表级别的锁,表示"我将要对某些行加行锁",用于快速判断表级操作是否冲突。 Gap Lock(间隙锁):锁定索引记录之间的间隙,防止插入新记录,解决幻读。 Next-Key Lock:行锁 + Gap Lock 的组合,InnoDB 在 REPEATABLE READ 下默认使用。 ...

2020-10-25 · 2 min · Kada Liao

MySQL 索引原理与优化实战

面试必考,工作必用。索引这个话题说简单也简单,说复杂也复杂。这篇文章尝试把最实用的部分说清楚。 B+ 树索引的工作原理 MySQL InnoDB 的索引底层是 B+ 树。B+ 树的特点: 所有数据存在叶子节点 叶子节点之间用链表相连(支持范围查询) 非叶子节点只存键值,不存数据(让每层能存更多节点) 这意味着一次查询最多只需要走 树高 次 IO。对于千万级的表,B+ 树高度通常只有 3-4 层,也就是 3-4 次 IO 就能找到数据。 聚簇索引 vs 二级索引 聚簇索引(主键索引):叶子节点直接存行数据。 二级索引(普通索引):叶子节点存的是主键值,查到后还需要回表(再走一次聚簇索引)。 -- 走二级索引,需要回表 SELECT * FROM users WHERE name = 'Kada'; -- 覆盖索引,不需要回表(索引包含了所有需要的字段) SELECT id, name FROM users WHERE name = 'Kada'; 覆盖索引是避免回表的常用技巧,查询的字段都在索引里,就不用回表了。 联合索引的最左前缀原则 -- 建了联合索引 (a, b, c) CREATE INDEX idx_abc ON t (a, b, c); -- 能用到索引 WHERE a = 1 WHERE a = 1 AND b = 2 WHERE a = 1 AND b = 2 AND c = 3 -- 不能用到索引(跳过了 a) WHERE b = 2 WHERE c = 3 WHERE b = 2 AND c = 3 联合索引按最左前缀匹配,中间不能断。 ...

2020-06-18 · 2 min · Kada Liao

Python 装饰器深入理解:从语法糖到元编程

装饰器是 Python 里我最喜欢的特性之一——它让你能在不修改函数本身的情况下,给它增加行为。这篇文章从头梳理一下装饰器的原理和用法。 从闭包说起 装饰器本质上是闭包。理解闭包是理解装饰器的前提: def outer(x): def inner(): print(x) # inner 捕获了外层的 x return inner f = outer(42) f() # 输出 42,即使 outer 已经返回 inner 函数"记住"了它定义时所在的环境(x = 42),这就是闭包。 装饰器的基本形态 装饰器就是一个接受函数、返回函数的函数: def my_decorator(func): def wrapper(*args, **kwargs): print("函数执行前") result = func(*args, **kwargs) print("函数执行后") return result return wrapper @my_decorator def say_hello(name): print(f"Hello, {name}!") # 等价于:say_hello = my_decorator(say_hello) say_hello("Kada") @my_decorator 只是语法糖,等价于 say_hello = my_decorator(say_hello)。 functools.wraps:保留函数元信息 用装饰器之后,函数的 __name__、__doc__ 会被替换成 wrapper 的: print(say_hello.__name__) # 输出 "wrapper",而不是 "say_hello" 用 functools.wraps 解决: import functools def my_decorator(func): @functools.wraps(func) # 把原函数的元信息复制过来 def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper 这是写装饰器的最佳实践,几乎任何时候都应该加上。 ...

2020-03-12 · 2 min · Kada Liao