Docker容器启动失败查看日志排查
容器一启动就退出、docker ps 里根本看不到,是 Docker 新手最常见的问题。很多人反复 run 却不看日志,自然找不到原因。关键原则:容器退出不代表没记录,用 logs 和 inspect 看它为什么挂。本文给出一套从日志到配置的排查顺序。
一、先确认容器状态
# 只看正在运行的,挂掉的容器不会出现
docker ps
# 看所有容器,包括已退出的,记录 CONTAINER ID
docker ps -a
状态为 Exited (1) 或 Exited (0),括号里就是退出码:0 是正常退出,非 0 是异常。
二、看容器日志(最关键一步)
# 看全部日志
docker logs 容器名或ID
# 实时跟踪最新输出,类似 tail -f
docker logs -f 容器名
# 只看最近 100 行
docker logs --tail 100 容器名
# 带时间戳
docker logs -t 容器名
日志里通常直接写了错误,比如配置文件找不到、端口被占、数据库连不上、权限拒绝。
三、查看容器详细退出原因
docker inspect 容器名 | grep -A 10 State
关注 ExitCode、Error、OOMKilled:OOMKilled 为 true 说明内存不足被杀死。
四、常见启动失败原因
1. 前台进程退出,容器就结束
容器必须有一个前台进程一直跑,命令执行完就退出,容器自然停。比如 nginx 镜像你用 nginx -s stop 或直接执行个脚本,跑完就退。要保证 CMD 以前台方式运行。
2. 配置文件/挂载路径错误
docker run -v /host/etc/nginx:/etc/nginx nginx
宿主机路径不存在、或挂载把镜像内配置覆盖成空,服务起不来。
3. 端口冲突
docker: Error starting userland proxy: listen tcp 0.0.0.0:80: bind: address already in use
宿主机 80 已被占用,换 -p 8080:80。
4. 数据库等依赖没就绪
应用启动时连不上 MySQL,直接退出。可用 restart 策略或等待脚本。
5. 权限不足 / 镜像命令写错
docker run 末尾的命令拼错,或挂载目录权限导致进程无法写文件。
五、反复重启看日志
# 查看实时日志验证修复
docker logs -f --tail 50 容器名
必要时用交互方式进临时容器复现:
docker run -it --entrypoint sh 镜像名
常见坑
- 只看 docker ps,挂掉的容器不在列表里,忘了 docker ps -a。
- 不看 logs 反复 run,浪费时间。
- 后台进程导致容器立刻退出,误以为镜像坏了。
- 端口被占却怀疑镜像,先查宿主机端口。
- OOMKilled 被忽略,实际是内存限制太小。
- 挂载路径拼反或覆盖了镜像内文件。
- 改了代码没重新 build,跑的还是旧镜像。
总结
容器启动失败排查主线:docker ps -a 找容器 → docker logs -f --tail 看真实报错 → docker inspect 看 ExitCode 和 OOMKilled → 对照端口、挂载、前台进程、依赖逐项修。日志是第一线索,别跳过它直接猜。
遇到问题加QQ23979811 协助处理
PHP开启错误显示php.ini配置方法
页面白屏、只报 500 却看不到具体错误,是 PHP 新手最头疼的问题。生产环境默认把错误藏起来是对的,但本地和调试环境必须能看到报错,否则只能盲猜。通过 php.ini 和脚本内函数两层控制错误显示与记录,下面讲清每个参数怎么配。
一、先找到 php.ini 位置
php --ini
或在 PHP 文件里打印:
<?php phpinfo(); ?>
页面里 Loaded Configuration File 那行就是当前生效的配置,改别的文件不生效。
二、调试环境要改的四个参数
在 php.ini 里:
; 把错误输出到页面(调试开,生产关)
display_errors = On
display_startup_errors = On
; 报告所有错误,包括通知和弃用提示
error_reporting = E_ALL
; 错误同时写进日志,便于排查致命错误
log_errors = On
error_log = /var/log/php_errors.log
改完重启 PHP-FPM 或 web 服务才生效:
systemctl restart php-fpm
三、脚本内临时开启(不改配置)
有时拿不到 php.ini,可在页面最前面加:
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
注意:致命错误(如内存耗尽、语法错误)发生在这行之前就不会被页面显示,这种必须看错误日志。
四、error_reporting 常用级别
error_reporting(E_ALL); // 全部,调试用
error_reporting(E_ALL & ~E_NOTICE); // 忽略通知
error_reporting(E_ERROR | E_WARNING); // 只报错误和警告
五、生产环境务必关闭页面显示
display_errors = Off
log_errors = On
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
生产把错误直接回给用户会泄露路径、数据库结构等敏感信息,只写日志、不显示。
六、常见坑
- 改了 php.ini 没重启 FPM,以为没生效。
- 多个 php.ini(CLI 和 FPM 各一份),命令行和网页行为不一致。
- 致命错误发生在 ini_set 之前,页面依旧白屏,只能查 error_log。
- display_errors 开着但 error_reporting 把错误级别滤掉了,照样看不到。
- 日志目录不存在或 PHP 用户无写权限,log_errors 等于白开。
- 生产环境误开 display_errors,造成信息泄露。
- Nginx/FPM 层有自己的错误压制,页面显示和日志要分别确认。
总结
调试开 display_errors=On + error_reporting=E_ALL,生产关页面显示、只保留 log_errors。拿不到配置就用 ini_set 在脚本顶面临时开,致命错误永远以日志为准。改完一定重启服务再验证。
遇到问题加QQ23979811 协助处理
MySQL索引失效常见场景避免踩坑
建了索引却不走,查询照样全表扫描,是 MySQL 优化里最常见的坑。索引不是建了就一定用,优化器要根据数据分布、写法、类型判断值不值得走。下面把高频失效场景列清楚,配合 explain 验证。
一、先判断走没走索引
EXPLAIN SELECT * FROM user WHERE phone = '13800000000';
看 type 列:const/ref/range 是走了索引;ALL 就是全表扫描。key 列显示实际用到的索引,rows 是预估扫描行数。
二、常见失效场景
1. 对索引列做函数、运算、转换
-- 失效:对列用函数
SELECT * FROM user WHERE LEFT(phone,3) = '138';
-- 失效:算术运算
SELECT * FROM order WHERE id + 1 = 100;
正确写法把运算放到右边:
SELECT * FROM user WHERE phone LIKE '138%';
SELECT * FROM order WHERE id = 99;
2. 隐式类型转换
phone 是 varchar,却传数字,字符串被转成数字导致索引失效:
-- 失效:字符串列传数值
SELECT * FROM user WHERE phone = 13800000000;
-- 正确:加引号
SELECT * FROM user WHERE phone = '13800000000';
3. 复合索引违反最左前缀
索引 (a,b,c),必须从最左列 a 开始用。跳过 a 直接查 b、c 就走不全:
-- 走索引:用到 a
SELECT * FROM t WHERE a = 1;
-- 部分用到:a、b
SELECT * FROM t WHERE a = 1 AND b = 2;
-- 失效:没带最左列 a
SELECT * FROM t WHERE b = 2;
4. 用了 or,且其中一列无索引
-- name 无索引,整个 OR 导致全表扫描
SELECT * FROM user WHERE phone = 'x' OR name = '张三';
5. LIKE 以 % 开头
-- 失效:前导通配符
SELECT * FROM user WHERE name LIKE '%张';
-- 可用:右匹配
SELECT * FROM user WHERE name LIKE '张%';
6. 范围查询后右侧列用不上
复合索引里遇到 >、<、between、like 后,后面的列索引截断。
7. 优化器估算全表更快
表数据量小、或 where 命中比例太高(超过约 20%~30%),优化器认为扫全表比回表划算,主动放弃索引,这属于正常选择。
三、验证与整改
每次改写法都用 explain 复核:
# 看慢查询里有没有全表扫描
grep -E "Rows_examined" slow.log | head
整改思路:索引列保持原样不做函数运算、类型严格匹配、复合索引遵守最左前缀、OR 两侧都建索引或改 UNION。
常见坑
- 以为建了联合索引随便查哪列都行,忽略最左前缀。
- varchar 列忘加引号,隐式转换静默失效。
- 在 where 里写
IFNULL、SUBSTRING、DATE()包裹索引列。 !=、NOT IN常导致放弃索引。- 索引列参与
+ - *计算。 - 数据量小却纠结索引,反而被优化器忽略,属正常。
- 改完不看 explain,凭感觉判断。
总结
索引失效大多来自「在索引列上动手脚」:函数运算、隐式转换、违反最左前缀、前导 %、OR 缺索引。改完写法一定用 explain 的 type 和 key 列验证,确认从 ALL 变成 ref/range 才算修好。
遇到问题加QQ23979811 协助处理
Nginx报502 Bad Gateway排查思路
502 Bad Gateway 是 Nginx 反向代理时最常见的报错:Nginx 自己能响应,但它转发给后端(通常是 PHP-FPM 或上游服务)拿不到有效回应,于是返回 502。注意它和 504 不同:504 是后端超时,502 是连接被拒、进程挂掉或返回了非法响应。按下面顺序排查,绝大多数都能定位。
一、先看两个关键日志
# Nginx 错误日志,先看这里
tail -100 /var/log/nginx/error.log
# PHP-FPM 日志
tail -100 /var/log/php-fpm/error.log
Nginx error.log 会直接写出原因,比如 connect refused、upstream prematurely closed、recv() failed,对应不同病根。
二、检查 PHP-FPM 是否在运行
systemctl status php-fpm
# 或按版本名 php74-fpm
ps aux | grep php-fpm
进程没起来,Nginx 连不上端口或 socket,就是 502。先把它启动:
systemctl start php-fpm
systemctl enable php-fpm
三、检查监听端口或 Socket 是否匹配
Nginx 配置里 fastcgi_pass 要和 FPM 监听一致。常见两种:
# 方式一:TCP 端口
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 方式二:Unix Socket
fastcgi_pass unix:/run/php-fpm/www.sock;
对照 FPM 的 www.conf:
listen = 127.0.0.1:9000
; 或
listen = /run/php-fpm/www.sock
两边不一致,Nginx 连不上就 502。
四、FPM 进程池耗尽或崩溃
并发高、php-fpm 子进程被打满,新请求接不住。看 www.conf 的 pm 参数:
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.max_requests = 500
# 查看当前 FPM 进程数
ts=$(ps -C php-fpm -o pid= | wc -l); echo $ts
内存不足时 FPM 会被 OOM 杀掉,dmesg 里找 out of memory。
五、脚本本身导致的问题
- PHP 段错误、内存超限(memory_limit)、执行超时,FPM worker 崩溃,连接被中断,Nginx 报 upstream prematurely closed。
- 文件权限:Nginx 读不到 PHP 文件、或 FPM 无执行权限,也会 502。
- SCRIPT_FILENAME 路径写错,FPM 找不到脚本。
六、常见坑
- 改完 FPM 配置只 reload Nginx,忘了重启 php-fpm,参数不生效。
- socket 文件属主和 Nginx 用户不一致,权限拒绝。
- SELinux 或防火墙拦了 9000 端口。
- 多版本 PHP(7.4/8.0)同时装,起的不是 Nginx 指向的那个 FPM。
- 把 502 和 504 混为一谈:502 先查进程和连接,504 先查超时和慢查询。
- 容器部署时 FPM 和 Nginx 不在同一网络,主机名解析失败。
- 只重启服务不看日志,反复重启却找不到根因。
总结
502 的排查主线:先看 error.log 定原因 → 确认 FPM 在跑 → 核对 fastcgi_pass 与 listen 是否一致 → 看进程池是否打满或被 OOM → 检查脚本权限与内存。定位到具体一行日志,基本就能一次修好。
遇到问题加QQ23979811 协助处理
Linux日志切割logrotate配置详解
服务器跑久了,Nginx、MySQL、PHP 的日志会越滚越大,把磁盘吃到 100%。手动删日志不现实,而且服务还在往旧文件里写。logrotate 是 Linux 自带的日志切割工具,按天或按大小自动归档、压缩、删旧,并通知服务重新打开日志句柄。本文讲清配置位置、关键字段和常见坑。
一、logrotate 工作机制
logrotate 由系统定时任务驱动,通常在 /etc/cron.daily/logrotate 里每天跑一次。它读取 /etc/logrotate.conf 和 /etc/logrotate.d/ 下的配置,对符合条件的日志执行切割、压缩、保留策略。
# 查看默认主配置
cat /etc/logrotate.conf
# 查看各软件独立配置
ls /etc/logrotate.d/
二、一个完整 Nginx 日志配置示例
在 /etc/logrotate.d/nginx 写入:
/var/log/nginx/*.log {
daily # 按天切割
missingok # 日志不存在不报错
rotate 14 # 保留 14 份历史
compress # 旧日志 gzip 压缩
delaycompress # 延迟一次再压缩,避免抢写
notifempty # 空文件不切割
create 0640 nginx adm # 新建日志的权限和属主
sharedscripts # 多个日志只执行一次脚本
postrotate
if [ -f /var/run/nginx.pid ]; then
kill -USR1 `cat /var/run/nginx.pid`
fi
endscript
}
三、常用关键字段说明
daily:按天切割,也可换weekly、monthly。size 100M:超过大小就切割,和时间条件可共存。rotate 14:保留份数,超出自动删除最旧的。compress/nocompress:是否压缩历史日志。notifempty:文件为空则跳过,防止产生一堆空归档。dateext:归档名带日期,如 access.log-20261115.gz。postrotate ... endscript:切割后执行的命令,最常见是给服务发信号重开日志。
四、手动测试配置
改完配置别等明天,先强制跑一遍验证:
# 只检查语法,不真正执行
logrotate -d /etc/logrotate.d/nginx
# 强制执行一次切割
logrotate -f /etc/logrotate.d/nginx
-d 是调试模式,会打印它打算怎么处理但不落盘,排错首选。
五、常见坑
- 切割后服务仍往旧文件写:必须在 postrotate 里给 Nginx 发 USR1 信号或 reload,让它重新打开日志文件,否则新日志写进归档。
- 压缩和写入冲突:同一次切割立刻压缩正在被写的文件会报错,用
delaycompress延后一轮。 - 路径用通配符 /var/log/nginx/*.log 时,别把已经切割压缩的归档再匹配进去。
rotate份数设太小,误删还想查的历史;设太大又占磁盘,按业务保留周期定。- 自定义配置文件放错目录:必须在 /etc/logrotate.d/ 下,主配置里要 include 该目录。
- 容器内日志不走 logrotate,要用 Docker 日志驱动或应用自身轮转。
- 改完没 reload,系统 cron 第二天才生效,紧急时手动
-f执行。
总结
logrotate 靠「时间或大小触发 + 保留份数 + 压缩 + postrotate 通知服务」解决日志膨胀。配置写在 /etc/logrotate.d/,改完先用 -d 调试、再用 -f 强制执行验证,关键别漏掉切割后让服务重开日志的信号。
遇到问题加QQ23979811 协助处理
Git撤销commit回退版本命令详解
提交写错、信息填错、误把不该提交的文件 commit 上去,是 Git 日常最高频的失误。关键要先分清两件事:commit 还没推送到远程,用 reset 本地回退;已经推送到共享分支,用 revert 生成一个反向提交,不要硬改历史。选错命令轻则改乱本地,重则让队友拉取冲突。本文把常用撤销和回退命令讲清。
一、查看提交历史
先看清楚要回退到哪:
git log --oneline
a1b2c3d 最新提交
f4e5d6c 上一个提交
9a8b7c6 更早提交
二、只改提交信息(未推送)
刚 commit 完发现 message 写错,不改内容:
git commit --amend -m '正确的提交信息'
三、撤销最近一次 commit,保留改动(未推送)
--soft 回退到上一次提交,但改动留在暂存区,可以重新提交:
git reset --soft HEAD~1
--mixed(默认)回退并把改动放回工作区、取消暂存:
git reset --mixed HEAD~1
--hard 彻底丢弃最近一次提交的所有改动,慎用:
git reset --hard HEAD~1
回退到指定提交:
git reset --hard f4e5d6c
四、已推送到远程:用 revert
共享分支上别用 reset 强推,改用 revert 生成一个抵消提交:
git revert HEAD
git revert f4e5d6c
它保留完整历史,新增一条「反向提交」,队友正常 pull 即可。
五、误提交文件的处理
把不该提交的文件加进去了,未推送时:
# 从暂存区移除但保留本地文件
git reset HEAD 文件名
# 或直接编辑后重新提交
git rm --cached 文件名
git commit --amend
六、三种 reset 参数对比
--soft:撤销 commit,改动留在暂存区。--mixed:撤销 commit,改动退回工作区(默认)。--hard:撤销 commit 并删除改动,不可恢复。
常见坑
- 已 push 的分支用
reset --hard再强推,会覆盖别人的提交,必须用 revert。 reset --hard丢弃的改动若没备份,工作区改动直接消失。- amend 会生成新 commit,已推送的提交 amend 后哈希变化,团队协作会乱。
- HEAD~1 表示上一个,HEAD~2 表示倒数第二个,别数错。
- 回退后想找回,可用
git reflog查看历史操作记录。 - 在公共分支强推前务必和团队确认,避免他人仓库历史分叉。
总结
未推送:改信息用 amend,保留改动用 reset --soft/--mixed,彻底丢弃用 --hard。已推送共享分支:一律 revert 生成反向提交。拿不准先 git log、git reflog 确认,再决定回退方式。
遇到问题加QQ23979811 协助处理
Docker数据卷与挂载目录区别详解
容器是临时的,删容器数据就没了,所以持久化必须靠挂载。Docker 提供三种存数据的方式:数据卷(volume)、绑定挂载(bind mount)和 tmpfs。新手最容易混淆 volume 和 bind mount,选错会导致权限乱、迁移难、数据丢失。本文讲清两者区别、适用场景和命令写法。
一、数据卷 volume(推荐)
由 Docker 自己管理,存放在 Docker 的目录下(Linux 一般在 /var/lib/docker/volumes/),完全脱离宿主机目录结构。
创建并运行:
# 创建命名卷
docker volume create mydata
# 挂载到容器
docker run -d -v mydata:/var/lib/mysql mysql:8.0
也可以直接在 run 时让 Docker 自动建卷:
docker run -d -v mysql_data:/var/lib/mysql mysql:8.0
查看卷、删除卷:
docker volume ls
docker volume inspect mydata
docker volume rm mydata
二、绑定挂载 bind mount
直接把宿主机上的某个真实目录或文件挂进容器,路径由你指定:
docker run -d -v /home/www/html:/usr/share/nginx/html nginx
也可以用新版 --mount 写法,更清晰:
docker run -d \
--mount type=bind,source=/home/www/html,target=/usr/share/nginx/html \
nginx
三、核心区别对比
- 归属:volume 归 Docker 管理;bind mount 是宿主机真实目录。
- 跨平台:volume 与宿主机路径解耦,Windows、Mac、Linux 都能用;bind mount 强依赖宿主机目录。
- 性能:volume 由 Docker 优化,大数据量、数据库推荐用卷;bind mount 在共享代码目录时很方便。
- 迁移备份:volume 可用 docker volume 命令和内置备份工具操作;bind mount 直接用宿主机工具拷目录。
- 容器间共享:多个容器挂同一个 volume 很容易;bind mount 也行但受目录权限限制。
四、怎么选
- 数据库、持久化业务数据:用 volume,稳定可移植。
- 开发时代码热更新、配置文件外挂:用 bind mount,改宿主机文件容器立刻生效。
- 只存临时内存数据:用 tmpfs,重启即消。
五、常见坑
- bind mount 路径写错,容器启动报 No such file or directory,先确认宿主机目录存在。
- 宿主机目录属主是普通用户,容器内进程以 root 写文件导致权限拒绝。
- 用
-v时前面路径不存在,Docker 在 Linux 会自动当成卷创建,造成预期外行为,绑定挂载前先 mkdir。 - 删容器时没加
-v,volume 不会随容器删除,长期堆积占磁盘,定期docker volume prune清理。 - Windows/Mac 挂载宿主机目录性能差,数据库不要用 bind mount。
- 混淆大小写路径,
source和target写反导致挂载点不对。
总结
持久化业务数据选 volume,交给 Docker 管理、跨平台好迁移;开发调试外挂代码和配置选 bind mount,直接映射宿主机目录。记牢:数据库用卷、代码用挂载,删容器前想清楚卷要不要一起删。
遇到问题加QQ23979811 协助处理
MySQL REGEXP正则匹配查询用法
MySQL 的 REGEXP(同义 RLIKE)用于按正则表达式匹配字符串,比 LIKE 通配符强大得多。LIKE 只能写 % 和 _ 做简单前后模糊,遇到「以数字开头」「包含连续三个字母」「手机号段校验」这类条件就力不从心,这时就要用 REGEXP。本文讲清基本语法、常用元字符和实战查询。
一、基本写法
SELECT * FROM users WHERE username REGEXP '^admin';
REGEXP 放在字段和模式之间,匹配成功返回 1。取反用 NOT REGEXP:
SELECT * FROM users WHERE email NOT REGEXP '@';
注意:REGEXP 不区分大小写(取决于校对集),要严格区分可加 BINARY。
二、常用元字符
^匹配开头,$匹配结尾。.任意单个字符。*前项出现 0 次或多次,+出现 1 次以上,?0 或 1 次。[abc]字符集合,[0-9]数字,[a-z]小写字母。{n,m}重复 n 到 m 次。|或,如cat|dog。
三、实战示例
以数字开头的用户名:
SELECT * FROM users WHERE username REGEXP '^[0-9]';
校验手机号(11 位、1 开头):
SELECT * FROM users WHERE phone REGEXP '^1[3-9][0-9]{9}$';
包含连续两个相同字母:
SELECT * FROM words WHERE name REGEXP '([a-z])\\1';
匹配邮箱大致格式:
SELECT * FROM users WHERE email REGEXP '^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-z]{2,}$';
四、REGEXP 与 LIKE 怎么选
-- LIKE:只支持 % 与 _
SELECT * FROM goods WHERE title LIKE '%手机%';
-- REGEXP:规则化匹配
SELECT * FROM goods WHERE title REGEXP '手机|平板|电脑';
简单包含用 LIKE 更省资源;需要边界、字符类、重复次数时用 REGEXP。
五、常见坑
- REGEXP 走不到普通索引,大表全表扫描会很慢,只适合小表或后台筛选。
- 反斜杠在 SQL 字符串里要写两个
\\,否则被 MySQL 先解析一层。 - 忘记写
^和$,变成只要包含即可,结果范围过大。 - 中文匹配受字符集影响,
[一-龥]类写法在 utf8mb4 下不一定可靠。 - 把正则当强校验,前端/后端仍需独立校验,数据库正则只是粗筛。
- 大小写敏感需求忘了加
BINARY。
总结
REGEXP 适合规则化的字符串筛选:^、$ 定边界,[] 定字符集,{n,m} 定重复次数,竖线表或。它不走索引,别在大表高频查询里硬用;简单模糊仍优先 LIKE。
遇到问题加QQ23979811 协助处理
Nginx proxy_pass斜杠路径转发区别
Nginx 反向代理里 proxy_pass 末尾到底带不带斜杠,是最容易写错、也最容易导致后端 404 的地方。同样一个 location,proxy_pass 有没有结尾斜杠,转发给后端的 URL 路径完全不同。很多人配置后后端报 404、路径多一截或少一截,问题就出在这个斜杠上。本文把规则讲清并给对比例子。
一、核心规则
proxy_pass 的目标地址分两种写法:
- 带协议和主机,末尾可带路径或斜杠:
http://127.0.0.1:8080/ - 不带路径(纯主机):
http://127.0.0.1:8080
关键判断:当 proxy_pass 后带了 URI(哪怕只是一个 /),Nginx 会把 location 匹配到的那一段替换掉;不带 URI 时,则把完整原始路径原样拼到后端。
二、带斜杠:替换匹配段
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}
访问 /api/user/list,匹配 /api/,这一段被替换成 /,后端收到的是:
http://127.0.0.1:8080/user/list
/api/ 前缀被剥掉了。
三、不带斜杠:原样转发
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
访问 /api/user/list,后端收到完整路径:
http://127.0.0.1:8080/api/user/list
/api/ 保留,原样透传。
四、带自定义路径的情况
location /api/ {
proxy_pass http://127.0.0.1:8080/backend/;
}
访问 /api/user,匹配段 /api/ 被替换成 /backend/,后端收到:
http://127.0.0.1:8080/backend/user
五、完整反向代理示例
location /app/ {
proxy_pass http://127.0.0.1:9000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
改完执行 nginx -t 校验,再 nginx -s reload 重载。
常见坑
- 后端本身就需要
/api/前缀,你却加斜杠剥掉,结果全 404。 - proxy_pass 写了主机又写端口,末尾漏斜杠导致路径重复。
- location 用正则
~时不能在 proxy_pass 里写固定路径,需用变量拼接。 - 只改配置没
nginx -t校验,重载失败导致整站 502。 - 后端框架按转发路径找路由,前缀不一致就报路由不存在。
- 误以为斜杠只是美观符号,实际它决定路径替换规则。
总结
记住一句话:proxy_pass 后带 URI(含 /)就替换 location 匹配段,不带就原样透传完整路径。先确认后端路由要不带 /api/,再决定斜杠去留,改完务必 nginx -t 再重载。
遇到问题加QQ23979811 协助处理
Linux rsync增量同步备份命令用法详解
rsync 是 Linux 下做文件同步和备份最常用的工具,核心优势是增量传输:只传有变化的部分,第一次全量、后续只补差异,比 scp、ftp 整包复制省大量带宽和时间。网站目录异地备份、两台服务器镜像、本地定时同步都靠它。本文讲清常用参数、远程同步写法和容易踩的坑。
一、本地目录同步
把 source 目录内容同步到 dest,保持一致:
rsync -av /data/web/ /backup/web/
-a归档模式,保留权限、属主、时间、软链接,等于-rlptgoD,备份必用。-v显示传输过程。
注意结尾斜杠:/data/web/ 表示同步目录里的内容;不带斜杠 /data/web 会把 web 目录本身拷过去,目标里多套一层。
二、通过 SSH 远程同步
拉取远端到本地:
rsync -avz root@192.168.1.10:/var/www/html/ /local/backup/
推送本地到远端:
rsync -avz /local/backup/ root@192.168.1.10:/var/www/html/
-z传输时压缩,跨公网必加,省带宽。- 走 SSH,默认 22 端口;改了端口用
-e 'ssh -p 2222'。
三、常用关键参数
# 删除目标端多出的文件,让两端严格一致(镜像)
rsync -av --delete /data/web/ /backup/web/
# 先演练不真正执行,检查会传什么、删什么
rsync -avn --delete /data/web/ /backup/web/
# 显示进度条和传输速率
rsync -av --progress /src/ /dst/
# 排除指定文件/目录
rsync -av --exclude='logs/' --exclude='*.tmp' /src/ /dst/
-n(dry-run)特别重要,配合 --delete 先演练,避免误删。
四、做定时备份脚本
结合 crontab 每天增量备份网站:
#!/bin/bash
rsync -avz --delete --exclude='cache/' \
root@192.168.1.10:/var/www/html/ \
/backup/web/$(date +%F)/
配密钥免密后加入 crontab,凌晨自动跑:
crontab -e
0 3 * * * /usr/local/bin/backup.sh
五、常见坑
- 结尾斜杠忘加,目标目录凭空多套一层,备份路径错乱。
--delete没加-n演练,误删目标端已有文件。- 远程用密码交互,定时任务卡住,必须配 SSH 密钥免密。
- 跨机器属主不一致,
-a在普通用户下可能无法保留 uid,必要时加--fake-super或用 root。 - 大量小文件首次全量慢属正常,后续增量很快,别误以为卡死。
- 防火墙或安全组没放行 SSH 端口,连接直接超时。
总结
rsync 备份记住三点:-avz 保权限并压缩;源目录结尾斜杠决定是否套层;--delete 先 -n 演练再执行。远程同步配密钥免密交给 crontab,即可实现稳定的每日增量异地备份。
遇到问题加QQ23979811 协助处理
PHP 命名空间 namespace 入门详解
PHP 命名空间 namespace 入门详解
一、命名空间解决什么问题
随着项目变大、引入第三方库,两个类同名(比如都叫 User)就会冲突,函数、常量重名也会报错。命名空间(namespace)相当于给代码划分独立隔间,让同名类在不同隔间里互不干扰,也是 Composer 自动加载 PSR-4 的基础。
二、基本声明
namespace 必须写在文件最顶部,前面不能有输出:
<?php
namespace App\Controller;
class User
{
public function show()
{
return '控制器里的用户';
}
}
反斜杠 \ 是层级分隔符,App\Controller 表示嵌套命名空间。
三、三种引用方式
在别的命名空间里使用这个类,有三种写法:
<?php
namespace App\View;
// 1. 全限定名,带开头反斜杠,从全局根开始找
$a = new \App\Controller\User();
// 2. use 引入后直接用(最常用)
use App\Controller\User;
$b = new User();
// 3. 起别名,类名冲突时用
use App\Controller\User as ControllerUser;
$c = new ControllerUser();
四、use 的规则
use只作用于当前文件,每个文件要单独引入;- 一个 use 只引入一个类,多个类用逗号或分行:
use App\Controller\User; use App\Model\Order; - 函数和常量在 PHP 8.0 前需要
use function、use const单独引入; - 开头的
\表示全局空间,比如\count()强制用全局函数,避免被当前空间的同名函数遮蔽。
五、非限定、限定、完全限定名
namespace App\Controller;
new User(); // 非限定名,当前空间内找
new Model\User(); // 限定名,相对当前空间
new \App\Model\User(); // 完全限定名,从根开始
六、结合自动加载
命名空间路径要和目录结构对应,PSR-4 下 App\Controller\User 对应 App/Controller/User.php,Composer 据此自动 require:
{
"autoload": {
"psr-4": { "App\\": "app/" }
}
}
命名空间写错或目录对不上,就会报“类未找到”。
七、常见误区
namespace写在 echo、HTML 之后,语法报错;- 以为 use 能跨文件自动继承,别的文件仍需重新 use;
- 类名冲突时不起别名,两个 use 同名类后者覆盖;
- 全限定名漏写开头反斜杠,被当成相对当前空间查找;
- 目录结构和命名空间层级不一致,自动加载失败。
八、总结
命名空间用 namespace 划分类、函数、常量的归属,避免重名冲突;引用用全限定名、use 或别名三种方式,use 按文件独立、配合 PSR-4 自动加载。写在文件顶部、层级与目录对应、注意反斜杠的全局含义,就能避开绝大部分命名空间报错。
HTTP 缓存 Cache-Control 与 ETag 详解
HTTP 缓存 Cache-Control 与 ETag 详解
一、为什么要做 HTTP 缓存
同一个静态资源(JS、CSS、图片)反复下载既耗带宽又拖慢首屏。HTTP 缓存让浏览器或 CDN 把资源存一份,再次访问直接复用,只在必要时才向服务器确认。缓存分两类:强缓存(不发请求)和协商缓存(发一次轻量请求确认)。
二、强缓存:Cache-Control
服务器通过响应头告诉浏览器资源能存多久,命中期间浏览器根本不发请求,直接用本地副本。
Cache-Control: max-age=86400, public
常见指令:
max-age=秒:缓存有效期,在期内直接用本地;public:允许 CDN、代理缓存;private:只允许浏览器缓存,代理不存;no-cache:可以缓存,但每次都要协商确认;no-store:完全不缓存。
location ~* \.(css|js|png|jpg)$ {
add_header Cache-Control "max-age=604800, public";
}
注意:no-cache 不是“不缓存”,而是“缓存但要协商”;真正不缓存是 no-store。
三、协商缓存:ETag 与 Last-Modified
强缓存过期后,浏览器带上标识问服务器“资源变了吗”,没变就返回 304 Not Modified,不回传正文。
ETag(实体指纹):服务器给资源生成唯一校验值,内容一变 ETag 就变。
# 响应
ETag: "a1b2c3-1234"
# 再次请求带 If-None-Match
If-None-Match: "a1b2c3-1234"
# 未变化返回 304
304 Not Modified
Last-Modified:按修改时间判断,配合请求头 If-Modified-Since。
四、ETag 与 Last-Modified 的取舍
- ETag 按内容指纹,精确;Last-Modified 只到秒级,同一秒多次改识别不出;
- 两者同时存在时,ETag 优先级更高;
- 动态内容、频繁变化用 ETag;纯静态文件也可只依赖时间;
- 分布式多机生成 ETag 要保证算法一致,否则反复 304 失败。
五、缓存更新策略
静态文件带内容指纹文件名是最佳实践:
<script src="/app.8f3a21.js"></script>
内容变了文件名哈希就变,旧文件名长期缓存也不会命中旧版本,HTML 本身则短缓存或不缓存。
六、常见误区
- 把
no-cache理解成不缓存,导致每次全量下载; - 给经常改的入口 HTML 设超长 max-age,用户永远拿旧页面;
- 强缓存超长又不做指纹文件名,更新后用户看不到新版本;
- ETag 每次随机生成,协商缓存彻底失效;
- 只配强缓存不配协商缓存,max-age 一过全部重新下载。
七、验证方法
浏览器 F12 Network 看资源:
- 强缓存命中显示
(disk cache),状态 200,无请求; - 协商缓存命中返回 304;
- 响应头里能看到 Cache-Control、ETag。
curl -I https://example.com/app.js查看返回的缓存头。
八、总结
Cache-Control 强缓存决定“存多久不用问”,ETag/Last-Modified 协商缓存决定“过期后轻量确认是否变化”。静态资源用指纹文件名加长缓存,HTML 短缓存,动态资源用协商。理清强缓存与协商缓存的配合,才能既省带宽又保证更新及时。
Redis 持久化 RDB 与 AOF 对比
Redis 持久化 RDB 与 AOF 对比
一、为什么需要持久化
Redis 数据默认在内存里,进程重启或宕机就全丢。持久化就是把内存数据写到磁盘,重启后能恢复。Redis 提供两种机制:RDB 快照和 AOF 日志,二者可单独用也可同时开。
二、RDB 快照
RDB 在某个时刻把全量内存数据拍成二进制快照,存成 dump.rdb 文件。
# redis.conf
save 900 1 # 900秒内至少1次写则触发
save 300 10
save 60 10000
dbfilename dump.rdb
dir /var/lib/redis
触发方式:save 命令阻塞主线程,bgsave fork 子进程后台写。重启时直接加载 rdb。
三、AOF 日志
AOF 把每条写命令追加到日志文件,重启时重放命令重建数据。
appendonly yes
appendfilename appendonly.aof
appendfsync everysec # 每秒刷盘,兼顾性能与安全
刷盘策略:always(每条都刷,最安全最慢)、everysec(默认,折中)、no(交给操作系统)。
四、核心对比
| 维度 | RDB | AOF |
|---|---|---|
| 保存形式 | 某时刻全量二进制快照 | 逐条写命令日志 |
| 文件大小 | 小,压缩二进制 | 大,命令文本 |
| 恢复速度 | 快 | 较慢,需重放命令 |
| 数据安全 | 可能丢两次快照间数据 | 丢的数据更少(最多1秒) |
| 对性能影响 | bgsave fork 时短暂占用 | 持续追加,刷盘有开销 |
五、各自适合场景
- RDB:做定时备份、灾备,能快速恢复;对数据丢失容忍度较高的缓存;
- AOF:要求高可靠性,尽量少丢数据,把 Redis 当准数据库使用;
- 生产常见做法:两者同时开启,AOF 保证近期数据不丢,RDB 用于快速冷备和灾难恢复。
六、混合持久化
Redis 4.0+ 支持混合模式:AOF 重写时把当前全量数据写成 RDB 格式放在文件头,之后增量命令追加在后面。重启先加载快照再重放少量增量,兼得恢复速度与数据安全:
aof-use-rdb-preamble yes
七、常见误区
- 以为开了 RDB 就零丢失,两次快照之间宕机仍会丢数据;
- 只用 save 阻塞命令,大实例时主线程卡顿;
- AOF 用 always 追求绝对安全,写性能明显下降;
- 只配持久化不做异地备份,磁盘损坏照样丢;
- 忽略 AOF 重写,日志无限膨胀占用磁盘。
八、运维检查命令
redis-cli info persistence
查看 rdb_last_save_time、aof_last_write_status、loading 等指标,确认持久化是否正常、有无上次写失败。
九、总结
RDB 是全量快照,文件小、恢复快但可能丢一段时间数据;AOF 是命令日志,数据更安全但文件大、恢复慢。生产建议 AOF 为主、RDB 做冷备,或开启混合持久化;按业务对丢失容忍度选择策略,并定期异地备份与监控持久化状态。
Python 列表推导式与生成器对比
Python 列表推导式与生成器对比
一、两者解决的都是“批量转换”
处理一个序列、按规则生成另一个序列时,列表推导式和生成器是 Python 最常用的两种写法。它们语法相近,但内存模型完全不同:一个一次性把结果装进列表,一个按需逐个产出。
二、列表推导式
方括号 [] 包裹,立刻计算并返回完整列表:
# 把 0~9 的偶数平方放进列表
result = [x * x for x in range(10) if x % 2 == 0]
print(result) # [0, 4, 16, 36, 64]
特点:所有结果同时占内存,可以反复索引、len()、切片;适合数据量小、需要多次访问的场景。
三、生成器表达式
圆括号 () 包裹,返回一个生成器对象,不立即计算:
g = (x * x for x in range(10) if x % 2 == 0)
print(g) # <generator object ...>
print(next(g)) # 0
print(list(g)) # [4, 16, 36, 64]
每调用一次 next 才算下一个值,用完即弃,内存恒定,适合大数据流、文件逐行处理。
四、生成器函数 yield
用 yield 定义的函数也是生成器:
def squares(n):
for x in range(n):
yield x * x
for v in squares(5):
print(v) # 0 1 4 9 16
函数执行到 yield 暂停,返回值并记住现场,下次从暂停处继续,不会一次性构造列表。
五、内存与性能对比
import sys
a = [x for x in range(1_000_000)]
b = (x for x in range(1_000_000))
print(sys.getsizeof(a)) # 占用很大(约 8MB 级别,随数据增长)
print(sys.getsizeof(b)) # 固定很小(生成器对象本身)
- 列表:O(n) 内存,可随机访问,计算一次性完成;
- 生成器:O(1) 内存,只能顺序迭代,且只能遍历一次;
- 生成器不能 len()、不能切片,遍历完就空了。
六、怎么选
- 结果要复用、要排序、要索引、数据量小 → 列表推导式;
- 处理超大文件、无限流、管道式计算 → 生成器;
- 直接传给 sum、max、join 这类消费函数时,用生成器更省内存:
total = sum(x * x for x in range(1_000_000))
七、常见误区
- 以为生成器能反复遍历,第一次 for 循环后再取就是空;
- 大数据还套列表推导式,直接内存爆满;
- 把生成器当列表用 len(),报 TypeError;
- 嵌套推导式过度复杂,可读性下降,不如拆成普通循环;
- 生成器内部异常会在 next 时才暴露,调试时机靠后。
八、总结
列表推导式用 [] 一次性产出完整列表,适合小数据与重复访问;生成器表达式用 () 或 yield 按需产出,内存恒定但只能顺序消费一次。选择依据是数据规模和是否需要复用:小而多用列表,大而流式用生成器,传给归约函数优先用生成器省内存。
Git 子模块 submodule 使用详解
Git 子模块 submodule 使用详解
一、什么时候用子模块
一个项目要引用另一个独立仓库(公共组件、SDK、前端 UI 库),既想跟着主仓库走,又想保留对方自己的版本历史。直接拷源码会丢失更新,用普通子目录复制又无法同步,这就是 Git 子模块(submodule)的场景:主仓库里只记录被引用仓库的一个特定提交,而不是全部文件。
二、添加子模块
git submodule add https://github.com/example/ui-lib.git third-party/ui
执行后会发生三件事:
- 在
third-party/ui克隆远端仓库; - 在主仓库生成/更新
.gitmodules(记录子模块路径与 URL); - 主仓库暂存区记录该子模块当前 commit。
三、克隆含子模块的项目
普通 clone 只拿到空目录,需要初始化:
# 方式一:克隆时递归拉取
git clone --recurse-submodules https://github.com/example/main.git
# 方式二:已克隆后再补拉
git submodule init
git submodule update
四、更新子模块到最新
进入子模块目录像操作普通仓库一样拉取,回主仓库提交新指针:
cd third-party/ui
git checkout main
git pull
cd ../..
git add third-party/ui
git commit -m "update ui-lib to latest"
主仓库只保存一个 commit 指针,对方发新版本后主仓库不会自动变,要主动更新并提交。
五、查看与遍历
# 查看所有子模块当前状态
git submodule status
# 一次性更新所有子模块到远端最新
git submodule update --remote
# 批量在所有子模块执行命令
git submodule foreach 'git pull'
git submodule status 前面的 + 号表示子模块当前提交与主仓库记录不一致。
六、移除子模块
不能只删目录,要按规范清理:
git submodule deinit -f third-party/ui
git rm -f third-party/ui
rm -rf .git/modules/third-party/ui
git commit
七、常见坑
- clone 后子模块目录是空的,忘了
submodule init/update; - 在子模块里改代码后回主仓库,只看到“指针变化”,误以为没保存;
- 在子模块处于游离 HEAD 状态直接提交,更新时丢失;
- 换分支后主仓库指针变了却不跑
submodule update,子模块停在旧版本; - 删除子模块只删目录,残留
.gitmodules和缓存导致再次添加报错。
八、子模块的取舍
优点:版本锁定、历史独立、可复用;缺点:操作比普通目录复杂,新手易踩坑。替代方案有 Git 子树(subtree)和包管理器(npm、Composer、Go modules),优先用包管理器,只有确实需要直接引用仓库源码时才用 submodule。
九、总结
submodule 让主仓库引用外部仓库的固定提交:添加用 add,克隆要 init/update,更新先在子模块拉取再回主仓库提交指针,移除必须 deinit + rm + 清理缓存。理解它只存 commit 指针这一点,就能避开大部分版本不同步的问题。
PHP解析JSON中文转义json_encode解决 PHP
前言
json_encode 后中文变成 \uXXXX 转义码,影响可读性和前端处理。加 JSON_UNESCAPED_UNICODE 参数即可保留中文。本文讲解编码参数、解析 JSON、处理错误和中文长度注意。
操作步骤
1. 输出不转义中文
$data = ['name' => '刘馨怡'];
echo json_encode($data, JSON_UNESCAPED_UNICODE);
// {"name":"刘馨怡"}
2. 同时美化输出
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
3. 解析 JSON 字符串
$obj = json_decode($json, true); // true 返回数组
4. 检查解析错误
$obj = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
echo 'JSON 错误:' . json_last_error_msg();
}
5. 处理 UTF-8 合法性
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
6. 保留换行斜杠等控制符按需加参数。
常见坑
- 漏加 JSON_UNESCAPED_UNICODE,中文全变 \u 码。
- json_decode 第二参不传 true,得到对象而非数组。
- 输入含非法 UTF-8,encode 返回 false。
- 中文引号、换行未转义,前端解析失败。
- JSON 不支持关联外的循环引用,会报错。
- 数字精度问题用 JSON_PRESERVE_ZERO_FRACTION。
- HTTP 响应要设 Content-Type: application/json。
总结
json_encode 加 JSON_UNESCAPED_UNICODE 保留中文,PRETTY_PRINT 美化;json_decode 传 true 得数组,用 json_last_error_msg 查错。输入必须合法 UTF-8,输出设 JSON 响应头。
遇到问题加QQ23979811 协助处理
PHP检测扩展是否开启extension_loaded函数 PHP
前言
代码用到某扩展(如 gd、mbstring、pdo_mysql)却报 Call to undefined function,说明扩展没装或没开启。运行前用 extension_loaded 判断,给出友好提示,比直接报错更稳。本文讲解检测函数、查看已装扩展、开启方法。
操作步骤
1. 检测扩展是否加载
if (!extension_loaded('gd')) {
exit('需要开启 gd 扩展');
}
2. 同时检测多个扩展
$need = ['pdo_mysql', 'mbstring', 'json'];
foreach ($need as $e) {
if (!extension_loaded($e)) exit("缺少扩展:$e");
}
3. 查看所有已加载扩展
print_r(get_loaded_extensions());
4. 查看 php.ini 位置
php_ini_loaded_file();
5. 命令行查看
php -m
php -r "var_dump(extension_loaded('gd'));"
6. 开启扩展(php.ini)
extension=gd
改完重启 php-fpm 或 web 服务。
常见坑
- 命令行和 FPM 是不同 php.ini,CLI 开了网页没开。
- extension_loaded 只查是否加载,不查版本。
- 函数不存在先用 function_exists 兜底也可。
- 编译内置扩展和动态扩展检测方式一致。
- 改了 php.ini 没重启,检测结果不变。
- 虚拟环境/宝塔面板有单独扩展管理。
- Windows 扩展名带 php_gd.dll,Linux 写 gd。
总结
extension_loaded('名称') 判断扩展是否加载,启动时批量检测给友好提示。CLI 与 FPM 用不同 ini,改完重启服务。用 php -m 和 get_loaded_extensions 核对,区分内置与动态扩展。
遇到问题加QQ23979811 协助处理
Docker 镜像分层原理与构建优化
Docker 镜像分层原理与构建优化
一、镜像为什么是分层的
Docker 镜像不是一个单一文件,而是由多层只读文件系统叠加而成。Dockerfile 里每一条指令(FROM、RUN、COPY、ADD)通常都会生成一层。下层只做增量修改,构建时相同层可被多个镜像共享,这就是镜像复用、拉取提速的基础。
二、分层与联合文件系统
底层用联合挂载(UnionFS)把多层目录合并成一个统一视图,容器启动时在最顶层加一层可写层,所有运行时改动都写在这层。镜像层只读,容器层可写,删除容器时可写层随之消失。
FROM php:8.2-cli
RUN apt-get update && apt-get install -y libzip-dev
COPY ./app /var/www/html
CMD ["php", "-S", "0.0.0.0:8000", "/var/www/html"]
每一行对应一层,基础镜像的层被复用,只新增自己的改动层。
三、构建缓存如何工作
Docker 按 Dockerfile 顺序逐层构建,某一层的指令和上游层未变化时,直接复用已有缓存层,不必重新执行。这就是改了一行代码却不必重装依赖的原因。
四、优化一:把变化少的指令放前面
依赖安装基本不变,业务代码频繁变动。顺序反过来会导致每次改代码都重装依赖:
# 推荐:先装依赖(缓存稳定),再拷代码
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npm", "start"]
COPY . . 每次代码变更都会让它之后的层失效,必须放在 npm ci 之后。
五、优化二:合并 RUN、清理缓存
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
多个 RUN 会产生多层且缓存残留无法删除;合并成一层并在同层清理安装缓存,镜像更小。
六、优化三:多阶段构建
编译产物和构建工具分开,最终镜像只留运行所需:
# 构建阶段
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o app
# 运行阶段,只拷贝二进制
FROM alpine:latest
COPY --from=builder /src/app /app
CMD ["/app"]
最终镜像不带 Go 工具链,体积大幅下降。
七、其他实用技巧
- 用
.dockerignore排除 node_modules、.git、日志,避免无谓拷入; - 选小基础镜像(alpine、distroless),但注意 glibc 兼容;
COPY优于ADD,ADD 会自动解压缩带来不确定性;- 避免
FROM latest这种浮动标签,保证可复现。
八、常见误区
- 把
COPY . .放在 RUN 安装依赖之前,缓存全失效; - 每条 RUN 单独写,镜像层层叠加又删不掉缓存;
- 直接用超大基础镜像塞进编译工具;
- 忽略 .dockerignore,把源码和依赖目录一起打进镜像;
- 以为分层越多越好,其实多余层只增体积不增价值。
九、总结
镜像分层让相同层共享、构建可缓存;优化关键是稳定指令前置、合并 RUN 并清理、用多阶段构建只留运行产物、配合 .dockerignore 和小基础镜像。理解层与缓存机制,才能把镜像做小、构建做快。
JavaScript 闭包与作用域链详解
JavaScript 闭包与作用域链详解
一、什么是闭包
闭包(Closure)是指函数能够记住并访问它词法作用域里的变量,即使外层函数已经执行结束。简单说:内层函数引用了外层函数的变量,外层函数返回后,这些变量不会被垃圾回收,依然存活在闭包里。
二、作用域与作用域链
JavaScript 有全局作用域、函数作用域(ES6 后还有块级作用域)。查找变量时,引擎从当前作用域开始,一层层向外找,直到全局,这条查找路径就是作用域链。
let a = '全局';
function outer() {
let b = '外层';
function inner() {
let c = '内层';
console.log(a, b, c); // 沿作用域链逐层找到
}
inner();
}
outer();
inner 找不到 b、a 时就沿链向上找。
三、最简单的闭包
function makeCounter() {
let count = 0; // 被闭包保护的私有变量
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
makeCounter 执行完本应销毁 count,但返回的匿名函数仍引用它,于是 count 被保留,且外部无法直接改动,形成私有状态。
四、常见应用场景
- 数据私有化:变量只能通过返回的方法访问,避免污染全局;
- 函数工厂:批量生成带不同配置的函数;
- 回调与事件监听:定时器、点击回调记住当时的环境变量。
function power(n) { return function (x) { return x ** n; }; } const square = power(2); console.log(square(5)); // 25
五、经典循环陷阱
// 错误:var 共享同一变量,结果全是 3
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3 3 3
}
// 正确:let 每轮块级绑定,或用立即执行函数隔离
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0 1 2
}
var 声明的 i 挂在函数作用域,三个回调闭包引用同一个 i;let 每轮生成新绑定,各自闭包保存对应值。
六、闭包的代价与注意
- 被闭包引用的变量不会回收,滥用会造成内存占用上升;
- 不需要的闭包及时把引用置 null,方便 GC;
- 闭包里的 this 指向由调用方式决定,不继承外层,需要保存时常用
const self = this; - 不要在循环里盲目创建大量闭包,避免性能损耗。
七、常见误区
- 以为闭包只是“函数套函数”,关键是对外层变量的持续引用;
- 以为返回函数后外层变量会自动清空,实际被闭包留住;
- 混淆作用域链查找方向,误以为内层能直接改外层;
- 把闭包当万能优化,忽略内存开销。
八、总结
闭包 = 函数 + 它引用的外层词法作用域。作用域链决定变量从内向外如何查找,闭包让外层变量在函数退出后仍然可访问。它用于私有化状态和函数工厂,但要警惕内存保留,循环场景优先用 let。理解闭包,才算真正懂了 JavaScript 的函数式特性。
PHP数组键值互换array_flip用法 PHP
前言
需要把数组的值变成键、键变成值时,array_flip 一步完成,常用于反向查找、去重、建立快速索引。但它对重复值和值类型有限制。本文讲解基本用法、反向查找、限制和替代方案。
操作步骤
1. 键值互换
$fruits = ['a' => '苹果', 'b' => '香蕉'];
$reversed = array_flip($fruits);
// ['苹果'=>'a', '香蕉'=>'b']
2. 反向判断某值是否存在
$codes = ['苹果' => 'a'];
if (isset($codes['苹果'])) {
echo '存在,代码:' . $codes['苹果'];
}
3. 配合去重
$unique = array_flip(array_flip($arr));
4. 用值做 O(1) 查询
$allow = array_flip(['jpg','png','gif']);
if (isset($allow[$ext])) { /* 允许 */ }
常见坑
- 值重复时,后面的覆盖前面的键,数据丢失。
- 值必须是字符串或整数,浮点/布尔互换会出错。
- 原数组键是数字,互换后变成字符串键,注意类型。
- 二维数组不能直接 flip,只支持一维标量值。
- 以为 flip 不改变原数组,它返回新数组。
- 关联数组顺序在 flip 后按新键排列。
- 大数组 flip 复制一份,内存翻倍。
总结
array_flip 把值变键、键变值,适合反向查找和白名单 O(1) 判断。要求一维、值为字符串/整数,重复值会覆盖。去重可用两次 flip,复杂结构需手动转换。
遇到问题加QQ23979811 协助处理