首页
直播
壁纸
友链
搜索
1
微信小程序支付全链路实战:JSAPI 下单、调起支付、回调验签与退款
266 阅读
2
微信小程序云开发实战:云函数、云数据库与云存储的正确使用姿势
256 阅读
3
微信小程序自定义 tabBar 实战:custom-tab-bar 从适配到深色模式
255 阅读
4
微信小程序 Skyline 渲染引擎实战:worklet 动画从原理到落地
253 阅读
5
微信小程序分包进阶:独立分包、预下载与分包异步化实战
246 阅读
服务器运维
后端技术
前端技术
梯子
数据库
小程序
登录
搜索
标签搜索
fastadmin
Redis
微信小程序
前端开发
RabbitMQ
Go
服务器
codex
buildadmin
小程序
mysql
Nginx
Docker
Vue3
Node.js
MySQL优化
Linux
TypeScript
JWT
PHP
沿途的风景
累计撰写
74
篇文章
累计收到
0
条评论
首页
栏目
服务器运维
后端技术
前端技术
梯子
数据库
小程序
页面
直播
壁纸
友链
搜索到
2
篇与
» MySQL优化
的结果
2026-08-15
webman 数据库实战:连接池、ORM 选型、事务边界与大数据处理
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, $skuId); // 建订单 Db::commit(); } catch (\Throwable $e) { Db::rollback(); throw $e; // 异常继续抛,别吞 }铁律二:事务里禁止外部 IO事务持锁期间调外部 HTTP / 发 MQ / 写 Redis,一旦外部服务慢,数据库连接和行锁被拖住,并发一上来就是雪崩。// ❌ 事务里调第三方风控接口(300ms),行锁拖 300ms Db::startTrans(); $stock = Stock::lock(true)->find($skuId); // SELECT ... FOR UPDATE RiskApi::check($uid); // 外部调用! $stock->num -= 1; $stock->save(); Db::commit(); // ✅ 外部检查前置,事务只做纯 DB 操作 RiskApi::check($uid); // 先调外部 Db::startTrans(); $stock = Stock::lock(true)->find($skuId); if ($stock->num < 1) throw new OutOfStock(); $stock->num -= 1; $stock->save(); Db::commit(); PublishOrderEvent::dispatch($order); // 后置异步动作铁律三:事务尽量短,锁尽量准悲观锁(lock(true))只锁必要的行,锁定前先校验数据存在的条件能用唯一索引防重(幂等)就别用"先查再插 + 锁"补充:嵌套事务与 savepointthink-orm 支持 startTrans 嵌套(底层转 savepoint),但事务边界只写在 Service 层一处,不要 Controller 包一层、Service 再包一层——嵌套事务的回滚语义很容易把人绕晕。五、大数据量:分批处理的三个工具1. chunk 分批读User::where('status', 1)->chunkById(1000, function ($users) { foreach ($users as $user) { // 逐批处理 } });用 chunkById 而不是 chunk:处理过程中如果更新了 offset 相关字段,chunk(基于 offset/limit)会漏数据或死循环,chunkById(基于主键游标)不会。2. 慎用 ORM 批量,必要时裸 SQLORM 逐条 save 在万级数据下慢得无法接受:// ❌ 10 万条逐条 update:几十分钟 foreach ($rows as $row) { Model::where('id', $row['id'])->update($row); } // ✅ 事务 + 批量拼接(注意分批,每批 ≤ 1000) Db::startTrans(); foreach (array_chunk($rows, 500) as $batch) { Db::table('users')->upsert($batch, ['id'], ['name', 'status']); // 批量 upsert } Db::commit();3. 超大导出:游标 + 流式输出百万行导出别 ORM ->get() 全量载入(内存直接爆),用游标逐行读、边读边写 CSV 流:$fp = fopen('php://output', 'w'); User::where('created', '>', $start)->select()->cursor()->each(function ($u) use ($fp) { fputcsv($fp, $u->toArray()); });cursor() 用 PDO 的 unbuffered 模式,内存占用恒定。六、读写分离与多库配置'connections' => [ 'mysql' => [ /* 主库写 */ ], 'mysql_read' => [ /* 从库读 */ ], ],think-orm/Eloquent 都支持配置读写分离自动路由,但强制主读(写后立读的场景:下单后立即查详情)必须显式指定主库连接,否则撞上主从延迟会查到旧数据:// 写后立刻要读的,强制走主库 $inDb = Order::on('mysql')->find($orderId);七、上线前检查清单[ ] 最大连接数核算:worker 数 × 每进程连接数 < MySQL max_connections × 0.8[ ] break_reconnect 开启 + 心跳进程就位[ ] 事务全部 try/catch/rollback 完整闭合(全局搜 startTrans 逐个人工审计)[ ] 长事务里无外部 IO(APM 看事务平均持锁时长)[ ] 大数据任务用 chunkById / cursor,无全量 ->get()[ ] ORM 字段缓存开启,生产关掉 SQL 日志写在最后数据库层的所有坑都指向同一个根因:连接的生命周期从"请求级"变成了"进程级"。把连接当进程级资源来管理(池化、心跳、重连),把事务当危险品来对待(短小、闭合、无 IO),webman 的数据库层就稳了。下一篇进入性能深水区:协程、连接池调优与压测。
2026年08月15日
6 阅读
0 评论
0 点赞
2026-03-02
MySQL 慢查询优化实战:从 EXPLAIN 到索引调优
前言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 * FROM orders WHERE status = "paid" AND user_id != 1;四、SQL 调优实战4.1 分页优化-- 慢:OFFSET 大时性能差 SELECT * FROM orders ORDER BY id LIMIT 1000000, 20; -- 优化方案 1:延迟关联 SELECT * FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id; -- 优化方案 2:游标分页(记住上一页最后一条 ID) SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 20;4.2 批量插入-- 慢:循环单条插入 INSERT INTO logs (msg) VALUES ("log1"); INSERT INTO logs (msg) VALUES ("log2"); -- 优化:批量插入 INSERT INTO logs (msg) VALUES ("log1"), ("log2"), ("log3");4.3 使用 Redis 缓存热点数据import redis import json r = redis.Redis(host="localhost", port=6379, db=0) def get_user(user_id): # 先查缓存 cache_key = f"user:{user_id}" cached = r.get(cache_key) if cached: return json.loads(cached) # 查数据库 user = db.query("SELECT * FROM users WHERE id = %s", user_id) if user: r.setex(cache_key, 3600, json.dumps(user)) # 缓存 1 小时 return user五、总结MySQL 慢查询优化的核心思路:先通过慢查询日志找到问题 SQL,再用 EXPLAIN 分析执行计划,最后通过添加索引、调整 SQL 写法、引入缓存等方式优化。记住:不是所有慢查询都需要加索引,有时调整业务逻辑比优化 SQL 更有效。
2026年03月02日
15 阅读
0 评论
0 点赞
0:00