沿途的风景 - Redis
type: channel
link: https://inspirecl.asia/index.php/tag/redis/
items: webman 数据库实战：连接池、ORM 选型、事务边界与大数据处理
items: Redis 缓存实战：穿透、击穿、雪崩三大问题解决方案
items: Redis 数据类型与核心命令详解：从 String 到 ZSet
items: Redis 持久化机制深度对比：RDB、AOF 与混合持久化
items: Redis 集群方案详解：主从复制、哨兵与 Cluster
items: Redis 分布式锁实现：从 SET NX 到 Redlock 算法
items: MySQL 慢查询优化实战：从 EXPLAIN 到索引调优

webman 数据库实战：连接池、ORM 选型、事务边界与大数据处理
type: item
date: 2026-08-15T09:22:54+08:00
description: webman 数据库实战：连接池、ORM 选型、事务边界与大数据处理数据库是 webman 与传统 FPM 框架差异最大的环节。FPM 下每个请求开新连接、请求结束关闭，天然隔离；webman 的每个 worker 进程持有自己的连接并跨请求复用——连接管理从"框架的事"变成了"你的事"。这篇讲透连接池配置、think-orm 与 Eloquent 的选型、事务的正确边界，以及千万级数据的处理姿势。一、常驻内存下的连接模型先看清楚 webman 的数据库连接结构：worker-1 ─── 连接A（MySQL）──┐ worker-2 ─── 连接B ├── MySQL max_connections 上限（如 1000） worker-3 ─── 连接C │ ...（16 个 worker） │ └── Redis 连接同理推论一：最大连接数 = worker 进程数 × 每进程连接数。 16 个 worker、每个用了 2 个连接配置（读写分离），就是 32 个常驻连接。上线前先算这笔账，别和 MySQL 的 max_connections 打架。推论二：连接是懒建立的。 进程启动时不连库，第一次查询才建立。所以刚 reload 完的进程有冷启动延迟，压测时记得先预热。推论三：连接会"死"。 MySQL wait_timeout（默认 8 小时）内没有活动，服务端单方面断开，webman 侧还握着死连接，下一个请求直接 MySQL has gone away。二、断线重连：三层配置兜底以 think-orm（webman 默认集成）为例：// config/thinkorm.php return [ 'default' => 'mysql', 'connections' => [ 'mysql' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'app', 'username' => 'root', 'password' => 'root', 'charset' => 'utf8mb4', 'break_reconnect' => true, // ① 断线自动重连 'fields_cache' => true, // ② 字段信息缓存（常驻内存的红利） ], ], ];三层兜底缺一不可：break_reconnect => true：ORM 层检测断连自动重连心跳保活：定时进程每 5 分钟发一条 SELECT 1，让连接永不超时（webman 的 config/process.php 里加自定义进程实现）重试幂等：断线重连只对查询安全，写操作重试必须保证幂等，否则可能双写// 心跳进程示例 return [ 'db-keepalive' => [ 'handler' => function () { while (true) { Db::query('SELECT 1'); sleep(300); } }, ], ];三、ORM 选型：think-orm 还是 Laravel Eloquent？webman 官方两者都支持，选型看团队基因：维度think-ormEloquent（illuminate/database）上手成本ThinkPHP 系团队零成本Laravel 系团队零成本模型功能基础够用关联预加载、访问器、软删更丰富常驻内存适配fields_cache 等针对性优化需要自己注意静态缓存生态迁移TP 项目平移Laravel 项目平移中大型项目的建议：ORM 只做"查询构造 + 结果映射"，复杂查询直接裸 SQL 或查询构造器。模型层越薄，常驻内存下的心智负担越小。一个 Eloquent 用户必须知道的区别：FPM 下 DB::connection() 每请求新建，webman 下连接跨请求驻留——事务没有提交/回滚，下一个请求会接着在这个事务里跑，数据不一致的幽灵bug就是这么来的。think-orm 的 Db::startTrans() 同理。// ❌ 事故现场：异常没回滚，连接带着事务回到池里 try { Db::startTrans(); // ... 写操作 throw new \RuntimeException('boom'); // 没有 catch 回滚！ } finally { Db::commit(); // finally 里 commit，异常时也 commit 了半截数据？ }四、事务的正确姿势铁律一：事务边界必须完整闭合try { Db::startTrans(); $this->deductStock($skuId, $num); // 扣库存 $this->createOrder($uid, $skuI…

