PHP报错Permission denied权限问题解决
前言
PHP写文件、读文件、上传文件,突然报"Permission denied"?这是Linux文件权限问题,最常见就是文件或目录的所有者不对、权限不够。本文讲怎么排查和解决。
一、报错长啥样
Warning: fopen(/data/test.txt): failed to open stream: Permission denied in /www/test.php on line 5
意思:PHP没权限操作这个文件/目录。
二、常见原因
原因1:文件/目录权限不够
PHP没有读或写的权限。
原因2:文件/目录所有者不对
PHP用www用户跑,但文件是root创建的,www没权限。
原因3:SELinux开着
CentOS默认开SELinux,即使权限对了也不让访问。
原因4:目录权限不够
要写文件的话,目录本身也要有写权限。
三、排查步骤
第一步:看报错的文件路径
报错信息里会告诉你是哪个文件:
Permission denied: /data/upload/test.jpg
就是 /data/upload/ 这个目录的问题。
第二步:看文件权限
ls -l /data/upload/
输出类似:
-rw-r--r-- 1 root root 1024 Oct 9 10:00 test.txt
看第一个字段:
-rw-r--r--表示所有者能读写,其他人只能读
看所有者:
root root表示所有者和组都是root
第三步:看PHP用什么用户跑
ps aux | grep php-fpm
输出里会看到:
www 1234 0.0 0.5 123456 7890 ? S 10:00 0:00 php-fpm: pool www
PHP是用 www 用户跑的。
如果文件所有者是root,那www用户就没权限写。
四、解决方法
方法1:改文件所有者
把文件/目录的所有者改成PHP的用户:
# 改单个文件
chown www:www /data/upload/test.txt
# 改整个目录(包括里面的文件)
chown -R www:www /data/upload/
推荐这个方法,最安全。
方法2:改权限
给所有人加权限:
# 文件加写权限
chmod 666 /data/upload/test.txt
# 目录加写权限
chmod 777 /data/upload/
注意: 777太不安全了,尽量用方法1改所有者。
方法3:看SELinux
CentOS开了SELinux的话,即使权限对了也不行。
检查SELinux:
getenforce
输出 Enforcing 就是开着。
临时关掉:
setenforce 0
永久关掉:
vi /etc/selinux/config
把:
SELINUX=enforcing
改成:
SELINUX=disabled
重启服务器生效。
或者只给目录加SELinux权限:
chcon -R -t httpd_sys_rw_content_t /data/upload/
五、常见场景
场景1:上传图片报错
用户上传图片,报Permission denied。
原因: uploads目录所有者不对。
解决:
chown -R www:www /www/wwwroot/你的站/uploads/
场景2:写日志报错
PHP写日志文件,报Permission denied。
原因: 日志目录没权限。
解决:
chown -R www:www /var/log/你的应用/
场景3:session不存
PHP session存不进去。
原因: session目录没权限。
解决:
chown -R www:www /var/lib/php/session/
场景4:缓存目录没权限
ThinkPHP/Laravel缓存写不进去。
原因: runtime目录没权限。
解决:
chown -R www:www runtime/
六、常用命令
看文件权限
ls -l 文件名
看目录权限
ls -ld 目录名
改所有者
chown 用户:组 文件
chown -R 用户:组 目录
改权限
chmod 755 目录
chmod 644 文件
七、权限数字含义
| 数字 | 含义 |
|---|---|
| 7 | 读+写+执行 (4+2+1) |
| 6 | 读+写 (4+2) |
| 5 | 读+执行 (4+1) |
| 4 | 只读 |
常用:
- 目录:755
- 文件:644
- 上传目录:775(或改所有者)
八、常见坑
坑1:只改了文件,没改目录
要写文件的话,目录本身也要有写权限。
解决: 目录也要改所有者。
坑2:递归改权限
chmod -R 777 目录
太危险了,所有人都能写。
解决: 优先改所有者,不要随便777。
坑3:SELinux偷偷开着
CentOS7以后默认开SELinux,权限对了也不行。
检查:
getenforce
坑4:父目录没权限
比如 /data/upload/ 有权限了,但 /data/ 没权限,还是进不去。
解决: 从根开始一层一层检查。
总结
记住:
- 报错先看是哪个文件/目录
- 看文件所有者是不是PHP的用户(一般是www)
- 不是就
chown -R www:www 目录 - 还不行就检查SELinux
- 尽量改所有者,不要随便777
最常用:
chown -R www:www /你的目录/
遇到问题加QQ23979811 协助处理
Redis 缓存穿透解决
Redis 缓存穿透解决
缓存穿透是常见问题,恶意请求能把数据库打挂。
1. 什么是缓存穿透
现象: 请求的数据在缓存和数据库中都不存在,每次请求都直接打到数据库。
危害:
- 恶意攻击时数据库压力大
- 正常请求被拖慢
- 数据库CPU飙升
2. 为什么会穿透
- 查询一个不存在的ID
- 缓存没有命中
- 数据库也查不到
- 请求直接穿透到数据库
3. 解决方案一:缓存空值
思路: 即使数据库查不到,也在Redis存一个空值,设置短过期时间。
function getUser($id) {
$key = "user:$id";
$user = $redis->get($key);
if ($user !== false) {
// 空值标记
if ($user === 'null') {
return null;
}
return json_decode($user, true);
}
// 查数据库
$user = $db->query("SELECT * FROM users WHERE id = $id");
if ($user) {
// 存在就缓存1小时
$redis->setex($key, 3600, json_encode($user));
} else {
// 不存在也缓存,5分钟过期
$redis->setex($key, 300, 'null');
}
return $user;
}
优点: 简单有效
缺点: 占内存,可能被恶意key刷爆
4. 解决方案二:布隆过滤器
思路: 在缓存前面加一层布隆过滤器,不存在的key直接拦截。
原理:
- 所有存在的数据先存到布隆过滤器
- 请求先过布隆过滤器
- 不存在直接返回,不查缓存和数据库
PHP 示例(用 Redis 的布隆过滤器模块):
# 加载 RedisBloom 模块
redis-cli BF.ADD user:1
redis-cli BF.EXISTS user:1
function getUser($id) {
$key = "user:$id";
// 先过布隆过滤器
if (!$redis->rawCommand('BF.EXISTS', 'user_filter', $id)) {
return null; // 一定不存在
}
// 布隆说可能存在,再查缓存
$user = $redis->get($key);
if ($user) {
return json_decode($user, true);
}
// 查数据库...
}
优点: 内存占用小,效率高
缺点: 有误判率,实现复杂
5. 解决方案三:参数校验
思路: 从源头过滤非法请求。
function getUser($id) {
// 参数校验
if ($id <= 0 || !is_numeric($id)) {
return null;
}
// 正常查询逻辑...
}
适用: ID类查询,不合法直接拦截
6. 三种方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 缓存空值 | 简单 | 占内存 | 数据量不大 |
| 布隆过滤器 | 高效 | 误判,复杂 | 数据量大 |
| 参数校验 | 最直接 | 只防非法 | ID类请求 |
7. 生产建议
1)组合使用
- 先做参数校验
- 再缓存空值
- 大流量用布隆过滤器
2)空值过期时间要短
- 5分钟到10分钟
- 不要太长
3)监控异常请求
- 发现大量404要警惕
- 及时加防护
4)接口限流
- 用 Nginx 或网关限流
- 防止恶意刷
8. 缓存雪崩和击穿
缓存穿透: 查不存在的数据
缓存击穿: 热点key过期,大量请求打数据库
缓存雪崩: 大量key同时过期
区分清楚: 三个问题现象类似但原因不同,解决方案也不同。
总结
Redis 缓存穿透解决要点:
- 查不存在的数据穿透到数据库
- 缓存空值是最简单方案
- 布隆过滤器适合大数据量
- 参数校验从源头拦截
- 空值过期时间要短
- 生产建议组合使用
- 注意和雪崩、击穿区分开
- 监控异常请求
- 接口加限流
缓存穿透是面试高频题,也是生产常见问题,一定要掌握。
Nginx 配置HTTPS证书
Nginx 配置HTTPS证书
现在网站都要HTTPS,配置证书是必备技能。
1. 为什么要HTTPS
- 加密传输,防窃听
- 浏览器安全标识
- SEO 排名加分
- 现在主流浏览器都要求HTTPS
2. 证书文件
一般有两个文件:
.crt:证书文件.key:私钥文件
# 证书文件放在这里
/etc/nginx/ssl/
├── example.com.crt
└── example.com.key
3. 基本HTTPS配置
server {
listen 443 ssl;
server_name example.com;
# 证书配置
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# 其他配置
location / {
root /var/www/html;
index index.html;
}
}
4. HTTP 跳转 HTTPS
# HTTP 重定向到 HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
5. 优化配置
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# 优化SSL配置
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# 加密套件
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
}
6. 检查配置
# 检查配置语法
nginx -t
# 重载配置
nginx -s reload
7. 证书申请
免费证书:
- Let's Encrypt(用 certbot 申请)
- 阿里云免费证书
- 腾讯云免费证书
certbot 申请:
# 安装 certbot
yum install certbot python3-certbot-nginx
# 自动申请并配置
certbot --nginx -d example.com
自动续期:
# 测试续期
certbot renew --dry-run
# 加入定时任务
0 3 * * * certbot renew --quiet
8. 常见问题
1)证书不生效
- 检查证书路径对不对
- 检查文件权限
- 重载 Nginx
2)浏览器提示不安全
- 证书过期了
- 域名不匹配
- 中间证书没装全
3)HTTPS 访问慢
- 开启 HTTP/2
- 优化 SSL 会话缓存
4)证书续期失败
- 检查域名解析
- 检查80端口是否通
- 手动运行 certbot 看报错
9. 证书有效期
- 免费证书一般 3 个月
- 要设置自动续期
- 不然到期网站就挂了
10. 生产建议
1)强制 HTTPS
- HTTP 全部 301 跳转
- 开启 HSTS
2)开启 HTTP/2
- listen 443 ssl http2
- 提升访问速度
3)定期检查证书
- 监控证书到期时间
- 提前续期
4)中间证书
- 有些证书要把中间证书合并
- 不然浏览器不信任
总结
Nginx 配置HTTPS要点:
- 配置证书和私钥路径
- HTTP 跳转 HTTPS
- 开启 HTTP/2
- 优化 SSL 会话缓存
- 用 certbot 免费申请
- 设置自动续期
- 定期检查证书有效期
- 开启 HSTS
- 注意中间证书问题
HTTPS 是网站标配,一定要配置好。
Git 常用命令大全
Git 常用命令大全
Git 是开发必备工具,常用命令一定要熟练。
1. 初始化仓库
新建仓库:
git init
克隆远程仓库:
git clone https://github.com/user/repo.git
克隆指定分支:
git clone -b dev https://github.com/user/repo.git
2. 查看状态
查看状态:
git status
查看提交历史:
git log
简洁历史:
git log --oneline --graph
查看文件修改:
git diff
3. 提交代码
添加所有修改:
git add .
添加指定文件:
git add index.php
提交:
git commit -m "修复bug"
添加并提交:
git commit -am "修复bug"
4. 远程操作
查看远程仓库:
git remote -v
拉取远程更新:
git pull
推送代码:
git push
推送并设置上游分支:
git push -u origin dev
5. 分支操作
查看本地分支:
git branch
查看所有分支:
git branch -a
创建分支:
git branch dev
切换分支:
git checkout dev
创建并切换:
git checkout -b dev
合并分支:
# 先切换到master
git checkout master
# 合并dev分支
git merge dev
删除分支:
git branch -d dev
强制删除:
git branch -D dev
6. 撤销操作
撤销工作区修改:
git checkout -- index.php
撤销暂存区:
git reset HEAD index.php
回退到上个版本:
git reset --hard HEAD^
回退到指定版本:
git reset --hard commit_id
7. 标签操作
打标签:
git tag v1.0.0
查看标签:
git tag
推送标签:
git push origin v1.0.0
推送所有标签:
git push origin --tags
8. 暂存操作
暂存当前修改:
git stash
查看暂存列表:
git stash list
恢复暂存:
git stash pop
删除暂存:
git stash drop
9. 其他常用
配置用户名:
git config --global user.name "张三"
配置邮箱:
git config --global user.email "zhangsan@example.com"
查看配置:
git config --list
查看当前分支:
git branch --show-current
10. 常用工作流
开发新功能:
# 拉最新代码
git pull
# 创建功能分支
git checkout -b feature/user
# 开发完提交
git add .
git commit -m "添加用户功能"
# 推送
git push -u origin feature/user
总结
Git 常用命令要点:
- git init 初始化仓库
- git clone 克隆仓库
- git status 看状态
- git add 添加修改
- git commit 提交
- git push 推送
- git pull 拉取
- git branch 分支操作
- git checkout 切换分支
- git merge 合并分支
- git reset 回退
- git stash 暂存
Git 熟练了,团队协作效率提升很多。
PHP file_get_contents获取https内容失败
前言
PHP用file_get_contents获取http内容没问题,换成https就报错了?这是最常见的问题,一般就是openssl扩展没开,或者SSL证书验证没过。本文讲怎么解决。
一、问题现象
<?php
$url = 'https://api.example.com/data';
$result = file_get_contents($url);
echo $result;
结果报错:
Warning: file_get_contents(): failed to open stream: Unable to find the wrapper "https"
或者:
Warning: file_get_contents(): SSL operation failed with code 1
二、常见原因
原因1:openssl扩展没装
PHP默认不开openssl扩展,file_get_contents不支持https。
原因2:allow_url_open没开
php.ini里禁了远程打开。
原因3:SSL证书验证失败
对方网站证书有问题,PHP验证不通过。
原因4:网络问题
服务器出不去,或者DNS解析不了。
三、排查步骤
第一步:看openssl扩展装了没
php -m | grep openssl
有输出就是装了,没输出就是没装。
或者写个phpinfo:
<?php
phpinfo();
搜"openssl",看有没有。
第二步:看allow_url_open开了没
<?php
echo ini_get('allow_url_fopen');
输出1就是开了,空就是没开。
第三步:用curl测试
curl https://api.example.com/data
curl能通就是PHP的问题,curl也不通就是网络问题。
四、解决方法
方法1:装openssl扩展(推荐)
CentOS
yum install php-openssl
或者:
yum install php7x-openssl
(x是你的PHP版本)
Ubuntu
apt install php-openssl
装完重启
systemctl restart php-fpm
方法2:改php.ini配置
打开php.ini,找到:
;extension=openssl
去掉前面的分号:
extension=openssl
然后重启php-fpm。
方法3:关掉SSL验证(临时)
不想改配置,可以在代码里关验证:
<?php
$context = stream_context_create([
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false
]
]);
$result = file_get_contents('https://api.example.com/data', false, $context);
echo $result;
注意: 这是临时方案,生产环境建议把证书配好。
方法4:改用curl(推荐)
file_get_contents不好用,直接用curl:
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.example.com/data');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$result = curl_exec($ch);
if (curl_errno($ch)) {
echo '错误: ' . curl_error($ch);
}
curl_close($ch);
echo $result;
curl比file_get_contents好用多了,推荐用curl。
五、常见坑
坑1:CLI和FPM配置不一样
命令行里openssl装了,网页上还是不行。
原因: CLI和FPM用不同的php.ini。
解决: 看phpinfo里的配置路径,改对应的那个。
坑2:装了扩展没重启
装完忘了重启php-fpm。
解决:
systemctl restart php-fpm
坑3:对方网站证书过期
对方网站SSL证书过期了,PHP验证不通过。
解决: 关验证,或者让对方更新证书。
坑4:allow_url_fopen没开
php.ini里禁了远程打开。
解决:
allow_url_fopen = On
坑5:防火墙挡了出站
服务器防火墙挡了443端口出站。
解决:
# 看能不能出去
curl -I https://www.baidu.com
# 不行的话开防火墙
firewall-cmd --add-port=443/tcp --permanent
firewall-cmd --reload
六、POST请求示例
file_get_contents发POST:
<?php
$url = 'https://api.example.com/submit';
$data = http_build_query([
'name' => '张三',
'age' => 20
]);
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'content' => $data,
'timeout' => 30
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false
]
]);
$result = file_get_contents($url, false, $context);
echo $result;
还是建议用curl,更灵活。
七、封装函数
function http_get($url, $timeout = 30)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
$result = curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
return ['code' => -1, 'msg' => $error];
}
curl_close($ch);
return ['code' => 0, 'data' => $result];
}
// 使用
$res = http_get('https://api.example.com/data');
if ($res['code'] == 0) {
echo $res['data'];
} else {
echo '请求失败: ' . $res['msg'];
}
总结
记住:
- file_get_contents不支持https,是openssl扩展没装
- 装openssl扩展,重启php-fpm
- 或者关SSL验证临时解决
- 最推荐:改用curl,比file_get_contents好用多了
推荐方案:
直接用curl,别用file_get_contents搞https。
遇到问题加QQ23979811 协助处理
PHP字符串处理常用函数,substr、explode
前言
PHP字符串处理天天用,截取、拼接、查找、替换,每个项目都离不开。本文整理最常用的字符串函数,收藏随用随查。
一、字符串长度
strlen()
字节长度(中文算3个字节):
echo strlen('hello'); // 5
echo strlen('你好'); // 6(UTF-8每个中文3字节)
mb_strlen()
字符长度(中文算1个字符):
echo mb_strlen('你好', 'utf-8'); // 2
区别: strlen看字节,mb_strlen看字符。
二、截取字符串
substr()
$str = 'hello world';
// 从第0位取5个
echo substr($str, 0, 5); // hello
// 从第6位取到最后
echo substr($str, 6); // world
// 从倒数第5位取
echo substr($str, -5); // world
mb_substr()(中文截取)
$str = '我是中国人';
// 从第0个字符取2个
echo mb_substr($str, 0, 2, 'utf-8'); // 我是
注意: 中文一定要用mb_substr,不然会乱码。
三、查找字符串
strpos()
找位置(从左找,找到返回位置,没找到返回false):
$str = 'hello world';
echo strpos($str, 'world'); // 6
// 没找到
var_dump(strpos($str, 'abc')); // bool(false)
strrpos()
从右找:
echo strrpos('a/b/c', '/'); // 3
strstr()
找并截取:
$str = 'user@example.com';
echo strstr($str, '@'); // @example.com
str_contains()(PHP8)
var_dump(str_contains('hello world', 'world')); // bool(true)
注意: PHP8以上才有。
四、替换字符串
str_replace()
$str = 'hello world';
echo str_replace('world', 'php', $str); // hello php
str_ireplace()
不区分大小写:
echo str_ireplace('WORLD', 'php', $str); // hello php
preg_replace()
正则替换:
$str = '2026-10-09';
// 把横线换成点
echo preg_replace('/-/g', '.', $str); // 2026.10.09
五、拼接和拆分
implode()(数组转字符串)
$arr = ['a', 'b', 'c'];
echo implode(',', $arr); // a,b,c
echo implode('-', $arr); // a-b-c
explode()(字符串转数组)
$str = 'a,b,c';
$arr = explode(',', $str);
// ['a', 'b', 'c']
限制拆分次数
$str = 'a,b,c,d';
// 只拆成2段
$arr = explode(',', $str, 2);
// ['a', 'b,c,d']
六、去除空白
trim()
去首尾空格:
$str = ' hello ';
echo trim($str); // hello
ltrim() / rtrim()
ltrim($str); // 去左边
rtrim($str); // 去右边
七、大小写转换
strtoupper('hello'); // HELLO
strtolower('HELLO'); // hello
ucwords('hello world'); // Hello World
ucfirst('hello'); // Hello
八、重复和填充
str_repeat()
echo str_repeat('ab', 3); // ababab
str_pad()
// 补到10位,右边补*
echo str_pad('123', 10, '*', STR_PAD_RIGHT); // 123*******
// 左边补
echo str_pad('123', 10, '0', STR_PAD_LEFT); // 0000000123
九、常见场景
场景1:手机号中间打星号
$phone = '13812345678';
echo substr($phone, 0, 3) . '****' . substr($phone, -4);
// 138****5678
场景2:截取文件名后缀
$file = 'photo.jpg';
echo strrchr($file, '.'); // .jpg
场景3:邮箱拆分
$email = 'user@example.com';
$name = strstr($email, '@', true); // user
$domain = strstr($email, '@'); // @example.com
场景4:URL参数解析
$url = 'https://example.com/index.php?id=1&name=zhangsan';
// 拿问号后面的
$query = parse_url($url, PHP_URL_QUERY);
// id=1&name=zhangsan
parse_str($query, $params);
// ['id' => '1', 'name' => 'zhangsan']
场景5:HTML标签转义
$str = '<script>alert(1)</script>';
echo htmlspecialchars($str);
// <script>alert(1)</script>
十、常见坑
坑1:中文截取乱码
用substr截中文,结果乱码了。
原因: substr按字节截,中文一个字3个字节,容易截到半个字。
解决: 用mb_substr:
mb_substr($str, 0, 10, 'utf-8');
坑2:strpos===0的问题
$str = 'abc';
if (strpos($str, 'a') == false) {
echo '没找到';
}
问题: strpos找到a在位置0,0==false是true。
解决:
if (strpos($str, 'a') === false) {
echo '没找到';
}
注意: 一定要用===,不是==。
坑3:trim只去空格
trim默认只去空格和换行。
要去指定字符:
trim($str, "/"); // 去首尾的/
坑4:explode空字符串
$str = '';
$arr = explode(',', $str);
// [''] 不是空数组!
解决:
if ($str === '') {
$arr = [];
} else {
$arr = explode(',', $str);
}
总结
最常用的10个:
strlen()字符串长度(字节)mb_strlen()字符长度(中文友好)substr()截取字符串strpos()查找位置str_replace()替换explode()字符串转数组implode()数组转字符串trim()去空格strtoupper()/strtolower()大小写str_pad()填充
记住:
- 中文用mb_*系列函数
- 查找用=== false判断
- 数组字符串互转用explode/implode
遇到问题加QQ23979811 协助处理
Docker 常用命令大全
Docker 常用命令大全
Docker 现在开发必备,常用命令一定要熟练。
1. 镜像命令
查看本地镜像:
docker images
搜索镜像:
docker search nginx
拉取镜像:
docker pull nginx:latest
删除镜像:
docker rmi nginx:latest
删除所有镜像:
docker rmi $(docker images -q)
构建镜像:
# 在Dockerfile目录下
docker build -t myapp:1.0 .
2. 容器命令
查看运行中的容器:
docker ps
查看所有容器(包括停止的):
docker ps -a
启动容器:
docker run -d --name mynginx -p 80:80 nginx
参数说明:
- -d:后台运行
- --name:容器名
- -p:端口映射(主机:容器)
- -v:目录挂载
停止容器:
docker stop mynginx
启动已停止的容器:
docker start mynginx
重启容器:
docker restart mynginx
删除容器:
docker rm mynginx
强制删除运行中的容器:
docker rm -f mynginx
删除所有停止的容器:
docker container prune
3. 进入容器
进入容器命令行:
docker exec -it mynginx /bin/bash
退出容器:
exit
4. 日志查看
查看容器日志:
docker logs mynginx
实时查看日志:
docker logs -f mynginx
查看最近100行日志:
docker logs --tail 100 mynginx
5. 文件操作
从主机复制到容器:
docker cp index.html mynginx:/usr/share/nginx/html/
从容器复制到主机:
docker cp mynginx:/etc/nginx/nginx.conf ./
6. 容器信息
查看容器详情:
docker inspect mynginx
查看容器端口映射:
docker port mynginx
查看容器资源使用:
docker stats
7. 数据卷
创建数据卷:
docker volume create mydata
查看数据卷:
docker volume ls
使用数据卷:
docker run -d -v mydata:/data nginx
8. 网络
查看网络:
docker network ls
创建网络:
docker network create mynet
容器加入网络:
docker network connect mynet mynginx
9. 其他常用
查看Docker版本:
docker version
查看Docker信息:
docker info
清理无用资源:
docker system prune
10. 常用组合
启动一个PHP环境:
docker run -d --name php \
-p 8080:80 \
-v /www:/var/www/html \
php:apache
启动MySQL:
docker run -d --name mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=123456 \
-v /data/mysql:/var/lib/mysql \
mysql:5.7
启动Redis:
docker run -d --name redis \
-p 6379:6379 \
redis:latest
总结
Docker 常用命令要点:
- docker images 看镜像
- docker ps 看容器
- docker run 启动容器
- docker exec 进入容器
- docker logs 看日志
- docker cp 复制文件
- docker stats 看资源
- -d 后台运行
- -p 端口映射
- -v 目录挂载
Docker 命令熟练了,开发部署效率提升很多。
MySQL 索引优化技巧
MySQL 索引优化技巧
索引是提升查询性能的关键,但用不好反而拖慢速度。
1. 为什么要建索引
- 没有索引要全表扫描
- 数据量大时查询很慢
- 索引相当于书的目录
- 合适的索引能提升百倍速度
2. 索引类型
主键索引:
- 主键自动创建
- 唯一且非空
普通索引:
- 最基本的索引
- 加速查询
唯一索引:
- 值不能重复
- 比如手机号
联合索引:
- 多个字段组成
- 遵循最左前缀原则
3. 建索引原则
1)最频繁查询的字段建索引
-- 经常按name查
CREATE INDEX idx_name ON users(name);
2)区分度高的字段建索引
- 性别这种区分度低的不要建
- 手机号、身份证这种适合
3)联合索引注意顺序
-- 联合索引 (a, b, c)
-- 最左前缀:
-- a 能用
-- a,b 能用
-- a,b,c 能用
-- 单独b,c 用不了
4. 索引失效情况
1)where 里对索引字段用函数
-- 失效
SELECT * FROM users WHERE DATE(create_time) = '2026-01-01';
-- 有效
SELECT * FROM users WHERE create_time >= '2026-01-01' AND create_time < '2026-01-02';
2)where 里对索引字段做运算
-- 失效
SELECT * FROM users WHERE age + 1 = 20;
-- 有效
SELECT * FROM users WHERE age = 19;
3)like 以 % 开头
-- 失效
SELECT * FROM users WHERE name LIKE '%张';
-- 有效
SELECT * FROM users WHERE name LIKE '张%';
4)隐式类型转换
-- phone是字符串
-- 失效(数字转字符串)
SELECT * FROM users WHERE phone = 13800138000;
-- 有效
SELECT * FROM users WHERE phone = '13800138000';
5. EXPLAIN 查看执行计划
EXPLAIN SELECT * FROM users WHERE name = '张三';
重点看:
- type: 访问类型,至少要 range
- key: 实际用到的索引
- rows: 扫描行数
- Extra: 额外信息
6. 索引不要建太多
- 每个索引都占空间
- 插入更新时要维护索引
- 不是越多越好
- 一张表索引建议不超过5个
7. 覆盖索引
避免回表:
-- 索引包含查询的所有字段
SELECT name, age FROM users WHERE name = '张三';
-- 索引 (name, age) 就是覆盖索引
-- 不用回表查主键
Extra 显示 Using index 就是覆盖索引。
8. 慢查询定位
-- 开启慢查询
slow_query_log = 1
long_query_time = 1 -- 超过1秒记录
-- 查看慢查询日志
-- 找到慢SQL,加索引优化
9. 常见优化场景
1)分页查询优化
-- 慢(深分页)
SELECT * FROM orders LIMIT 100000, 10;
-- 快(子查询)
SELECT * FROM orders
WHERE id >= (SELECT id FROM orders LIMIT 100000, 1)
LIMIT 10;
2)JOIN 字段建索引
- JOIN 的两边都要建索引
- 类型要一致
3)ORDER BY 用索引
- 排序字段建索引
- 避免 filesort
10. 索引维护
查看索引:
SHOW INDEX FROM users;
删除多余索引:
DROP INDEX idx_name ON users;
分析表:
ANALYZE TABLE users;
总结
MySQL 索引优化要点:
- 区分度高的字段建索引
- 联合索引遵循最左前缀
- 避免索引失效(函数、运算、like%)
- 用 EXPLAIN 分析执行计划
- 索引不要建太多
- 覆盖索引避免回表
- 慢查询日志找问题SQL
- 深分页要优化
- JOIN 两边都建索引
- 定期维护索引
索引优化是MySQL性能优化的核心,一定要多实践。
PHP数组常用函数大全,array操作
前言
PHP数组用得最多,几乎每个脚本都要操作数组。本文整理最常用的数组函数,收藏起来随用随查。
一、创建和遍历
数组定义
<?php
// 索引数组
$arr = [1, 2, 3, 4, 5];
// 关联数组
$user = [
'name' => '张三',
'age' => 20,
'email' => 'zhangsan@qq.com'
];
遍历
// foreach遍历
foreach ($arr as $value) {
echo $value;
}
// 带key
foreach ($user as $key => $value) {
echo "$key: $value";
}
二、增删改查
添加元素
// 末尾加
$arr[] = 6;
array_push($arr, 7, 8);
// 开头加
array_unshift($arr, 0);
// 关联数组加
$user['phone'] = '13800138000';
删除元素
// 删最后一个
array_pop($arr);
// 删第一个
array_shift($arr);
// 删指定key
unset($user['age']);
// 删多个
unset($arr[0], $arr[1]);
修改元素
$arr[0] = 100;
$user['name'] = '李四';
查找元素
// 值是否在数组里
in_array(3, $arr); // true
// 找key
array_key_exists('name', $user); // true
// 找值的key
array_search(2, $arr); // 1
三、数组统计
长度
count($arr);
sizeof($arr); // 和count一样
求和、最大、最小
array_sum($arr); // 求和
max($arr); // 最大
min($arr); // 最小
去重
$unique = array_unique($arr);
打乱
shuffle($arr);
四、排序
索引数组排序
sort($arr); // 升序
rsort($arr); // 降序
关联数组排序
asort($user); // 按值升序
arsort($user); // 按值降序
ksort($user); // 按key升序
krsort($user); // 按key降序
五、拆分和合并
取一段
// 从第2个开始取3个
$slice = array_slice($arr, 1, 3);
合并数组
$arr1 = [1, 2];
$arr2 = [3, 4];
$merge = array_merge($arr1, $arr2); // [1,2,3,4]
交差
$arr1 = [1, 2, 3];
$arr2 = [2, 3, 4];
array_intersect($arr1, $arr2); // [2,3] 交集
array_diff($arr1, $arr2); // [1] 差集
六、key和value操作
取所有key
$keys = array_keys($user);
// ['name', 'age', 'email']
取所有value
$values = array_values($user);
// ['张三', 20, 'zhangsan@qq.com']
翻转key和value
$flipped = array_flip($user);
填充数组
// 5个元素,值都是0
$arr = array_fill(0, 5, 0);
七、数组和字符串互转
数组转字符串
$arr = ['a', 'b', 'c'];
$str = implode(',', $arr); // 'a,b,c'
字符串转数组
$str = 'a,b,c';
$arr = explode(',', $str); // ['a', 'b', 'c']
八、常用场景
场景1:数组去重
$arr = [1, 2, 2, 3, 3, 3];
$unique = array_unique($arr);
// [1, 2, 3]
场景2:查值在不在
$fruits = ['apple', 'banana', 'orange'];
if (in_array('apple', $fruits)) {
echo '有苹果';
}
场景3:二维数组按key排序
$users = [
['name' => '张三', 'age' => 20],
['name' => '李四', 'age' => 25],
['name' => '王五', 'age' => 18]
];
// 按age排序
array_multisort(array_column($users, 'age'), SORT_ASC, $users);
场景4:数组分页
$page = 1;
$pageSize = 10;
$offset = ($page - 1) * $pageSize;
$pageData = array_slice($allData, $offset, $pageSize);
场景5:过滤空值
$arr = ['a', '', null, 'b', 0, 'c'];
$filtered = array_filter($arr);
// ['a', 'b', 'c'] 空字符串、null、0都被过滤了
九、常见坑
坑1:count(null)报错
PHP7.2以后,count(null)会警告。
解决:
$count = is_array($arr) ? count($arr) : 0;
坑2:==和===的区别
in_array('123', [123]); // true(==)
in_array('123', [123], true); // false(===)
注意: 第三个参数true才是严格比较。
坑3:array_merge和+的区别
$arr1 = ['a' => 1, 'b' => 2];
$arr2 = ['b' => 3, 'c' => 4];
array_merge($arr1, $arr2); // ['a'=>1, 'b'=>3, 'c'=>4]
$arr1 + $arr2; // ['a'=>1, 'b'=>2, 'c'=>4]
区别: array_merge后者覆盖前者,+前者保留。
坑4:引用传递
foreach ($arr as &$value) {
$value = $value * 2;
}
unset($value); // 一定要unset,不然后面会出问题
总结
最常用的10个:
count()数组长度in_array()值是否在数组里array_key_exists()key是否存在array_unique()去重array_merge()合并array_slice()取一段array_sum()求和explode()字符串转数组implode()数组转字符串array_column()取二维数组某一列
遇到问题加QQ23979811 协助处理
PHP cURL请求超时,超时时间设置
前言
PHP用cURL请求接口,结果一直卡住或者超时?cURL默认没有超时时间,对方服务器慢的话能卡你几分钟。必须设超时,不然整个脚本都堵死了。本文讲怎么设。
一、为什么要设超时
不设超时的后果:
- 对方服务器挂了,你的脚本一直等着
- 对方服务器慢,你的脚本卡很久
- 高并发下,进程都堵在cURL上,服务器卡死
一定要设!
二、两个超时参数
1. CURLOPT_CONNECTTIMEOUT
连接超时,单位秒。
就是TCP连接花多久。
推荐: 5-10秒
2. CURLOPT_TIMEOUT
总超时,单位秒。
整个请求(连接+发送+接收)花多久。
推荐: 30-60秒
区别
| 参数 | 含义 | 推荐值 |
|---|---|---|
| CURLOPT_CONNECTTIMEOUT | 连接超时 | 5秒 |
| CURLOPT_TIMEOUT | 总超时 | 30秒 |
三、完整示例
<?php
$url = 'https://api.example.com/data';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 连接超时(秒)
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
// 总超时(秒)
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
// 毫秒级超时(可选)
// curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 5000);
// curl_setopt($ch, CURLOPT_TIMEOUT_MS, 30000);
$result = curl_exec($ch);
if (curl_errno($ch)) {
echo 'cURL错误: ' . curl_error($ch);
}
curl_close($ch);
echo $result;
四、超时错误码
常见错误码
| 错误码 | 说明 |
|---|---|
| 28 | 超时(CURLE_OPERATION_TIMEDOUT) |
| 7 | 连接失败(CURLE_COULDNT_CONNECT) |
| 6 | 域名解析失败(CURLE_COULDNT_RESOLVE_HOST) |
检查错误码
if (curl_errno($ch) == 28) {
echo "请求超时了";
}
五、常见坑
坑1:timeout设成了毫秒
CURLOPT_TIMEOUT的单位是秒,不是毫秒!
错误:
curl_setopt($ch, CURLOPT_TIMEOUT, 5000); // 5000秒,不是5秒!
正确:
curl_setopt($ch, CURLOPT_TIMEOUT, 5); // 5秒
或者用毫秒版:
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 5000); // 5000毫秒 = 5秒
坑2:set_time_limit不够
PHP脚本本身有最大执行时间。
如果cURL超时30秒,但PHP脚本只允许10秒,PHP先超时了。
解决:
set_time_limit(0); // 不限制
// 或者
set_time_limit(60); // 60秒
坑3:DNS解析慢
域名解析也要时间,算在连接超时里。
解决: 连接超时设长一点,比如10秒。
坑4:https证书问题
https请求如果证书有问题,也会卡住或报错。
临时关闭验证:
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
注意: 生产环境不建议关,最好把证书配好。
坑5:重定向太多
对方返回301/302重定向,cURL默认不自动跳。
开启自动跳转:
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3); // 最多跳3次
六、POST请求示例
<?php
$url = 'https://api.example.com/submit';
$data = [
'name' => '张三',
'age' => 20
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$result = curl_exec($ch);
if (curl_errno($ch)) {
echo '错误: ' . curl_error($ch);
}
curl_close($ch);
echo $result;
七、封装成函数
function http_curl($url, $post = false, $data = [], $timeout = 30)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
if ($post) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? http_build_query($data) : $data);
}
$result = curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
return ['code' => -1, 'msg' => $error];
}
curl_close($ch);
return ['code' => 0, 'data' => $result];
}
// 使用
$res = http_curl('https://api.example.com/data');
if ($res['code'] == 0) {
echo $res['data'];
} else {
echo '请求失败: ' . $res['msg'];
}
八、推荐配置
普通接口请求
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5秒连接
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // 30秒总超时
快速接口(查个数据)
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); // 3秒连接
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // 10秒总超时
慢接口(上传大文件)
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 10秒连接
curl_setopt($ch, CURLOPT_TIMEOUT, 120); // 2分钟总超时
总结
记住:
- cURL一定要设超时,不然卡死
CURLOPT_CONNECTTIMEOUT连接超时(秒)CURLOPT_TIMEOUT总超时(秒)- 单位是秒,不是毫秒
- 超时了用
curl_errno判断是不是28
最常用:
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
遇到问题加QQ23979811 协助处理
Nginx 反向代理配置
Nginx 反向代理配置
反向代理是 Nginx 最常用的功能之一,配置很简单。
1. 什么是反向代理
反向代理就是:用户访问 Nginx,Nginx 把请求转发给后端服务器,再把结果返回给用户。
作用:
- 隐藏后端服务器
- 负载均衡
- 统一入口
- 缓存静态内容
2. 基本配置
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
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_pass:后端地址
- proxy_set_header:传递请求头
3. 常用请求头
location / {
proxy_pass http://127.0.0.1:8080;
# 传递域名
proxy_set_header Host $host;
# 传递真实IP
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;
}
4. 超时配置
location / {
proxy_pass http://127.0.0.1:8080;
# 连接超时
proxy_connect_timeout 60s;
# 发送超时
proxy_send_timeout 60s;
# 读取超时
proxy_read_timeout 60s;
}
5. WebSocket 代理
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# WebSocket 升级
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
6. 负载均衡
# 定义后端服务器组
upstream backend {
server 127.0.0.1:8080 weight=1;
server 127.0.0.1:8081 weight=2;
server 127.0.0.1:8082 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
}
}
7. 静态文件直接返回
server {
listen 80;
server_name example.com;
# 静态文件直接处理
location ~* \.(js|css|png|jpg|gif)$ {
root /var/www/static;
expires 30d;
}
# 其他请求转发后端
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
}
}
8. 配置生效
# 检查配置
nginx -t
# 重载配置
nginx -s reload
9. 常见问题
1)502 Bad Gateway
- 后端服务没启动
- 后端端口不对
- 后端超时
2)504 Gateway Timeout
- 后端处理太慢
- 调整超时时间
3)获取不到真实IP
- 没配置 X-Real-IP
- 后端没读取请求头
4)WebSocket 连不上
- 没配置 Upgrade 和 Connection 头
10. 生产建议
1)一定要传请求头
- Host
- X-Real-IP
- X-Forwarded-For
2)合理设置超时
- 根据业务调整
- 不要太长也不要太短
3)静态文件 Nginx 直接处理
- 减轻后端压力
- 提高性能
4)开启 gzip 压缩
gzip on;
gzip_types text/css application/javascript;
总结
Nginx 反向代理配置要点:
- proxy_pass 指定后端地址
- 一定要传请求头
- 常用超时配置
- WebSocket 要升级头
- upstream 做负载均衡
- 静态文件直接返回
- 配置完 nginx -t 检查
- 502 检查后端是否启动
- 生产环境传全请求头
反向代理是 Nginx 基本功,一定要熟练掌握。
Linux 查看内存使用
Linux 查看内存使用
服务器内存不够用的时候,得知道谁在吃内存。
1. free 命令
最常用的查看内存命令:
free -h
输出说明:
- total: 总内存
- used: 已用内存
- free: 空闲内存
- shared: 共享内存
- buff/cache: 缓存
- available: 可用内存
注意: 不要只看 free,要看 available。Linux 会用空闲内存做缓存,这是好事。
2. top 命令
实时查看内存占用:
top
常用操作:
- 按
M按内存排序 - 按
P按CPU排序 - 按
q退出
重点看:
- %MEM:进程内存占用百分比
- RES:进程实际占用物理内存
3. ps 命令
查看进程内存占用:
# 按内存占用排序,前10
ps aux --sort=-%mem | head -10
输出说明:
- USER:进程用户
- %MEM:内存占用
- RSS:实际物理内存(KB)
- COMMAND:进程命令
4. vmstat 命令
查看整体内存和CPU:
# 每秒刷新一次
vmstat 1
重点列:
- swpd:交换分区使用
- free:空闲内存
- buff:缓冲区
- cache:缓存
- si:换入内存
- so:换出内存
注意: si 和 so 大于0说明内存不够,在用swap。
5. /proc/meminfo
查看详细内存信息:
cat /proc/meminfo
重点看:
- MemTotal:总内存
- MemFree:空闲内存
- MemAvailable:可用内存
- Buffers:缓冲区
- Cached:缓存
- SwapTotal:交换分区总量
- SwapFree:交换分区空闲
6. 缓存说明
Linux 内存缓存机制:
- 空闲内存用来做文件缓存
- 程序需要时会自动释放
- cached 高不是坏事
- available 才是真正可用的
7. 内存不够怎么办
1)找出吃内存的进程
ps aux --sort=-%mem | head
2)重启大内存进程
- 确认是哪个进程
- 重启释放内存
3)清理缓存
# 清理页面缓存
echo 1 > /proc/sys/vm/drop_caches
# 清理目录项和inode
echo 2 > /proc/sys/vm/drop_caches
# 全部清理
echo 3 > /proc/sys/vm/drop_caches
4)加交换分区
- 临时方案,治标不治本
- 最好加物理内存
8. 常用组合命令
# 查看内存占用前5的进程
ps aux --sort=-%mem | head -6
# 查看swap使用最多的进程
for i in $(ls /proc | grep -E '^[0-9]+'); do
awk '/VmSwap/{print $2/1024 " MB", $0}' /proc/$i/status 2>/dev/null
done | sort -nr | head
9. 监控建议
1)设置内存告警
- 超过80%告警
- 及时处理
2)定期检查
- 每天看一次内存
- 发现异常及时处理
3)不要盲目清缓存
- 缓存是好事
- 清缓存会降低性能
总结
Linux 查看内存使用要点:
- free -h 看整体内存
- top 按M看内存占用
- ps aux 排序找大进程
- vmstat 看swap使用
- /proc/meminfo 看详细信息
- 不要只看 free,要看 available
- cached 高是好事
- 内存不够先找大进程
- 清缓存要谨慎
- 最好加物理内存
学会看内存,服务器问题就好排查了。
Redis 持久化机制
Redis 持久化机制
Redis 是内存数据库,断电数据就没了,所以需要持久化。
1. 为什么要持久化
- Redis 数据在内存中
- 服务器断电/重启数据丢失
- 持久化把数据存到磁盘
- 重启后可以恢复数据
2. RDB 快照
原理: 定时把内存数据全量保存到磁盘。
优点:
- 恢复速度快
- 文件小
- 适合备份
缺点:
- 可能丢失最后一次快照后的数据
- fork子进程可能阻塞
配置:
# redis.conf
save 900 1 # 900秒内1个key变化就保存
save 300 10 # 300秒内10个key变化
save 60 10000 # 60秒内10000个key变化
手动触发:
# 前台保存(阻塞)
redis-cli save
# 后台保存(不阻塞)
redis-cli bgsave
3. AOF 日志
原理: 记录每个写命令,重启时重放。
优点:
- 数据更安全(最多丢1秒)
- 日志可读
缺点:
- 文件比 RDB 大
- 恢复速度慢
配置:
# redis.conf
appendonly yes # 开启AOF
appendfilename "appendonly.aof"
# 同步策略
appendfsync everysec # 每秒同步(推荐)
# appendfsync always # 每次写都同步
# appendfsync no # 交给操作系统
AOF 重写:
# 手动触发AOF重写
redis-cli bgrewriteaof
4. RDB vs AOF 对比
| 对比项 | RDB | AOF |
|---|---|---|
| 数据安全 | 可能丢数据 | 更安全 |
| 文件大小 | 小 | 大 |
| 恢复速度 | 快 | 慢 |
| 性能影响 | fork阻塞 | 较小 |
| 适用场景 | 备份/容灾 | 数据安全要求高 |
5. 混合持久化
Redis 4.0+ 支持混合持久化:
- AOF 重写时前半段用 RDB 格式
- 后半段增量用 AOF 格式
- 结合两者优点
配置:
aof-use-rdb-preamble yes
6. 生产建议
1)数据不敏感用 RDB
- 缓存场景
- 丢点数据没关系
2)数据重要用 AOF
- 每秒同步
- 最多丢1秒数据
3)生产环境推荐
- 同时开 RDB + AOF
- 混合持久化
- 定期备份 RDB 文件
7. 数据恢复
RDB 恢复:
# 把 dump.rdb 放到数据目录
# 重启 Redis 自动加载
AOF 恢复:
# 开启 appendonly yes
# 重启自动重放AOF日志
8. 常见问题
1)fork 阻塞怎么办
- 数据量大 fork 慢
- 用 AOF 代替
- 减少单实例数据量
2)AOF 文件太大
- 定期触发重写
- 设置自动重写阈值
3)恢复慢怎么办
- 用 RDB 恢复快
- 混合持久化更优
总结
Redis 持久化要点:
- RDB 是快照,恢复快但可能丢数据
- AOF 是日志,数据安全但文件大
- 生产建议同时开 RDB + AOF
- 混合持久化结合两者优点
- 缓存用 RDB,重要数据用 AOF
- 定期备份持久化文件
- fork 阻塞要关注数据量
持久化是 Redis 生产必备配置,一定要重视。
PHP获取当前时间不对,时区设置date.timezone
前言
PHP用date()获取当前时间,结果比实际时间差了8个小时?这是最常见的时区问题。PHP默认用UTC时区,北京时间是东八区,所以差了8小时。本文讲怎么改。
一、问题现象
<?php
echo date('Y-m-d H:i:s');
输出:
2026-10-09 00:30:00
但实际北京时间是:
2026-10-09 08:30:00
差了8个小时。
二、原因
PHP默认时区是UTC(世界标准时间),不是北京时间。
北京时间是 UTC+8,所以差了8小时。
三、解决方法
方法1:改php.ini(推荐)
第一步:找到php.ini
php --ini
找到Loaded Configuration File那一行:
Loaded Configuration File: /etc/php.ini
第二步:修改date.timezone
打开php.ini,找到这行:
;date.timezone =
改成:
date.timezone = Asia/Shanghai
第三步:重启php-fpm
systemctl restart php-fpm
方法2:代码里设置(临时)
不想改php.ini,可以在代码里设:
<?php
date_default_timezone_set('Asia/Shanghai');
echo date('Y-m-d H:i:s');
注意: 要在所有日期函数之前设。
方法3:.htaccess里设
Apache的话,可以在.htaccess里设:
php_value date.timezone Asia/Shanghai
四、常用时区对照
| 时区 | 说明 |
|---|---|
| Asia/Shanghai | 北京时间(东八区) |
| Asia/Chongqing | 重庆时间 |
| Asia/Urumqi | 乌鲁木齐时间 |
| Asia/Hong_Kong | 香港时间 |
| Asia/Taipei | 台北时间 |
| Asia/Tokyo | 东京时间(东九区) |
| America/New_York | 纽约时间 |
| Europe/London | 伦敦时间 |
| UTC | 世界标准时间 |
国内用: Asia/Shanghai
五、验证时区
看当前时区
<?php
echo date_default_timezone_get();
// 输出:Asia/Shanghai
看服务器时间
date
看PHP时间
<?php
echo date('Y-m-d H:i:s');
对比一下,应该一样了。
六、常见坑
坑1:CLI和FPM时区不一样
命令行里改了php.ini,但网页上还是不对。
原因: CLI和FPM用不同的php.ini。
解决: 看phpinfo()里的配置路径,改对应的那个。
<?php
phpinfo();
搜"Loaded Configuration File",看用的哪个文件。
坑2:改了没重启
改了php.ini忘了重启php-fpm。
解决:
systemctl restart php-fpm
坑3:代码里又改回去了
某个框架或库里又调了 date_default_timezone_set('UTC')。
解决: 全局搜一下,看哪里改的。
坑4:用了gmdate()
gmdate()总是用UTC时区,不管你设什么。
比如:
echo gmdate('Y-m-d H:i:s');
这个输出的是UTC时间,不是北京时间。
解决: 用date()不要用gmdate(),或者自己加8小时。
坑5:数据库时间不对
PHP时区对了,但MySQL里的时间不对。
原因: MySQL自己也有时区设置。
解决: MySQL连接后设时区:
SET time_zone = '+8:00';
或者改my.cnf:
default-time-zone = '+8:00'
七、常用日期函数
获取当前时间
date_default_timezone_set('Asia/Shanghai');
// 当前日期时间
echo date('Y-m-d H:i:s');
// 今天
echo date('Y-m-d');
// 现在几点
echo date('H:i:s');
// 时间戳
echo time();
时间戳转日期
$time = time();
echo date('Y-m-d H:i:s', $time);
日期转时间戳
$str = '2026-10-09 08:30:00';
echo strtotime($str);
几天以后
// 3天后
echo date('Y-m-d', strtotime('+3 days'));
// 下周
echo date('Y-m-d', strtotime('next week'));
八、框架里的设置
ThinkPHP
在config/app.php里:
'default_timezone' => 'Asia/Shanghai',
Laravel
在config/app.php里:
'timezone' => 'Asia/Shanghai',
总结
记住:
- PHP默认UTC时区,差8小时
- 改php.ini里的
date.timezone = Asia/Shanghai - 改完重启php-fpm
- 或者代码里
date_default_timezone_set('Asia/Shanghai') - CLI和FPM可能用不同的php.ini
最常用:
date_default_timezone_set('Asia/Shanghai');
echo date('Y-m-d H:i:s');
遇到问题加QQ23979811 协助处理
PHP session不生效,cookie配置检查
前言
PHP用session存用户登录状态,结果刷新页面就掉了?session不生效太常见了,一般就是cookie配置不对、路径不对、或者目录没权限。本文讲怎么排查和解决。
一、session工作原理
简单说:
- 浏览器访问网站
- PHP创建session,生成session_id
- session_id通过cookie发给浏览器
- 浏览器每次请求带上这个cookie
- PHP根据cookie里的session_id找到对应数据
核心: session靠cookie传session_id,cookie不行session就没了。
二、常见原因
原因1:cookie没设置对
session.cookie_path、session.cookie_domain配置不对。
原因2:session保存目录没权限
session文件存不进去。
原因3:session.auto_start没开
或者开了但和代码冲突。
原因4:跨域问题
主域和子域之间cookie不共享。
原因5:HTTPS下cookie的secure问题
cookie设了secure,但用http访问。
三、排查步骤
第一步:检查session有没有启动
<?php
session_start();
// 存个值
$_SESSION['test'] = 'hello';
// 看有没有
echo '<pre>';
print_r($_SESSION);
echo '</pre>';
刷新页面,看test还在不在。
第二步:看cookie里有没有PHPSESSID
按F12打开浏览器开发者工具 → Application → Cookies。
看有没有 PHPSESSID 这个cookie。
没有的话: 就是cookie没设对。
有的话: 那就是session保存目录的问题。
四、php.ini配置检查
找到php.ini位置
php --ini
关键配置
;session保存目录
session.save_path = "/var/lib/php/session"
;是否自动启动
session.auto_start = 0
;cookie路径
session.cookie_path = /
;cookie域名
session.cookie_domain =
;cookie有效期(秒)
session.cookie_lifetime = 0
;是否仅https
session.cookie_secure = 0
;是否仅http(防XSS)
session.cookie_httponly = 1
五、常见坑和解决
坑1:session目录没权限
session文件写不进去。
检查:
ls -ld /var/lib/php/session
解决:
chmod 777 /var/lib/php/session
chown apache:apache /var/lib/php/session
或者看php-fpm用的哪个用户。
坑2:cookie_path不对
比如网站装在子目录,但cookie_path设成了别的。
解决:
ini_set('session.cookie_path', '/');
session_start();
坑3:cookie_domain不对
多域名访问时,cookie只在一个域名下有效。
解决:
ini_set('session.cookie_domain', '.example.com');
注意前面有个点,表示所有子域名都生效。
坑4:HTTPS下设了secure
cookie_secure=1表示只在https下传cookie,用http访问就没了。
解决:
session.cookie_secure = 0
或者全站用https。
坑5:session.auto_start冲突
代码里已经session_start()了,但php.ini里也开了auto_start,冲突。
解决:
session.auto_start = 0
六、代码里动态设置
不想改php.ini,可以在代码里设:
<?php
// 必须在session_start()之前
ini_set('session.cookie_path', '/');
ini_set('session.cookie_domain', '.example.com');
ini_set('session.cookie_lifetime', 0);
session_start();
$_SESSION['user_id'] = 1;
七、检查session是否生效
写个测试页:
<?php
session_start();
// 第一次访问,设置
if (!isset($_SESSION['count'])) {
$_SESSION['count'] = 1;
} else {
$_SESSION['count']++;
}
echo "访问次数: " . $_SESSION['count'];
echo "<br>";
echo "Session ID: " . session_id();
echo "<br>";
echo "Session 文件: " . session_save_path() . "/sess_" . session_id();
刷新页面,数字应该一直涨。
如果不涨: 就是session没存住。
八、常见场景
场景1:登录后刷新就掉
原因: cookie_path或domain不对。
解决: 检查cookie配置。
场景2:换了个子域名就掉
比如 www.example.com 登录了,跳到 bbs.example.com 就没登录。
原因: cookie_domain没设成 .example.com。
解决:
ini_set('session.cookie_domain', '.example.com');
场景3:从http跳到https就掉
原因: cookie_secure=1,但http不带cookie。
解决: 全站用https,或者关掉secure。
场景4:session目录满了
原因: session文件太多。
解决: 清理旧session,或者设gc_maxlifetime。
session.gc_maxlifetime = 1440
九、快速排查清单
- F12看cookie里有没有PHPSESSID
- 没有 → 查cookie_path、cookie_domain、cookie_secure
- 有 → 查session目录权限
- 都没问题 → 看session.auto_start有没有冲突
- 跨域问题 → cookie_domain设成 .主域名
总结
记住:
- session靠cookie传session_id
- 先看cookie里有没有PHPSESSID
- 没有就查cookie配置
- 有就查session目录权限
- 跨域名记得设cookie_domain=.主域名
最常用:
ini_set('session.cookie_path', '/');
session_start();
遇到问题加QQ23979811 协助处理
MySQL大表分页越查越慢,优化方案
数据量小的时候分页很快,一旦表数据上百万条,翻到后面的页就慢得要死?这是MySQL分页最常见的问题,下面说几个实用的优化方案。
1. 先看为什么会慢
SELECT * FROM 表名 ORDER BY id LIMIT 100000, 10;
这个SQL的意思是:扫描前100010条,丢掉前100000条,返回后10条。
数据量越大,扫描的行数越多,就越慢。这就是为什么越往后翻越慢。
2. 方案一:子查询优化(最常用)
SELECT * FROM 表名
WHERE id >= (SELECT id FROM 表名 ORDER BY id LIMIT 100000, 1)
LIMIT 10;
原理:
- 子查询先通过索引找到第100001条的id
- 然后直接从这个id开始往后查
- 不需要扫描前10万条
速度对比:
- 原SQL:扫描10万+行,耗时几秒
- 优化后:走索引,几毫秒就出来
3. 方案二:游标分页(推荐用在后台)
-- 第一页
SELECT * FROM 表名 ORDER BY id LIMIT 10;
-- 第二页(记住上一页最后一条的id)
SELECT * FROM 表名 WHERE id > 100 ORDER BY id LIMIT 10;
-- 第三页
SELECT * FROM 表名 WHERE id > 110 ORDER BY id LIMIT 10;
优点:
- 永远都是走索引,不管翻到第几页都一样快
- 适合无限滚动加载
缺点:
- 不能直接跳到第100页
- 不适合传统的"上一页/下一页"翻页
4. 方案三:延迟关联
SELECT t.* FROM 表名 t
INNER JOIN (
SELECT id FROM 表名 ORDER BY id LIMIT 100000, 10
) tmp ON t.id = tmp.id;
原理:
- 先通过索引查出需要的10个id
- 再用这10个id去关联原表查完整数据
- 避免回表太多行
5. 方案四:限制最大翻页深度
如果不是必须让用户翻到很后面,可以直接限制:
-- 最多只允许翻100页(1000条数据)
IF 页码 > 100 THEN
-- 提示用户"请搜索更精确的条件"
END IF;
一般用户也不会翻到很后面的页,限制一下性能就好很多。
6. 方案五:用ES做搜索
如果是文章、商品之类的内容,直接用Elasticsearch做全文搜索和分页,MySQL只存原始数据。
ES天生适合做大数据量的分页搜索,MySQL不是干这个的。
常见坑
坑1:ORDER BY 字段没加索引
ORDER BY 的字段一定要加索引,不然不管怎么优化都是全表扫描。
坑2:OFFSET 太大了
OFFSET 100万,不管怎么优化都慢,直接用游标分页。
*坑3:SELECT 不要用**
只查需要的字段,减少回表数据量。
遇到问题加QQ23979811 协助处理
MySQL数据库导入sql文件报错解决
导入sql文件的时候各种报错?下面整理了最常见的导入报错和解决办法,照着排查就行。
1. 先看最常见的导入命令
mysql -u root -p 数据库名 < /path/to/你的文件.sql
或者进入MySQL后用source:
mysql -u root -p
USE 数据库名;
source /path/to/你的文件.sql;
2. 报错1:Unknown database
原因: 数据库不存在,你直接导入了。
解决: 先创建数据库再导入:
CREATE DATABASE 数据库名 DEFAULT CHARACTER SET utf8mb4;
然后再执行导入命令。
3. 报错2:Access denied
原因: 权限不够,或者密码错了。
解决: 确认用户名密码对不对,给用户授权:
GRANT ALL PRIVILEGES ON *.* TO '用户名'@'localhost' IDENTIFIED BY '密码';
FLUSH PRIVILEGES;
4. 报错3:File not found
原因: 文件路径不对,或者文件没权限读。
解决:
- 用绝对路径,不要用相对路径
- 确认文件存在:
ls -l /path/to/file.sql - 给文件加读权限:
chmod 644 file.sql
5. 报错4:Duplicate entry
原因: 导入的表已经有数据了,主键冲突。
解决:
- 先清空表再导入:
TRUNCATE TABLE 表名; - 或者导入SQL文件开头加上
DROP TABLE IF EXISTS 表名;
6. 报错5:Unknown collation
原因: 字符集排序规则不一致,比如导出用的是 utf8mb4_general_ci,导入的数据库是 utf8_general_ci。
解决: 导入前设置字符集:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- 导入你的SQL
SET FOREIGN_KEY_CHECKS = 1;
7. 报错6:MySQL server has gone away
原因: SQL文件太大,导入超时了。
解决: 修改MySQL配置:
# /etc/my.cnf
max_allowed_packet = 64M
wait_timeout = 300
interactive_timeout = 300
然后重启MySQL:
systemctl restart mysqld
8. 报错7:Syntax error
原因: SQL文件格式不对,或者MySQL版本不兼容。
解决:
- 导出的时候选择兼容版本
- 去掉注释行(-- 开头的)
- 用文本编辑器打开检查一下有没有乱码
9. 大文件导入更快的方法
如果SQL文件几百MB,用mysql命令行比phpMyAdmin快10倍:
mysql -u root -p 数据库名 < big_file.sql
还可以开事务加速:
SET autocommit = 0;
SET unique_checks = 0;
SET foreign_key_checks = 0;
-- 导入完成后
SET autocommit = 1;
SET unique_checks = 1;
SET foreign_key_checks = 1;
COMMIT;
遇到问题加QQ23979811 协助处理
PHP 过滤和验证数据
PHP 过滤和验证数据
用户输入不可信,必须过滤验证,这是Web开发的基本功。
1. 为什么要过滤验证
- 防止 SQL 注入
- 防止 XSS 攻击
- 保证数据格式正确
- 防止脏数据入库
2. filter_var 函数
PHP 内置过滤器,很好用。
验证邮箱
$email = 'test@example.com';
if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo '邮箱格式正确';
} else {
echo '邮箱格式错误';
}
验证数字
$age = '20abc';
if (filter_var($age, FILTER_VALIDATE_INT)) {
echo '是整数';
}
验证URL
$url = 'https://example.com';
if (filter_var($url, FILTER_VALIDATE_URL)) {
echo 'URL格式正确';
}
验证IP
$ip = '192.168.1.1';
if (filter_var($ip, FILTER_VALIDATE_IP)) {
echo 'IP格式正确';
}
3. filter_input 获取并过滤
从 GET/POST 获取并过滤:
// 获取并过滤GET参数
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
// 获取并过滤POST参数
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
4. 过滤输入
过滤HTML标签
$str = '<script>alert(1)</script>你好';
// 去除HTML标签
echo strip_tags($str);
// HTML转义(安全输出)
echo htmlspecialchars($str);
过滤特殊字符
$str = "hello ' world";
// 转义单引号
echo addslashes($str);
5. 常用验证场景
验证手机号
function checkPhone($phone) {
return preg_match('/^1[3-9]\d{9}$/', $phone);
}
if (checkPhone('13800138000')) {
echo '手机号正确';
}
验证身份证
function checkIdCard($idcard) {
return preg_match('/^\d{17}[\dXx]$/', $idcard);
}
验证用户名
// 4-16位,字母数字下划线
function checkUsername($name) {
return preg_match('/^\w{4,16}$/', $name);
}
6. 安全建议
1)所有外部输入都要验证
- GET、POST、COOKIE
- 不要相信任何用户输入
2)用预处理防SQL注入
- 不要自己拼接SQL
- 用 PDO 预处理
3)输出时转义
- 输出到HTML要 htmlspecialchars
- 防止 XSS 攻击
4)白名单验证
- 只接受允许的值
- 不要用黑名单
7. 完整示例
// 接收用户注册信息
$username = trim($_POST['username']);
$email = trim($_POST['email']);
$age = (int)$_POST['age'];
$errors = [];
// 验证用户名
if (!preg_match('/^\w{4,16}$/', $username)) {
$errors[] = '用户名4-16位字母数字下划线';
}
// 验证邮箱
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = '邮箱格式错误';
}
// 验证年龄
if ($age < 18 || $age > 100) {
$errors[] = '年龄必须18-100岁';
}
if (empty($errors)) {
// 验证通过,入库
echo '注册成功';
} else {
print_r($errors);
}
总结
PHP 过滤和验证数据要点:
- 所有用户输入都不可信
- 用 filter_var 验证格式
- 用 filter_input 获取并过滤
- 输出用 htmlspecialchars 防XSS
- 用预处理防SQL注入
- 用正则验证手机号、用户名
- 白名单验证比黑名单好
- 不要自己拼接SQL字符串
做Web开发,安全永远第一位。
MySQL 主键和自增
MySQL 主键和自增
主键是表的重要概念,搞懂怎么用很有必要。
1. 什么是主键
主键(Primary Key)是表中唯一标识一行记录的字段。
特点:
- 唯一性: 每行数据主键不能重复
- 非空性: 主键不能为 NULL
- 一张表只能有一个主键
2. 创建主键
建表时指定
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50)
);
自增主键(最常用)
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);
3. 自增主键特点
- 自动增长: 插入数据不用指定ID,自动+1
- 从1开始: 默认从1开始
- 不重复: 保证唯一
-- 插入数据不用写id
INSERT INTO users (name) VALUES ('张三');
-- id自动变成1
-- 再插入自动变成2
4. 查看自增值
-- 查看表当前自增值
SHOW TABLE STATUS LIKE 'users';
-- 查看建表语句
SHOW CREATE TABLE users;
5. 修改自增值
-- 设置下一次自增值为100
ALTER TABLE users AUTO_INCREMENT = 100;
6. 联合主键
多个字段组成主键:
CREATE TABLE user_role (
user_id INT,
role_id INT,
PRIMARY KEY (user_id, role_id)
);
7. 主键设计建议
1)用自增整数做主键
- INT 或 BIGINT
- 查询快,占用空间小
- 不要用 UUID 做主键
2)为什么不用字符串做主键
- 占空间大
- 查询慢
- 自增效率高
3)什么时候用联合主键
- 中间表(如用户角色关联表)
- 没有合适的单字段做主键
4)BIGINT 还是 INT
- 一般 INT 够用(约21亿)
- 数据量大用 BIGINT
8. 常见问题
1)删除数据后ID不连续
- 正常现象,自增只增不减
- 不要依赖ID连续
2)自增值跳号
- 删除了中间数据
- 插入失败也会占用ID
- 正常现象
3)为什么不用手机号做主键
- 可能会变
- 字符串查询慢
- 用自增ID更稳妥
总结
MySQL 主键和自增要点:
- 主键唯一且非空
- 一张表只能一个主键
- 自增主键最常用
- 用 INT 或 BIGINT
- 不要用 UUID 做主键
- 中间表可以用联合主键
- ID不连续是正常现象
主键设计对表性能影响很大,一定要重视。
Nginx 访问日志配置
Nginx 访问日志配置
访问日志是排查问题和分析流量的重要依据,配置好很有必要。
1. 默认日志位置
# 访问日志
/var/log/nginx/access.log
# 错误日志
/var/log/nginx/error.log
2. 日志格式
Nginx 默认有两种日志格式:
combined(默认)
log_format combined '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
common
log_format common '$remote_addr [$time_local] "$request" $status $body_bytes_sent';
3. 自定义日志格式
在 nginx.conf 的 http 块添加:
http {
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time $upstream_response_time';
access_log /var/log/nginx/access.log main;
}
4. 常用日志变量
- $remote_addr: 客户端IP
- $time_local: 本地时间
- $request: 请求行
- $status: 状态码
- $body_bytes_sent: 发送字节数
- $http_referer: 来源页面
- $http_user_agent: 浏览器信息
- $request_time: 请求总耗时
- $upstream_response_time: 后端响应时间
5. 按站点分开日志
不同站点用不同日志文件:
server {
listen 80;
server_name a.com;
access_log /var/log/nginx/a.access.log;
}
server {
listen 80;
server_name b.com;
access_log /var/log/nginx/b.access.log;
}
6. 关闭日志
静态资源不记日志:
location ~* \.(jpg|jpeg|png|gif|css|js)$ {
access_log off;
}
7. 日志切割
Nginx 自带日志切割:
http {
# 每天切割,保留30天
access_log /var/log/nginx/access.log main;
log_not_found off;
}
也可以用 logrotate:
# 配置文件
/etc/logrotate.d/nginx
8. 分析日志
统计访问量
# 统计总请求数
wc -l /var/log/nginx/access.log
# 统计独立IP
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn
统计状态码
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c
统计访问最多的URL
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
总结
Nginx 访问日志配置要点:
- 默认日志在 /var/log/nginx/
- combined 是默认格式
- 自定义格式添加 $request_time 等变量
- 不同站点分开日志文件
- 静态资源可以关闭日志
- 日志要定期切割
- 用 awk 分析日志很方便
做好日志配置,排查问题和分析流量都有用。