Nginx配置检查测试
Nginx配置检查测试
引言
改完Nginx配置直接reload最容易踩坑:语法错、证书路径错、端口被占会导致Nginx起不来甚至站点全挂。规范流程是先检查语法,通过后再平滑重载,出问题能快速回退。
语法检查
nginx -t
检查默认配置文件语法,输出syntax ok和test is successful才说明配置无误。
指定配置文件检查
nginx -t -c /etc/nginx/nginx.conf
自定义路径时用-c指定,避免检查错文件。
平滑重载
nginx -s reload
不中断现有连接重新加载配置,改动生效不宕机。
查看编译参数和版本
nginx -V
大写V看版本和编译模块,确认是否包含ssl、gzip、stream等模块。
打印生效配置
nginx -T
大写T会把所有include进来的配置打印出来,排查重复server或覆盖时用。
出错常见原因
- 括号不配对、分号漏写;
- 证书key路径不存在或权限600不对;
- listen端口已被其他进程占用;
- upstream后端地址写错;
- include路径下的文件本身语法错。
注意事项
- 永远先-t再reload,禁止跳过检查;
- 改配置前备份原文件,出错可立刻回退;
- -T输出很长,重定向到文件再查;
- reload失败先看error.log定位行号;
- 生产环境改完curl本机验证再放行;
- 多台机器逐台灰度,不要一次全改。
总结
nginx -t检查语法,-c指定文件,-s reload平滑生效,-V看编译模块,-T打印完整配置。铁律是先检查通过再重载,改前备份、出错看error.log、本机curl验证后再上线,生产逐台灰度,避免一次改全量导致整站不可用。
MySQL查看数据库列表
MySQL查看数据库列表
引言
刚连上一台MySQL服务器,先要看实例上有哪些库、当前账号能访问哪些库。SHOW DATABASES是最基础的入口,配合information_schema可拿到库名、字符集、容量等完整信息。
列出所有数据库
SHOW DATABASES;
列出当前账号有权限看到的全部库,看不到的库是权限不足,不代表不存在。
查看当前所在库
SELECT DATABASE();
切换库之前先确认当前连接落在哪个库,避免写SQL操作错库。
切换数据库
USE mydb;
切库后后续不加库名的表操作都落在mydb。
查库字符集和排序规则
SELECT SCHEMA_NAME, DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM information_schema.SCHEMATA;
拿到每个库的默认字符集,排查乱码时先看这里。
查看库占用空间
SELECT table_schema,
ROUND(SUM(data_length+index_length)/1024/1024,2) AS size_mb
FROM information_schema.tables
GROUP BY table_schema
ORDER BY size_mb DESC;
按库汇总数据和索引大小,快速找出占空间大户。
注意事项
- SHOW DATABASES只显示有权限的库,根账号才看全;
- information_schema是系统库,不要往里写业务数据;
- 新库建库时指定utf8mb4,避免emoji乱码;
- 删库DROP DATABASE不可逆,先备份再操作;
- 多实例用端口区分,别把实例和库混淆;
- 脚本里用SELECT SCHEMA_NAME批量遍历,比SHOW好解析。
总结
SHOW DATABASES列库,SELECT DATABASE()看当前库,USE切库,information_schema.SCHEMATA查字符集,按table_schema聚合查容量。先确认当前库再写SQL,建库统一utf8mb4,删库前务必备份,脚本批量处理优先走information_schema。
PHP时间戳转日期
PHP时间戳转日期
引言
数据库里常存Unix时间戳(秒级整数),展示到页面要转成可读日期;反过来用户选的日期也要转成时间戳入库。date()和strtotime()是这对基础转换工具,理解格式符和时区才能避免差8小时。
时间戳转日期
$ts = 1760000000;
echo date('Y-m-d H:i:s', $ts);
date()第二个参数缺省就是当前时间time(),传指定时间戳则格式化该时刻。
当前时间戳
echo time(); // 秒级时间戳
echo microtime(true); // 带微秒的浮点时间戳
日期字符串转时间戳
$ts = strtotime('2026-10-19 08:30:00');
echo $ts;
strtotime支持英文相对表达式:
strtotime('tomorrow');
strtotime('+1 week');
strtotime('next Monday');
DateTime面向对象写法
$dt = new DateTime('@' . $ts);
echo $dt->format('Y-m-d H:i:s');
$d = DateTime::createFromFormat('Y-m-d', '2026-10-19');
echo $d->getTimestamp();
复杂时区和格式解析用DateTime更稳。
常用格式符
- Y四位年,m两位月,d两位日,H24小时,i分,s秒;
- 带毫秒用v,星期用D/l,12小时制用g/a。
注意事项
- 服务器时区不对会差8小时,先date_default_timezone_set('Asia/Shanghai');
- date()对1970前或2038前后32位系统有边界问题;
- strtotime解析失败返回false,要判空再用;
- 毫秒时间戳除以1000再传给date();
- 跨时区存储建议统一UTC,展示时再转本地;
- MySQL存int(10)秒级,避免用字符串日期做比较。
总结
时间戳转日期用date('Y-m-d H:i:s', $ts),日期转时间戳用strtotime(),复杂解析用DateTime。先设好Asia/Shanghai时区,毫秒先除以1000,strtotime失败要判空。入库统一存秒级时间戳,展示层再格式化,比存字符串日期更好比较和索引。
Docker查看运行容器
Docker查看运行容器
引言
部署多个服务后,先要看清哪些容器在跑、状态如何、映射了哪些端口、跑了多久。docker ps系列命令是日常运维入口,配合格式化和过滤快速定位目标容器。
只看运行中容器
docker ps
输出容器ID、镜像、命令、创建时间、状态、端口映射、名称,默认只列正在运行的。
查看全部容器(含已停止)
docker ps -a
把Exited状态的容器也列出来,排查退出残留时用。
只看容器ID
docker ps -q
输出纯ID,配合批量停止删除。
自定义输出列
docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}"
只保留关心的列,脚本采集更清晰。
按镜像过滤
docker ps --filter ancestor=nginx:latest
列出指定镜像启动的容器。
查看容器资源占用
docker stats
实时看每个容器CPU、内存、网络IO,Ctrl+C退出。
注意事项
- ps默认不含已停止容器,找退出容器用-a;
- STATUS显示Up时长或Exited码,退出码非0要查日志;
- PORTS列看清宿主端口到容器端口的映射;
- 容器名重复会冲突,--name要唯一;
- 配合docker inspect看单个容器完整配置;
- 批量操作前先用-q列出ID确认范围。
总结
docker ps看运行中容器,-a含已停止,-q取ID批量处理,--format自定义列,--filter按镜像过滤,docker stats看实时资源。运维先列清单确认状态和端口,再决定停止、重启或排查,避免凭印象操作容器。
Nginx默认站点配置
Nginx默认站点配置
引言
直接用IP或未匹配到server_name的域名访问时,Nginx会落到默认站点。装好Nginx自带的欢迎页就是默认server,安全上建议改掉,避免暴露版本或被扫描利用。
默认站点如何确定
同一端口下,第一个server块、或带default_server标记的就是默认站点:
server {
listen 80 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html;
}
server_name用_占位,表示匹配任意未命中的主机名。
配置占位拒绝页
对外只暴露正式站点时,默认站点直接返回444或简单403:
server {
listen 80 default_server;
server_name _;
return 444;
}
444是Nginx特有,直接断开连接,不返回内容。
隐藏版本号
http {
server_tokens off;
}
避免错误页暴露Nginx版本,减少针对性漏洞扫描。
自定义默认首页
server {
listen 80 default_server;
server_name _;
root /var/www/default;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
多端口默认
HTTPS同理,443也要配default_server:
server {
listen 443 ssl default_server;
server_name _;
# 自签或占位证书,否则握手失败
return 444;
}
注意事项
- 一台机器同端口只能有一个default_server;
- 不配默认站,未匹配域名会落到第一个server,可能串站点;
- 改完nginx -t校验再reload;
- 云主机IP直扫建议默认站返回444,不暴露首页;
- 区分default_server和servername ,前者是优先级标记;
- SSL默认站必须有证书,否则浏览器握手报错。
总结
默认站点由default_server或同端口首个server决定,用server_name _占位。安全做法是把默认站返回444拒绝、server_tokens off隐藏版本,443也要配默认拒绝站,避免IP直扫串到正式站点。改完务必-t校验再reload。
MySQL查看表结构
MySQL查看表结构
引言
接手老库、写SQL、加字段前,先看清表里有哪些列、类型、主键、索引和默认值。DESC看字段概览,SHOW CREATE TABLE拿到完整建表语句,information_schema可批量查元数据。
DESC看字段概览
DESC users;
-- 等价
DESCRIBE users;
输出Field、Type、Null、Key、Default、Extra六列,快速看字段类型和是否主键。
完整建表语句
SHOW CREATE TABLE users;
拿到原始CREATE TABLE,包含引擎、字符集、索引、外键、注释,迁移和备份最有用。
查看表索引
SHOW INDEX FROM users;
看有哪些索引、唯一性、列顺序,分析慢查询时用。
查表状态和引擎
SHOW TABLE STATUS LIKE 'users';
看引擎、行数、行格式、数据和索引大小、自增值。
information_schema批量查
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='dbname' AND TABLE_NAME='users';
程序里动态读表结构、批量比对字段时用。
注意事项
- DESC不显示完整索引和外键,要看SHOW CREATE TABLE;
- 大表SHOW TABLE STATUS的行数是估算值,非精确;
- 看字段注释用information_schema的COLUMN_COMMENT;
- 修改结构前先SHOW CREATE TABLE留底;
- 区分NULL和默认空串,Default列直接体现;
- 自增字段在Extra列显示auto_increment。
总结
日常DESC快速看字段,要完整定义用SHOW CREATE TABLE,查索引用SHOW INDEX,看容量引擎用SHOW TABLE STATUS,脚本批量取元数据查information_schema。改结构前先把建表语句留底,避免误删索引或默认值。
Linux查看CPU信息
Linux查看CPU信息
引言
服务器扩容、压测、排查性能瓶颈前,先要知道机器几颗CPU、多少核、主频和架构。/proc/cpuinfo是内核暴露的原生信息,配合lscpu、nproc能快速拿到规格,top/mpstat再看实时占用。
lscpu看汇总
lscpu
一次列出架构、颗数、每颗核数、线程数、主频、缓存,最简洁。
nproc看逻辑核数
nproc
直接输出可用逻辑CPU数,脚本里取核数最方便。
读/proc/cpuinfo
cat /proc/cpuinfo
每颗逻辑核一段,看model name型号、cpu MHz频率、flags指令集。
统计物理颗数和核数
# 物理CPU颗数
grep "physical id" /proc/cpuinfo | sort -u | wc -l
# 每颗物理核数
grep "cpu cores" /proc/cpuinfo | uniq
# 逻辑核总数
grep -c processor /proc/cpuinfo
物理颗数×每颗核数×超线程=逻辑核数。
实时CPU占用
top
mpstat 1
top看整体和进程,mpstat每秒采样看用户态、系统态、idle占比。
注意事项
- 云主机看到的是分配的vCPU,不一定是物理独占;
- 超线程逻辑核不等于物理性能翻倍;
- flags里看是否支持虚拟化、AVX等指令集;
- 排查高占用先top按P按CPU排序找进程;
- 结合uptime看负载和核数是否匹配;
- lscpu在大部分发行版预装,无此命令再读/proc。
总结
规格用lscpu看汇总、nproc取核数、/proc/cpuinfo读型号和指令集,配合grep统计物理颗数与核数,实时占用用top和mpstat。压测和扩容前先分清物理核、逻辑核和超线程,避免按vCPU误判真实算力。
PHP判断数组为空
PHP判断数组为空
引言
接口返回、查询结果、表单收集都要先判断数组有没有数据,再决定走空分支还是遍历。empty、count、!$arr三种写法结果接近,但适用边界不同,选错会把0或null误判。
empty最常用
$list = [];
if (empty($list)) {
echo '数组为空';
}
empty对空数组、未定义变量、null、0、''都返回true,不会报未定义警告,最适合先判空。
count取元素个数
if (count($arr) === 0) {
echo '没有元素';
}
精确判断元素数量,count为0才是空;注意$arr本身未定义会警告,先isset。
逻辑非写法
if (!$arr) {
echo '空';
}
短但要求$arr一定是已初始化数组,未定义会Notice。
判断键是否为空
不是判断整体,而是某键值:
if (empty($arr['name'])) {
echo 'name为空';
}
避免直接取未定义键。
多维数组判空
if (!empty($arr['items']) && is_array($arr['items'])) {
foreach ($arr['items'] as $item) {}
}
先确认外层键存在且是数组,再遍历内层。
注意事项
- empty(0)和empty('')也为true,数字0场景别用empty判数组元素;
- count第二个参数1可统计多维总元素;
- 严格比较用count($arr)===0;
- 数组里全是空字符串不等于空数组;
- 查询无结果返回[]而非null,按空数组处理;
- 结合isset防止未定义键警告。
总结
整体判空首选empty($arr)防未定义,精确统计用count($arr)===0,短逻辑非要求变量已初始化,判单个键用empty($arr['key']),多维先确认键存在再遍历。注意empty对0和空串也为真,数字场景改用count严格判断。
Docker查看本地镜像
Docker查看本地镜像
引言
用Docker跑服务前先确认本地有哪些镜像、大小多少、标签是什么,避免重复拉取或用错版本。docker images系列命令能列出镜像仓库名、标签、ID、大小和创建时间。
列出全部本地镜像
docker images
# 等价
docker image ls
输出REPOSITORY、TAG、IMAGE ID、CREATED、SIZE五列。
只看镜像ID
docker images -q
只输出短ID,配合批量删除使用。
按名称过滤
docker images nginx
docker images | grep php
列出指定仓库名或关键词的镜像。
查看镜像详细信息
docker inspect nginx:latest
JSON输出架构、环境变量、挂载点、默认启动命令等。
查看镜像历史层
docker history nginx:latest
看每层做了什么改动、每层大小,理解镜像分层结构。
过滤和格式化
docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}"
自定义输出列,脚本里取数更方便。
注意事项
- 同一个镜像不同TAG可能共用层,SIZE不重复累加;
- 悬空镜像
是旧版本残留,用prune清理; - 镜像大小是解压后占用,下载压缩包更小;
- 看磁盘占用用docker system df;
- 未被任何容器引用的镜像可安全删除;
- 拉取前先docker images判断是否已存在。
总结
docker images列出本地镜像,-q取ID、按仓库名过滤、inspect看详情、history看分层、--format自定义输出。日常先列镜像确认版本和大小,再决定拉取或删除,配合system df掌握磁盘占用。
Nginx代理缓存配置
Nginx代理缓存配置
引言
反向代理每次都回源取页面,后端压力大、首屏慢。Nginx自带proxy_cache,把后端响应存到本地磁盘,命中时直接返回,既减轻源站又加快响应,适合静态资源和不常变的页面。
定义缓存区
http块里先声明缓存目录和大小:
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=mycache:10m max_size=1g inactive=7d;
- keys_zone=mycache:10m 是缓存名和索引内存;
- max_size=1g 磁盘上限;
- inactive=7d 7天没访问自动清理。
代理站点启用缓存
server或location块:
location / {
proxy_cache mycache;
proxy_cache_key $host$uri$is_args$args;
proxy_cache_valid 200 304 1h;
proxy_cache_valid 404 1m;
proxy_cache_methods GET HEAD;
proxy_pass http://backend;
}
200/304缓存1小时,404只存1分钟。
命中状态标识
加响应头方便排查:
add_header X-Cache-Status $upstream_cache_status;
HIT命中、MISS未命中、EXPIRED过期、BYPASS绕过。
绕过缓存的场景
登录后动态内容不缓存:
proxy_no_cache $http_cookie;
proxy_cache_bypass $http_cookie;
带cookie时不读不写缓存。
主动清除缓存
# 配置删除接口需要第三方模块或手动删目录
rm -rf /data/nginx/cache/*
更新内容后清对应目录,或按URL前缀匹配删除。
注意事项
- 缓存目录要先mkdir并给nginx读写权限;
- 带POST的接口不要缓存;
- 频繁变动的页面缓存时间设短或不缓存;
- 缓存键要带参数,避免不同query串内容;
- 配置改完nginx -t校验再reload;
- 大文件下载用分片缓存更稳妥。
总结
proxy_cache_path在http块定义缓存区,location里proxy_cache启用、proxy_cache_valid设有效期、proxy_cache_key拼缓存键,用X-Cache-Status看命中。静态和低频页面开缓存扛压力,动态登录内容用cookie绕过,更新后及时清理。
MySQL唯一索引创建
MySQL唯一索引创建
引言
邮箱、手机号、订单号这类业务字段要求不能重复,光靠应用层判断并发下仍可能插入重复值。唯一索引在数据库层直接约束重复,是防脏数据的最后一道保险,同时还能加速等值查询。
建表时定义唯一索引
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
UNIQUE KEY uk_email (email)
);
uk_email是索引名,email列唯一。
已有表追加唯一索引
ALTER TABLE users ADD UNIQUE KEY uk_phone (phone);
数据里已有重复值时会报错1062,需先清理重复再加。
组合唯一索引
多列组合唯一,比如同一商品同一活动只领一次券:
ALTER TABLE coupon ADD UNIQUE KEY uk_user_goods (user_id, goods_id);
允许单列重复,但组合起来不能重复。
插入重复的报错处理
INSERT INTO users(email) VALUES('a@x.com');
-- 再次插入同email触发 1062 Duplicate entry
应用层catch错误码,返回邮箱已注册,而不是让500报错。
插入或更新冲突
INSERT INTO users(email) VALUES('a@x.com')
ON DUPLICATE KEY UPDATE login_time=NOW();
唯一键冲突时转成更新,适合登录时间刷新。
注意事项
- 加唯一索引前先查重:
SELECT email,COUNT(*) FROM users GROUP BY email HAVING COUNT(*)>1; - NULL在唯一索引里可以出现多个,MySQL不认为NULL相等;
- 唯一索引也是普通索引,等值查询能走索引;
- 大表加索引会锁表或耗时,高峰期用在线DDL;
- 删除索引
DROP INDEX uk_email ON users。
总结
唯一索引在建表时用UNIQUE KEY定义,已有表用ALTER TABLE ADD UNIQUE KEY追加,多列组合唯一用组合索引。加之前先清重复,插入冲突用ON DUPLICATE KEY UPDATE转更新,数据库层约束比应用层判断更可靠。
PHP遍历关联数组
PHP遍历关联数组
引言
关联数组用键名而非数字下标组织数据,配置项、接口返回的JSON转成数组后都是关联数组。遍历要同时拿到键和值,foreach是最常用的方式,不同场景配合array_keys、array_walk更灵活。
foreach取键和值
$user = ['name'=>'刘馨怡','age'=>26,'city'=>'连云港'];
foreach ($user as $key => $value) {
echo "$key: $value\n";
}
$key => $value同时拿到键名和值,最通用。
只取值
foreach ($user as $value) {
echo $value;
}
不需要键名时省略$key。
用键名取单个值
echo $user['name'];
// 判断键是否存在
if (isset($user['email'])) {
echo $user['email'];
}
取单值直接下标访问,先isset避免未定义键报错。
array_keys取所有键
$keys = array_keys($user);
print_r($keys); // ['name','age','city']
需要单独处理键名列表时用。
array_walk回调遍历
array_walk($user, function($value, $key) {
echo "$key => $value";
});
适合把遍历逻辑封装成回调。
二维关联数组遍历
$list = [
['id'=>1,'name'=>'A'],
['id'=>2,'name'=>'B']
];
foreach ($list as $row) {
echo $row['id'].'-'.$row['name'];
}
循环里再按键名取子元素。
注意事项
- foreach不会改变原数组指针,比each安全;
- 遍历中不要随意增删键,可能导致迭代异常;
- 关联数组键名大小写敏感;
- 值可能为null时用array_key_exists判断键是否存在,isset对null返回false;
- 大数组遍历时注意内存,避免在循环内重复计算。
总结
关联数组遍历首选foreach的$key => $value形式,单值直接下标加isset判断,取键名用array_keys,封装逻辑用array_walk,二维数组外层foreach内层按键取值。掌握这几种就能应对配置、接口数据等绝大多数场景。
Git查看提交历史
Git查看提交历史
引言
排查线上问题要追溯某行代码是谁、什么时候改的,就靠git log。学会用筛选和格式化参数,能快速定位某次提交,而不是在整屏历史里翻找。
基础查看
git log
默认从新到旧列出提交哈希、作者、时间、说明,按q退出。
一行精简显示
git log --oneline
每个提交压缩成一行短哈希加标题,浏览全貌最常用。
按作者和时间筛选
git log --author="张三"
git log --since="2026-10-01" --until="2026-10-10"
git log --grep="修复登录"
分别按作者、时间段、提交说明关键词过滤。
查看文件改动历史
git log --oneline -- index.php
只看某个文件的提交记录,追溯单个文件演变。
显示每次改动内容
git log -p
git log --stat
-p展开具体diff,--stat只看增删文件和行数概览。
图形化分支历史
git log --oneline --graph --all
用线条画出分支合并拓扑,理清多分支走向。
注意事项
- --oneline配合--graph看分支最直观;
- grep匹配提交说明,搜代码内容用git log -S或git blame;
- 限制数量git log -n 10只看最近10条;
- 已推送的公开历史不要用rebase改写;
- 配合git show 哈希看单次提交详情。
总结
git log是追溯代码的主入口,--oneline精简、--author和--grep筛选、--graph看分支、-p看diff。排查问题时先按作者或关键词缩小范围,再用git show定位单次提交,比整屏翻找高效得多。
Linux查看端口占用
Linux查看端口占用
引言
启动服务时报Address already in use,说明目标端口被别的进程占着。上线前要确认端口是否监听、被哪个PID占用,常用ss、netstat、lsof三个工具,先定位进程再决定停掉还是换端口。
ss查看(推荐)
ss替代netstat,更快更全:
ss -tlnp | grep 80
- -t TCP,-l 监听状态,-n 数字端口不解析,-p 显示进程PID和名称。
看TCP监听端口最直接。
netstat兼容命令
老系统没有ss时:
netstat -tlnp | grep 80
参数含义和ss一致,输出本地地址:端口、PID/程序名。
lsof按端口查进程
lsof -i:80
直接列出占用80端口的进程命令、PID、用户,适合只盯单个端口。
找到并处理
确认是无用进程后:
kill -9 PID
若是被系统服务占用,优先systemctl stop对应服务,别盲目kill。
查看所有监听端口
ss -tlnp
不加grep,列出全部TCP监听,盘点机器开了哪些服务。
注意事项
- 查端口要root或sudo,普通用户看不到别的进程PID;
- 状态LISTEN才是监听,ESTABLISHED是已建立连接,别混淆;
- 端口被占先判断是不是正常服务,误杀线上进程影响业务;
- 云服务器端口不通还要查安全组和防火墙,不只是本机占用;
- ss在新发行版预装,netstat需装net-tools。
总结
日常用ss -tlnp grep端口最省事,netstat兼容老系统,lsof -i:端口看单个占用。定位PID后先确认进程归属再kill,端口连不上时本机占用、防火墙、云安全组三层都要排查。
MySQL枚举与集合
MySQL枚举与集合
引言
状态、性别、标签这类取值固定的字段,有人用varchar存中文,导致拼写不一、无法约束。MySQL提供ENUM单选和SET多选两个字符串型,把合法值列在定义里,从源头限制脏数据。
ENUM单选
一个字段只能取定义值中的一个,适合性别、订单状态:
CREATE TABLE orders (
id INT PRIMARY KEY AUTO_INCREMENT,
status ENUM('待付款','已付款','已发货','已完成','已取消') NOT NULL DEFAULT '待付款'
);
插入不在列表的值,严格模式直接报错,非严格模式存空串,避免写错状态。
SET多选
一个字段可存多个定义值,适合兴趣、标签、权限位:
CREATE TABLE users (
id INT PRIMARY KEY,
interest SET('阅读','运动','音乐','旅行') NOT NULL
);
-- 插入多个值,逗号分隔
INSERT INTO users(interest) VALUES('阅读,音乐');
底层按位存储,最多64个成员。
两者区别
- ENUM单选,SET多选;
- ENUM内部按序号1、2、3存储,SET按二进制位;
- ENUM常见于状态机,SET常见于标签和多选权限;
- SET查单个值用FIND_IN_SET:
SELECT * FROM users WHERE FIND_IN_SET('阅读', interest);
注意事项
- 新增/修改枚举值要ALTER TABLE,字段多、值不稳定就别用ENUM;
- 枚举值排序按内部序号而非字面字母序,容易误判;
- 中文枚举注意字符集一致;
- 关系型设计里多值标签更推荐关联表,SET只适合简单固定多选;
- 不要用ENUM存会频繁增长的分类。
总结
ENUM单选约束状态,SET多选存标签,底层分别用序号和位存储。取值固定且变动少的字段用它们防脏数据,值频繁增加或需要多对多关系时改用独立关联表更规范。
Redis缓存穿透击穿雪崩区别与防护方案
前言
Redis 把热点数据放进内存提速,但缓存一旦失效或被恶意穿透,数据库会被瞬间打垮。很多人把穿透、击穿、雪崩混为一谈,三者成因不同,防护手段也不一样,本篇一次讲清区别和方案。
一、缓存穿透:查不存在的数据
攻击者或程序反复查询一个数据库里根本不存在的 key,缓存永远不命中,每次都打到 MySQL。
- 典型:刷不存在的用户ID、商品ID。
- 防护:
- 布隆过滤器(Bloom Filter):请求先过过滤器,不存在直接拦截,不查库;
- 缓存空值:对查不到的结果也缓存一个空对象并设短过期时间(如60秒)。
// 伪代码:缓存空值
$user = $db->find($id);
if ($user === null) {
$redis->setex("u:$id", 60, 'NULL');
return null;
}
$redis->setex("u:$id", 3600, json_encode($user));
二、缓存击穿:热点key过期瞬间
一个被高频访问的热点 key 突然过期,大量并发同时打到数据库重建缓存。和穿透区别:数据真实存在,只是刚好过期。
- 防护:
- 热点key逻辑过期,不设物理TTL,后台异步刷新;
- 加互斥锁(SETNX),只让一个线程回源重建,其余等待。
// SETNX 单线程重建
$lock = $redis->setnx("lock:u:$id", 1);
if ($lock) {
$redis->expire("lock:u:$id", 10);
$data = $db->find($id);
$redis->setex("u:$id", 3600, json_encode($data));
}
三、缓存雪崩:大量key同时过期
大批缓存同一时间失效,数据库承压暴增。和击穿区别:击穿是单个热点,雪崩是大面积。
- 防护:过期时间加随机偏移,避免同时失效;多级缓存;Redis集群高可用;限流降级兜底。
$ttl = 3600 + mt_rand(0, 600); // 3600~4200 秒随机
$redis->setex("list:$page", $ttl, json_encode($rows));
四、三者对比速记
- 穿透:数据不存在 → 布隆过滤器 + 缓存空值;
- 击穿:热点key过期 → 互斥锁 + 逻辑过期;
- 雪崩:大批key同时过期 → TTL随机 + 高可用 + 限流。
总结
先分清"查不存在""单点过期""批量过期"三种场景,再对应布隆过滤器、互斥锁、随机TTL,就能把数据库挡在缓存后面。
遇到问题加QQ23979811 协助处理
PHP cURL发送POST
PHP cURL发送POST
引言
后端对接第三方接口、登录模拟、提交表单,PHP最常用cURL扩展发HTTP请求。file_get_contents只能简单GET,cURL能控制POST、请求头、Cookie、超时和错误处理,是对接API的标配。
基础POST表单
$ch = curl_init('https://api.example.com/login');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'username' => 'user01',
'password' => '123456',
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 结果返回字符串而非直接输出
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$res = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
POSTFIELDS传数组即multipart表单,接口收到的是form-data。
发送JSON
对接REST接口要手动编码并声明头:
$json = json_encode(['name' => '测试', 'age' => 18]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $token,
]);
注意JSON方式POSTFIELDS传字符串,别传数组。
带Cookie维持登录
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookie.txt'); // 第一次保存
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookie.txt'); // 第二次读取
模拟登录再访问需鉴权页面时,用cookie文件维持会话。
错误与状态码处理
if ($err) { echo '请求失败:' . $err; }
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$data = json_decode($res, true);
别只看返回体,必须判断$code和curl_error,500/404也要处理。
注意事项
- 关SSL校验仅限本地测试,生产保留CURLOPT_SSL_VERIFYPEER;
- 超时必须设,防止接口慢拖垮进程;
- HTTPS证书异常先排查证书链,别长期关闭校验;
- 敏感token放请求头,别写进URL参数;
- 用完curl_close释放句柄。
总结
cURL POST先init,设CURLOPT_POST和POSTFIELDS,表单传数组、JSON传字符串并带头,RETURNTRANSFER取回结果,配合超时、错误判断和状态码,生产保留SSL校验。掌握这套就能稳定对接绝大多数第三方API。
Nginx代理WebSocket
Nginx代理WebSocket
引言
聊天、实时推送、在线协作这类场景用WebSocket长连接。前端经过Nginx访问后端WS服务时,如果只按普通HTTP反代配置,握手会失败,因为WebSocket要先发起Upgrade协议升级,Nginx必须把升级头透传给后端。
WebSocket握手原理
客户端先发HTTP请求,带Upgrade: websocket和Connection: Upgrade头,后端同意后把HTTP协议升级成持久双向长连接。Nginx默认不透传这两个头,导致握手400。
标准代理配置
location /ws/ {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
- proxy_http_version 1.1:WS基于HTTP/1.1;
- Upgrade $http_upgrade:透传客户端升级协议;
- Connection "upgrade":标记连接升级;
- read_timeout拉长:长连接默认60秒无数据会被Nginx断开。
用map统一处理
不同location可能同时有HTTP和WS,写map避免硬编码:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# location内
proxy_set_header Connection $connection_upgrade;
普通HTTP请求$http_upgrade为空时走close,WS请求走upgrade,一套配置兼容。
常见问题
- 握手400/502:没加Upgrade和Connection头;
- 连接几十秒自动断开:read_timeout太短,按需拉长或服务端发心跳;
- wss(HTTPS)走Nginx终止TLS后,后端用ws即可;
- 后端端口和proxy_pass写对,别漏http://。
注意事项
- 长连接超时靠心跳和read_timeout共同维持;
- 集群WS要配合粘性会话或后端共享状态,否则消息发不到正确连接;
- HTTPS页面必须用wss://,浏览器会拦截混合内容;
- 改配置nginx -t后reload。
总结
Nginx代理WebSocket核心是加proxy_http_version 1.1、透传Upgrade和Connection头、拉长read_timeout,用map兼容普通HTTP。握手失败先查这三个头,断线先查超时和心跳。
Docker清理悬空镜像
Docker清理悬空镜像
引言
反复build镜像后,旧的无标签镜像(
先看占用
docker system df
查看镜像、容器、卷、构建缓存各占多少,先摸清再动手。
什么是悬空镜像
build出新标签后,旧层变成
只清悬空镜像(安全)
docker image prune
只删除无标签无引用的dangling镜像,已打标签和在用镜像不动,最稳妥。
清停止的容器
docker container prune
删掉所有已退出的容器,正在运行的不受影响。
清无用镜像和构建缓存(更彻底)
docker system prune
一次清悬空镜像、停止容器、无用网络和构建缓存,加-a连未被容器使用的镜像也删,加--volumes连未使用卷一起删:
docker system prune -a --volumes
手动删单个镜像
docker image rm 镜像ID
若提示被容器引用,先确认容器用途,别直接强制删。
注意事项
- 生产慎用-a和--volumes,卷里可能存数据库数据,删了不可恢复;
- 先docker system df看占用,再决定清哪一类;
- 清理前确认没有需要保留的停止容器和备份卷;
- 镜像仓库里还能重新拉取的镜像才适合-a全清;
- 定期配合定时任务清理,避免磁盘缓慢耗尽。
总结
日常用docker image prune清悬空镜像最安全,docker system prune清停止容器和缓存,-a和--volumes只在确认无保留价值时用。先df看占用、分级清理,避免误删业务卷和在用镜像。
PHP二维数组排序
PHP二维数组排序
引言
从数据库查出的列表通常是二维数组,要按价格、销量、时间某一字段升降序排列,单维的sort/rsort直接用会把结构打乱。需要按指定键排序,常用array_multisort和usort两种思路。
array_multisort按字段排
$goods = [
['name' => 'A商品', 'price' => 99],
['name' => 'B商品', 'price' => 39],
['name' => 'C商品', 'price' => 69],
];
// 抽出price列作为排序依据
$price = array_column($goods, 'price');
array_multisort($price, SORT_DESC, $goods);
print_r($goods); // 价格从高到低
array_column先取单列,multisort按该列降序,主数组同步跟着重排。
多字段组合排序
先按销量降序,销量相同再按价格升序:
$sales = array_column($goods, 'sales');
$price = array_column($goods, 'price');
array_multisort($sales, SORT_DESC, $price, SORT_ASC, $goods);
usort自定义比较
规则复杂或中文拼音排序时用usort:
usort($goods, function($a, $b) {
return $a['price'] <=> $b['price']; // 升序
});
太空船运算符<=>比较大小,降序交换$a、$b即可。
注意事项
- array_multisort会改变原数组顺序,需要原数据先备份;
- 比较的字段类型要一致,避免字符串和数字混比;
- usort是稳定排序,同值元素相对位置保留;
- 中文排序可配合intl扩展或自定义映射,别直接靠默认strcmp;
- 数据量大时,优先在SQL层ORDER BY,PHP排序只做内存小列表。
总结
简单单字段排序用array_column加array_multisort,多字段依次传入,复杂规则用usort加<=>比较。能在MySQL ORDER BY解决的就别把数据拉到PHP再排,减轻内存压力。