Redis 缓存实战：穿透、击穿、雪崩三大问题解决方案
type: item
date: 2026-05-10T11:42:54+08:00
description: 前言Redis 作为缓存使用时，会遇到三大经典问题：缓存穿透、缓存击穿和缓存雪崩。这三个问题在实际生产中频繁出现，处理不当可能导致数据库被压垮甚至服务不可用。本文将深入分析每个问题的成因并给出完整解决方案。一、缓存穿透1.1 问题描述用户请求的数据在缓存和数据库中都不存在，每次请求都直接打到数据库。常见于恶意攻击（用不存在的 ID 大量请求）或业务异常数据。请求 → 缓存MISS → 数据库MISS → 返回空 ↑ │ └──── 不断重复 ──────────────────────┘1.2 危害数据库承受大量无效查询如果是恶意攻击，可能打垮数据库浪费服务器资源1.3 解决方案方案一：缓存空值function getUser($id) { $cacheKey = "user:{$id}"; $data = $redis->get($cacheKey); if ($data !== false) { // 命中缓存（包括空值缓存） return $data === 'NULL' ? null : json_decode($data, true); } // 查数据库 $data = $db->query("SELECT * FROM users WHERE id = ?", [$id]); if (empty($data)) { // 缓存空值，设置较短过期时间（60秒） $redis->setex($cacheKey, 60, 'NULL'); return null; } $redis->setex($cacheKey, 3600, json_encode($data)); return $data; }优点：实现简单缺点：可能缓存大量空值，占用内存；数据后来新增了也不会被感知（除非过期）方案二：布隆过滤器（Bloom Filter）// 系统初始化时，将所有存在的ID加入布隆过滤器 function initBloomFilter() { $redis->del('bloom:user_ids'); $userIds = $db->query("SELECT id FROM users"); foreach ($userIds as $id) { // 使用多个哈希函数 bloomAdd('bloom:user_ids', $id); } } // 检查ID是否可能存在 function bloomAdd($key, $value) { $hashes = multiHash($value); foreach ($hashes as $h) { $redis->setBit($key, $h, 1); } } function bloomExists($key, $value) { $hashes = multiHash($value); foreach ($hashes as $h) { if ($redis->getBit($key, $h) == 0) { return false; // 一定不存在 } } return true; // 可能存在（有误判率） } // 请求处理 function getUser($id) { // 先过布隆过滤器 if (!bloomExists('bloom:user_ids', $id)) { return null; // 一定不存在，直接返回 } // 再查缓存和数据库 // ... }方案三：接口层参数校验// 在入口处拦截非法参数 function getUser($id) { // ID 必须是正整数 if (!is_numeric($id) || $id <= 0 || $id > PHP_INT_MAX) { throw new InvalidArgumentException('Invalid user ID'); } // 其他业务规则校验 if (strlen($id) > 10) { throw new InvalidArgumentException('ID too long'); } // 正常流程... }二、缓存击穿2.1 问题描述某个热点 key 突然过期（或被删除），大量并发请求同时访问这个 key，全部穿透到数据库。热点key过期 │ ├──→ 请求1 → 缓存MISS → 查DB ──┐ ├──→ 请求2 → 缓存MISS → 查DB ──┤ ├──→ 请求3 → 缓存MISS → 查DB ──┤ ├──→ 请求N → 缓存MISS → 查DB ──┤ │ │ └──────── 数据库被压垮 ◄────────┘2.2 解决方案方案一：互斥锁（推荐）function getHotData($key) { $data = $redis-…

Redis 数据类型与核心命令详解：从 String 到 ZSet
type: item
date: 2026-05-09T21:45:03+08:00
description: 前言Redis 是目前最流行的内存数据库，凭借极高的读写性能和丰富的数据结构，被广泛应用于缓存、消息队列、分布式锁等场景。本文将系统讲解 Redis 的五大核心数据类型及其常用命令，帮助你快速上手。一、Redis 数据类型总览数据类型底层结构典型应用场景StringSDS（简单动态字符串）缓存、计数器、分布式锁Hashziplist / hashtable对象存储、用户信息Listquicklist（ziplist + 链表）消息队列、文章列表Setintset / hashtable标签、共同好友、去重ZSetziplist / skiplist + hashtable排行榜、延迟队列二、String（字符串）String 是 Redis 最基础的类型，一个 key 对应一个 value，最大支持 512MB。2.1 基本命令# 设置值 SET name "redis" SET name "redis" EX 30 # 30秒过期 SET name "redis" PX 30000 # 30000毫秒过期 SET name "redis" NX # 仅当key不存在时设置 SET name "redis" XX # 仅当key存在时设置 # 获取值 GET name # "redis" # 批量操作 MSET k1 "v1" k2 "v2" k3 "v3" MGET k1 k2 k3 # 1) "v1" 2) "v2" 3) "v3" # 追加 APPEND name " tutorial" # "redis tutorial" # 长度 STRLEN name # 14 # 自增自减（值必须是数字） SET counter 100 INCR counter # 101 INCRBY counter 10 # 111 DECR counter # 110 DECRBY counter 20 # 902.2 实战：页面访问计数器# 每次访问页面自增 INCR page:home:views # 1 INCR page:home:views # 2 INCR page:home:views # 3 # 获取今日访问量（按日期分key） INCR page:home:views:20260724 # 1 # 设置过期时间，自动清理历史数据 EXPIRE page:home:views:20260724 864002.3 实战：分布式锁（简单版）# 加锁：SET NX 保证原子性 SET lock:order:1001 "uuid-xxx" NX EX 30 # 释放锁（需要Lua脚本保证原子性） # eval "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock:order:1001 uuid-xxx三、Hash（哈希）Hash 是一个键值对集合，特别适合存储对象。3.1 基本命令# 设置字段 HSET user:1001 name "张三" HSET user:1001 age 25 HSET user:1001 email "zhangsan@example.com" # 批量设置 HMSET user:1002 name "李四" age 30 email "lisi@example.com" # 获取字段 HGET user:1001 name # "张三" HMGET user:1001 name age # 1) "张三" 2) "25" # 获取所有字段 HGETALL user:1001 # 1) "name" # 2) "张三" # 3) "age" # 4) "25" # 5) "email" # 6) "zhangsan@example.com" # 获取所有字段名/值 HKEYS user:1001 # name, age, email HVALS user:1001 # 张三, 25, zhangsan@example.com # 字段数量 HLEN user:1001 # 3 # 删除字段 HDEL user:1001 email # 字段自增 HINCRBY user:1001 age 1 # 263.2 实战：购物车# 添加商品到购物车（user:1001 的购物车） HSET cart:1001 product:2001 2 # 商品2001，数量2 HSET cart:1001 product:2002 1 # 商品2002，数量1 # 修改数量 HINCRBY cart:1001 product:2001 1 # 数量+1 → 3 # 查看购物…

Redis 持久化机制深度对比：RDB、AOF 与混合持久化
type: item
date: 2026-04-29T02:53:29+08:00
description: 前言Redis 作为内存数据库，数据存储在 RAM 中，一旦服务器重启数据就会丢失。为了解决这个问题，Redis 提供了两种持久化机制：RDB（快照）和 AOF（追加日志）。本文将深入对比这两种机制的原理、配置和选择策略。一、RDB（Redis Database）1.1 工作原理RDB 是将 Redis 内存中的数据以二进制快照的形式写入磁盘。触发方式有三种：┌─────────────────────────────────────┐ │ Redis 内存数据 │ │ │ │ ┌───────┐ ┌───────┐ │ │ │ Key1 │ │ Key2 │ ... │ │ └───────┘ └───────┘ │ │ │ │ ▼ fork() 子进程 │ │ │ │ ┌─────────────────────┐ │ │ │ BGSAVE 子进程 │ │ │ │ 写入 dump.rdb │ │ │ └─────────────────────┘ │ └─────────────────────────────────────┘1.2 触发方式手动触发：# 阻塞式（生产环境禁用） SAVE # 后台异步执行（推荐） BGSAVE自动触发（配置文件）：# redis.conf # 900秒内至少1个key变化 → 触发RDB save 900 1 # 300秒内至少10个key变化 → 触发RDB save 300 10 # 60秒内至少10000个key变化 → 触发RDB save 60 10000 # 禁用自动RDB # save ""其他触发场景：主从复制时，主节点自动触发 BGSAVE执行 SHUTDOWN 命令时执行 FLUSHALL 命令时（生成空快照）1.3 RDB 文件配置# redis.conf # RDB 文件名 dbfilename dump.rdb # 存储目录 dir /var/lib/redis # 压缩（默认开启，用 LZF 压缩） rdbcompression yes # 校验和（默认开启） rdbchecksum yes # BGSAVE 出错时停止写入（默认yes，推荐开启） stop-writes-on-bgsave-error yes1.4 RDB 优缺点优点：文件紧凑，体积小，适合备份和传输恢复速度快（直接加载二进制数据）对性能影响小（子进程异步执行）适合做冷备和灾难恢复缺点：宕机时会丢失最后一次快照后的所有数据fork 子进程时，如果数据量大，可能造成短暂阻塞不适合对数据完整性要求高的场景二、AOF（Append Only File）2.1 工作原理AOF 以追加的方式将每条写命令记录到日志文件中，重启时按顺序回放命令恢复数据。┌─────────────────────────────────────┐ │ Redis 内存数据 │ │ │ │ 客户端写入命令 │ │ │ │ │ ▼ │ │ ┌──────────┐ ┌──────────────┐ │ │ │ Redis执行 │ → │ 追加到AOF缓冲 │ │ │ │ 命令 │ │ 区 │ │ │ └──────────┘ └──────┬───────┘ │ │ │ │ │ ▼ fsync │ │ ┌──────┴───────┐ │ │ │ appendonly.aof│ │ │ └──────────────┘ │ └─────────────────────────────────────┘2.2 开启 AOF# redis.conf appendonly yes appendfilename "appendonly.aof"2.3 刷盘策略# 三种 fsync 策略 # always: 每条命令都fsync（最安全，性能最差） # everysec: 每秒fsync一次（推荐，最多丢1秒数据） # no: 由OS决定何时fsync（性能最好，可能丢较多数据） appendfsync everysec策略数据安全性性能适用场景always最高（不丢数据）最差对数据极其敏感everysec高（最多丢1秒）好推荐默认no低最好纯缓存场景2.4 AOF 重写随着时间推移，AOF 文件会越来越大。Redis 通过"重写"机制压缩文件：分析当前内存数据，用最少的命令重新生成 AOF 文件。# 手动触发重写 BGREWRITEAOF# redis.conf # AOF 文件大小比上次重写后增长 100% 时自动重写 auto-aof-rewrite-percentage 100 # AOF 文件最小重写体积（64MB） auto-aof-rewrite-min-size 64mb # 重写期间不执行 fsync（减少磁盘 IO…

Redis 集群方案详解：主从复制、哨兵与 Cluster
type: item
date: 2026-04-22T19:49:33+08:00
description: 前言当单机 Redis 无法满足业务需求时，就需要引入集群方案。Redis 提供了三种主要的集群模式：主从复制、哨兵模式和 Cluster 集群。本文将系统讲解三种方案的原理、搭建和适用场景。一、主从复制1.1 架构 ┌──────────────┐ │ Master │ │ (读+写) │ └──────┬───────┘ │ 复制 ┌──────────┼──────────┐ │ │ │ ┌──────▼───┐ ┌───▼────┐ ┌──▼─────┐ │ Slave 1 │ │Slave 2 │ │Slave 3 │ │ (只读) │ │(只读) │ │(只读) │ └──────────┘ └────────┘ └────────┘1.2 工作原理全量同步：Slave 首次连接 Master 时，Master 执行 BGSAVE 生成 RDB 文件，发送给 Slave增量同步：全量同步完成后，Master 将新的写命令实时同步给 Slave断线重连：Slave 断线后重连，通过 replid + offset 判断是否可以增量同步1.3 搭建配置# slave1.conf port 6380 # 配置主节点 slaveof 192.168.1.10 6379 # 从节点只读 slave-read-only yes # 主节点密码 masterauth your_password# 启动从节点 redis-server slave1.conf # 验证主从关系 redis-cli -p 6380 INFO replication # role:slave # master_host:192.168.1.10 # master_port:63791.4 优缺点优点缺点配置简单Master 宕机需手动切换读写分离，提升读性能不具备自动故障转移数据有副本全量同步时可能阻塞 Master二、哨兵模式（Sentinel）2.1 架构┌──────────┐ ┌──────────┐ ┌──────────┐ │Sentinel 1│ │Sentinel 2│ │Sentinel 3│ │ (监控) │ │ (监控) │ │ (监控) │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ └──────┬───────┴──────┬──────┘ │ │ ┌──────▼──────┐ ┌────▼─────┐ │ Master │ │ Slave │ │ (读+写) │ │ (只读) │ └─────────────┘ └──────────┘2.2 工作原理监控：Sentinel 不断向 Master/Slave 发送 PING 命令主观下线：某个 Sentinel 发现节点无响应，标记为主观下线客观下线：超过半数 Sentinel 都报告节点下线，标记为客观下线选举 Leader：Sentinel 之间选举一个 Leader 执行故障转移故障转移：Leader 选一个 Slave 提升为新 Master，通知其他 Slave 和客户端2.3 搭建配置# sentinel1.conf port 26379 # 监控的 Master（名称 IP 端口 仲裁人数） sentinel monitor mymaster 192.168.1.10 6379 2 # Master 无响应 30 秒判定下线 sentinel down-after-milliseconds mymaster 30000 # 故障转移超时时间 sentinel failover-timeout mymaster 180000 # 同时只允许1个Slave做同步 sentinel parallel-syncs mymaster 1 # Master 密码 sentinel auth-pass mymaster your_password# 启动3个Sentinel redis-sentinel sentinel1.conf redis-sentinel sentinel2.conf redis-sentinel sentinel3.conf2.4 故障转移过程1. Master宕机 │ 2. Sentinels检测到Master无响应 │ 3. 超过quorum(2)个Sentinel标记为客观下线 │ 4. 选举Leader Sentinel │ 5. Leader选择最优Slave提升为Master: - 排除故障Slave - 选择offset最大的Slave（数据最新） - 优先级高的Slave优先 │ 6. 其他Slave指向新Master │ 7. 通知客户端新Master地址 │ 8. 旧Master恢…

Redis 分布式锁实现：从 SET NX 到 Redlock 算法
type: item
date: 2026-04-02T10:27:44+08:00
description: 前言分布式锁是分布式系统中控制资源访问的核心机制。Redis 因其单线程特性和高性能，是实现分布式锁的首选方案。本文将从最简单的实现出发，逐步演进到生产级的 Redlock 算法。一、为什么需要分布式锁1.1 单机锁的问题// 单机环境：PHP文件锁或flock可以解决 $fp = fopen('/tmp/lock.txt', 'w+'); flock($fp, LOCK_EX); // 加锁 // 执行业务逻辑 flock($fp, LOCK_UN); // 释放锁 fclose($fp);但在分布式环境中，请求可能被分配到不同服务器，文件锁无法跨机器生效。1.2 分布式锁的核心要求要求说明互斥性同一时刻只有一个客户端持有锁避免死锁锁必须有过期时间，防止持有者宕机导致永久锁死解铃还须系铃人只有锁的持有者才能释放锁高可用锁服务自身不能成为单点二、Redis 分布式锁演进2.1 版本一：SETNX（初版）// 最简单的实现 $lockKey = 'lock:order:1001'; // 尝试加锁 $redis->setnx($lockKey, 1); // 返回1表示成功，0表示失败 // 设置过期时间（防死锁） $redis->expire($lockKey, 30); // 业务逻辑 processOrder(); // 释放锁 $redis->del($lockKey);致命问题：setnx 和 expire 是两条命令，不保证原子性。如果 setnx 成功后服务崩溃，锁将永远不会过期，导致死锁。2.2 版本二：SET NX EX（推荐基础版）$lockKey = 'lock:order:1001'; $lockValue = uniqid('', true); // 生成唯一标识 // 一条命令完成加锁+过期，原子操作 $result = $redis->set($lockKey, $lockValue, ['NX', 'EX' => 30]); if ($result) { try { // 获取锁成功，执行业务 processOrder(); } finally { // 释放锁 $redis->del($lockKey); } } else { // 获取锁失败 throw new Exception('系统繁忙，请稍后重试'); }改进：SET key value NX EX 30 是原子操作，解决了死锁问题。遗留问题：释放锁时直接 DEL，可能删掉别人的锁。2.3 版本三：锁标识 + Lua 释放（安全版）$lockKey = 'lock:order:1001'; $lockValue = uniqid('', true); // 加锁 $result = $redis->set($lockKey, $lockValue, ['NX', 'EX' => 30]); if (!$result) { throw new Exception('获取锁失败'); } try { processOrder(); } finally { // 用Lua脚本安全释放锁：只有value匹配才删除 $luaScript = " if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end "; $redis->eval($luaScript, [$lockKey, $lockValue], 1); }为什么需要 Lua 脚本？时间线： T1: 客户端A GET lock → value=A T2: 锁过期，自动删除 T3: 客户端B SET lock NX → 成功，value=B T4: 客户端A DEL lock → 删掉了B的锁！Lua 脚本保证了 GET + 判断 + DEL 三步原子执行。2.4 版本四：锁续期（生产可用版）业务执行时间可能超过锁的过期时间，需要后台续期：class RedisLock { private $redis; private $lockKey; private $lockValue; private $ttl = 30; private $renewTimer = null; public function lock($key, $ttl = 30) { $this->lockKey = $key; $this->lockValue = uniqid('', true); $this->ttl = $ttl; // 尝试加锁，最多重试3次 for ($i = 0; $i < 3; $i++) { $result = $this->redis->set( $t…

MySQL 慢查询优化实战：从 EXPLAIN 到索引调优
type: item
date: 2026-03-02T05:49:47+08:00
description: 前言MySQL 慢查询是影响应用性能的常见瓶颈。本文从慢查询日志分析入手，结合 EXPLAIN 执行计划解读，系统讲解索引优化和 SQL 调优方法。一、慢查询日志1.1 开启慢查询日志-- 查看慢查询配置 SHOW VARIABLES LIKE "%slow_query%"; -- 开启慢查询日志 SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; -- 超过 1 秒的查询 SET GLOBAL slow_query_log_file = "/var/log/mysql/slow.log"; -- 记录未使用索引的查询 SET GLOBAL log_queries_not_using_indexes = ON;1.2 使用 mysqldumpslow 分析# 按平均查询时间排序，取前 10 mysqldumpslow -s t -t 10 /var/log/mysql/slow.log # 按总耗时排序 mysqldumpslow -s at -t 10 /var/log/mysql/slow.log # 按次数排序 mysqldumpslow -s c -t 10 /var/log/mysql/slow.log二、EXPLAIN 执行计划2.1 基本用法EXPLAIN SELECT * FROM orders WHERE user_id = 10086 AND status = "paid";2.2 关键字段解读字段说明重点关注type访问类型至少达到 range 级别，避免 ALLkey实际使用的索引不为 NULLrows预估扫描行数越小越好Extra附加信息避免 Using filesort 和 Using temporary2.3 type 类型（从好到差）system > const > eq_ref > ref > range > index > ALLconst：主键或唯一索引等值查询eq_ref：JOIN 时使用主键或唯一索引ref：非唯一索引等值查询range：索引范围查询（BETWEEN、>、<、IN）index：扫描整个索引树ALL：全表扫描，必须优化三、索引优化3.1 创建合适的索引-- 单列索引 CREATE INDEX idx_user_id ON orders(user_id); -- 联合索引（注意最左前缀原则） CREATE INDEX idx_user_status ON orders(user_id, status, created_at); -- 覆盖索引（查询字段都在索引中） CREATE INDEX idx_cover ON orders(user_id, status, amount);3.2 最左前缀原则-- 联合索引 (a, b, c) -- 能命中索引： SELECT * FROM t WHERE a = 1; SELECT * FROM t WHERE a = 1 AND b = 2; SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3; -- 不能命中索引： SELECT * FROM t WHERE b = 2; SELECT * FROM t WHERE c = 3; SELECT * FROM t WHERE b = 2 AND c = 3;3.3 避免索引失效-- 函数操作导致索引失效 SELECT * FROM orders WHERE YEAR(created_at) = 2026; -- 优化： SELECT * FROM orders WHERE created_at >= "2026-01-01" AND created_at < "2027-01-01"; -- 隐式类型转换 SELECT * FROM orders WHERE order_no = 10086; -- order_no 是 varchar -- 优化： SELECT * FROM orders WHERE order_no = "10086"; -- LIKE 前导通配符 SELECT * FROM users WHERE name LIKE "%张"; -- 优化：使用全文索引或调整业务逻辑 SELECT * FROM users WHERE name LIKE "张%"; -- OR 导致索引失效 SELECT * FROM orders WHERE user_id = 1 OR status = "paid"; -- 优化：使用 UNION ALL SELECT * FROM orders WHERE user_id = 1 UNION ALL SELECT …