Nginx配置HTTPS证书,http自动跳转https
前言
现在网站都要上 HTTPS,搜索引擎也更喜欢 HTTPS 的网站。本文讲一下 Nginx 怎么配置 HTTPS 证书,以及怎么把 HTTP 请求自动跳转到 HTTPS。
第一步:申请 SSL 证书
免费证书(Let's Encrypt)
用 certbot 自动申请:
# 安装 certbot
yum install certbot python3-certbot-nginx -y
# 自动申请并配置
certbot --nginx -d www.example.com -d example.com
certbot 会自动修改 Nginx 配置,不用手动改。
自己上传证书
如果是从阿里云、腾讯云申请的证书,下载 Nginx 版本的,一般有两个文件:
xxx.crt(证书文件)xxx.key(私钥文件)
上传到服务器:
mkdir -p /etc/nginx/ssl
把两个文件传到 /etc/nginx/ssl/ 目录下。
第二步:Nginx 配置 HTTPS
编辑 Nginx 配置文件:
vi /etc/nginx/conf.d/example.conf
配置内容:
server {
listen 443 ssl http2;
server_name www.example.com example.com;
# 证书路径
ssl_certificate /etc/nginx/ssl/xxx.crt;
ssl_certificate_key /etc/nginx/ssl/xxx.key;
# SSL 优化配置
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# 网站根目录
root /var/www/html;
index index.html index.php;
location / {
try_files $uri $uri/ =404;
}
}
第三步:HTTP 自动跳转 HTTPS
在同一个配置文件里,加一个 80 端口的 server:
# HTTP 跳转 HTTPS
server {
listen 80;
server_name www.example.com example.com;
return 301 https://$host$request_uri;
}
这样访问 http://www.example.com 就会自动跳转到 https://www.example.com。
第四步:检查配置并重载
检查 Nginx 配置有没有语法错误:
nginx -t
没问题就重载:
nginx -s reload
完整配置示例
# HTTP 跳转 HTTPS
server {
listen 80;
server_name www.example.com example.com;
return 301 https://$host$request_uri;
}
# HTTPS 配置
server {
listen 443 ssl http2;
server_name www.example.com example.com;
ssl_certificate /etc/nginx/ssl/xxx.crt;
ssl_certificate_key /etc/nginx/ssl/xxx.key;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /var/www/html;
index index.html index.php;
location / {
try_files $uri $uri/ =404;
}
}
常见坑
坑1:证书文件路径不对
配置里写的证书路径和实际路径不一致,Nginx 启动报错。
解决: 确认证书文件确实在配置写的那个位置。
坑2:443 端口没开放
配置了 HTTPS 但访问不了。
原因: 防火墙没开 443 端口。
解决:
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
坑3:跳转死循环
HTTP 跳 HTTPS 后又跳回 HTTP,无限循环。
原因: 用了 CDN 或者负载均衡,后端以为还是 HTTP。
解决: Nginx 配置里加:
set $flag 0;
if ($http_x_forwarded_proto = 'http') {
set $flag 1;
}
if ($flag = 1) {
return 301 https://$host$request_uri;
}
坑4:证书过期了
HTTPS 证书一般一年有效期,忘了续签就会过期。
解决: 用 certbot 的话自动续签:
certbot renew
加个定时任务自动续签:
0 3 1 * * certbot renew --quiet
总结
| 步骤 | 命令/操作 |
|---|---|
| 申请证书 | certbot --nginx -d 域名 |
| 配置 HTTPS | 写 443 端口的 server 块 |
| HTTP 跳转 | return 301 https://$host$request_uri |
| 检查配置 | nginx -t |
| 重载 | nginx -s reload |
| 开防火墙 | firewall-cmd --permanent --add-service=https |
提醒: HTTPS 证书要定期续签,别等过期了才想起。
遇到问题加QQ23979811 协助处理
Nginx报502 Bad Gateway完整排查思路(PHP-FPM场景)
前言
502 Bad Gateway 是 Nginx 最常见的报错之一,尤其是 LNMP 架构下,Nginx 把 PHP 请求转发给 PHP-FPM,PHP-FPM 出问题了 Nginx 就报 502。本文讲一下完整的排查思路。
502 是什么意思
502 表示 Nginx 作为反向代理,去请求后端服务(PHP-FPM)的时候,后端没响应或者报错了。
注意: 502 不是 Nginx 的问题,是后端 PHP-FPM 的问题。
排查步骤
第一步:检查 PHP-FPM 服务是否在运行
systemctl status php-fpm
如果显示 inactive (dead),说明 PHP-FPM 挂了,启动它:
systemctl start php-fpm
systemctl enable php-fpm
第二步:检查 Nginx 和 PHP-FPM 的通信配置
看 Nginx 配置里的 fastcgi_pass 对不对:
grep fastcgi_pass /etc/nginx/nginx.conf
常见的配置:
# 用 socket 文件
fastcgi_pass unix:/run/php-fpm/www.sock;
# 用 TCP 端口
fastcgi_pass 127.0.0.1:9000;
问题1:socket 文件路径不对
Nginx 配置里写的 socket 路径和 PHP-FPM 实际的不一致。
PHP-FPM 的 socket 路径在 /etc/php-fpm.d/www.conf 里:
grep listen /etc/php-fpm.d/www.conf
确保两边路径一致。
问题2:端口被占
如果用 9000 端口,检查一下是不是被别的进程占了:
lsof -i:9000
第三步:看错误日志
Nginx 错误日志:
tail -f /var/log/nginx/error.log
PHP-FPM 错误日志:
tail -f /var/log/php-fpm/error.log
PHP 慢日志:
tail -f /var/log/php-fpm/www-slow.log
根据日志里的报错信息找原因。
第四步:常见原因
原因1:PHP-FPM 进程数不够
访问量大的时候,PHP-FPM 进程都被占满了,新请求排队超时。
解决: 增加 PHP-FPM 进程数。
编辑 /etc/php-fpm.d/www.conf:
pm.max_children = 50
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
改完重启:
systemctl restart php-fpm
原因2:PHP 脚本执行超时
PHP 脚本执行时间太长,超过了 Nginx 的超时时间。
解决: 调大超时时间。
Nginx 配置里:
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
PHP 配置里:
max_execution_time = 300
原因3:PHP 内存不足
PHP-FPM 进程内存溢出,导致进程崩溃。
解决: 调大 PHP 内存限制:
memory_limit = 256M
或者减少 PHP-FPM 进程数,避免内存不够。
原因4:文件权限不对
Nginx 运行的用户和 PHP-FPM 运行的用户不一致,导致 PHP-FPM 读不到文件。
解决: 确保 Nginx 和 PHP-FPM 用同一个用户(一般是 www)。
PHP-FPM 配置 /etc/php-fpm.d/www.conf:
user = www
group = www
然后把网站文件的属主改成 www:
chown -R www:www /var/www/html
第五步:重启所有服务
改完配置后重启:
systemctl restart php-fpm
systemctl restart nginx
常见坑
坑1:只重启了 Nginx
改了 PHP 配置但只重启了 Nginx,PHP-FPM 还是老配置。
解决: PHP 配置改了要重启 PHP-FPM,Nginx 配置改了要重启 Nginx。
坑2:socket 文件权限不对
PHP-FPM 的 socket 文件权限是 600,Nginx 用户读不到。
解决: 改 socket 文件权限:
listen.mode = 0666
或者改 socket 文件的属主:
listen.owner = www
listen.group = www
坑3:PHP-FPM 挂了但没自动重启
PHP-FPM 进程崩溃了,没人自动拉起来。
解决: 配置 systemd 自动重启:
systemctl edit php-fpm
加:
[Service]
Restart=always
RestartSec=2
总结
502 排查步骤:
- 先看 PHP-FPM 是不是挂了 →
systemctl status php-fpm - 再看 Nginx 配置对不对 → fastcgi_pass 路径/端口
- 看错误日志 → Nginx error.log + PHP-FPM error.log
- 常见原因 → 进程数不够、超时太短、权限不对、内存不足
记住:502 问题出在后端,不是 Nginx 的锅。
遇到问题加QQ23979811 协助处理
Linux查看系统运行级别,multi-user.target详解
前言
经常听到别人说"运行级别 3"、"运行级别 5",这是什么意思?CentOS7 之后用 systemd 代替了老的 SysVinit,运行级别也变成了 target。本文讲一下怎么查看和切换系统运行级别。
老的运行级别(SysVinit)
老系统(CentOS6 之前)用数字表示运行级别:
| 级别 | 含义 |
|---|---|
| 0 | 关机 |
| 1 | 单用户模式(救援模式) |
| 2 | 多用户模式(无 NFS) |
| 3 | 完全多用户模式(命令行,服务器常用) |
| 4 | 未使用 |
| 5 | 图形界面模式 |
| 6 | 重启 |
新的 target(systemd)
CentOS7 之后用 target 代替了数字运行级别:
| target | 对应老级别 | 含义 |
|---|---|---|
| poweroff.target | 0 | 关机 |
| rescue.target | 1 | 救援模式 |
| multi-user.target | 3 | 命令行模式(服务器常用) |
| graphical.target | 5 | 图形界面模式 |
| reboot.target | 6 | 重启 |
查看当前运行级别
systemctl 命令
systemctl get-default
输出:
multi-user.target
就是当前默认的运行级别是命令行模式。
老命令(还能用)
runlevel
输出类似:
N 3
第一个是上次运行级别(N 表示没有),第二个是当前运行级别。
切换运行级别
临时切换(重启后恢复)
systemctl isolate 目标target
比如从图形界面切到命令行:
systemctl isolate multi-user.target
或者从命令行切到图形界面:
systemctl isolate graphical.target
永久切换(重启后生效)
systemctl set-default 目标target
比如默认启动到命令行模式:
systemctl set-default multi-user.target
默认启动到图形界面:
systemctl set-default graphical.target
常用操作
进入救援模式
系统出问题了进不去?进救援模式:
systemctl rescue
进入紧急模式
更底层的救援模式:
systemctl emergency
关机
systemctl poweroff
或者:
systemctl halt
重启
systemctl reboot
常见坑
坑1:图形界面切命令行后黑屏
执行了 isolate multi-user.target,屏幕黑了。
原因: 这是正常的,切到命令行模式就是黑屏,只有终端提示符。
解决: 输入用户名密码登录就行。
坑2:设置了 multi-user.target 但还是进图形界面
执行了 set-default multi-user.target,但重启后还是进图形界面。
原因: 可能有多个桌面管理器在抢着启动。
解决: 确认一下:
systemctl get-default
应该显示 multi-user.target。如果不对,再执行一次。
坑3:把运行级别设成 0 或 6 了
不小心设成了 poweroff.target 或 reboot.target。
后果: 一开就关机或者一开就重启,无限循环。
解决: 进救援模式改回来。
总结
| 命令 | 作用 |
|---|---|
systemctl get-default |
查看当前默认运行级别 |
systemctl set-default multi-user.target |
设置为命令行模式 |
systemctl set-default graphical.target |
设置为图形界面模式 |
systemctl isolate multi-user.target |
临时切到命令行 |
systemctl isolate graphical.target |
临时切到图形界面 |
runlevel |
老命令查看运行级别 |
服务器一般用 multi-user.target(命令行模式),更省资源、更稳定。
遇到问题加QQ23979811 协助处理
CentOS7 安装图形界面,最小化安装升级
前言
很多人装 CentOS7 的时候选了最小化安装,后来发现想用图形界面怎么办?不用重装,直接在线装就行。本文讲一下怎么从命令行升级到带图形界面的版本。
先更新系统
装图形界面前先把系统更新一下:
yum update -y
安装 GNOME 桌面
方法一:安装 GNOME 桌面(推荐)
yum groupinstall -y "GNOME Desktop"
这个安装包比较大,大概几个 G,需要等一会儿。
方法二:安装带图形界面的服务器版
yum groupinstall -y "Server with GUI"
这个比 GNOME Desktop 多一些服务器相关的工具。
设置默认启动方式
查看当前默认启动方式
systemctl get-default
输出 multi-user.target 就是命令行模式,graphical.target 就是图形界面模式。
设置默认启动到图形界面
systemctl set-default graphical.target
设置默认启动到命令行
如果以后想改回命令行:
systemctl set-default multi-user.target
重启生效
reboot
重启之后就会进入图形界面了。
不重启直接进入图形界面
如果不想重启,可以直接启动图形界面:
startx
或者:
systemctl isolate graphical.target
常见坑
坑1:yum 安装报错
安装的时候报错,下载失败。
原因: yum 源没配好。
解决: 先换国内 yum 源(参考之前的 yum 源更换教程),然后再装。
坑2:安装完了还是命令行
装完重启后还是进命令行。
原因: 没设置默认启动方式。
解决:
systemctl set-default graphical.target
reboot
坑3:服务器装图形界面很卡
云服务器上装了图形界面之后特别卡。
原因: 图形界面占内存和 CPU 很多,云服务器配置一般不高。
建议: 生产服务器不要装图形界面,命令行更省资源。如果确实需要图形界面,用 VNC 或者远程桌面连。
坑4:安装包太大下载慢
几个 G 的包下载半天。
解决: 先换国内 yum 源,速度会快很多。
总结
| 步骤 | 命令 |
|---|---|
| 更新系统 | yum update -y |
| 安装 GNOME 桌面 | yum groupinstall -y "GNOME Desktop" |
| 设置默认图形界面 | systemctl set-default graphical.target |
| 重启 | reboot |
提醒: 生产服务器建议用命令行模式,更稳定、更省资源。图形界面只在测试或者个人电脑上用。
遇到问题加QQ23979811 协助处理
Linux查看端口被哪个进程占用,lsof命令
前言
启动服务的时候提示 "Address already in use",端口被占了,但不知道被哪个进程占了?这时候就需要用 lsof 或者 netstat 命令来查一下。本文讲一下怎么用。
lsof 命令
安装
CentOS 最小化安装可能没有 lsof:
yum install -y lsof
查看某个端口被哪个进程占用
lsof -i:端口号
比如看 80 端口被谁占了:
lsof -i:80
输出类似:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 1234 root 6u IPv4 12345 0t0 TCP *:http (LISTEN)
重点看:
PID:进程号COMMAND:进程名
知道了 PID 之后,想杀掉就用 kill PID。
查看所有正在监听的端口
lsof -i -P -n | grep LISTEN
输出所有处于监听状态的端口和对应的进程。
netstat 命令
安装
来自 net-tools 包:
yum install -y net-tools
查看端口被哪个进程占用
netstat -tlnp | grep 端口号
比如看 80 端口:
netstat -tlnp | grep :80
输出类似:
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1234/nginx
最后一列就是 PID/进程名。
参数解释
| 参数 | 含义 |
|---|---|
-t |
显示 TCP 连接 |
-l |
只显示监听状态的 |
-n |
显示数字,不解析域名 |
-p |
显示进程名和 PID |
查看所有监听端口
netstat -tlnp
fuser 命令
查看端口被哪个进程占用
fuser 端口号/tcp
比如:
fuser 80/tcp
输出:
80/tcp: 1234
直接显示 PID,简单粗暴。
杀掉占用端口的进程
fuser -k 80/tcp
直接把占用 80 端口的进程杀掉。
常见场景
场景1:启动 Nginx 提示端口被占
nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
查一下:
lsof -i:80
发现是旧的 nginx 进程没杀掉,直接杀:
kill 1234
或者:
fuser -k 80/tcp
场景2:3306 端口不知道被谁占了
lsof -i:3306
发现是 MySQL 进程,正常的。
场景3:找出所有被占用的端口
netstat -tlnp
列出所有正在监听的端口。
常见坑
坑1:lsof 命令找不到
执行 lsof 提示 command not found。
原因: 没装 lsof 包。
解决:
yum install -y lsof
坑2:普通用户看不到其他用户的进程
用普通用户执行 lsof,看不到 root 的进程。
原因: 权限不够。
解决: 加 sudo 或者切 root:
sudo lsof -i:80
坑3:端口明明没被占但提示被占
服务明明没启动,但启动时提示端口被占。
原因: 可能有 TIME_WAIT 状态的连接还没释放。
解决: 等一会儿再试,或者改端口。
坑4:杀了进程端口还是没释放
杀掉进程了,但端口还是显示被占。
原因: 进程还在退出中,等几秒就好了。
总结
| 命令 | 作用 |
|---|---|
lsof -i:端口号 |
查看端口被哪个进程占用 |
netstat -tlnp \| grep :端口号 |
用 netstat 查端口 |
fuser 端口号/tcp |
查看端口对应的 PID |
fuser -k 端口号/tcp |
杀掉占用端口的进程 |
最常用的就是 lsof -i:端口号,一眼就能看到进程名和 PID。
遇到问题加QQ23979811 协助处理
Linux上传下载文件,rz和sz命令使用
前言
经常需要从本地电脑上传文件到服务器,或者把服务器上的文件下载到本地。除了用 FTP 工具,还有更简单的 rz 和 sz 命令,直接在终端里就能操作。本文讲一下怎么用。
安装 lrzsz
rz 和 sz 命令来自 lrzsz 包,CentOS 最小化安装可能没有:
yum install -y lrzsz
Ubuntu 的话:
apt install -y lrzsz
rz 上传文件(从本地上传到服务器)
基本用法
直接输入:
rz
执行后会弹出一个文件选择窗口,选你要上传的文件,点确定就开始上传了。
注意: 文件会上传到你当前所在的目录。
指定文件名
上传后直接改成指定名字:
rz -y 新文件名
-y 是覆盖同名文件。
上传多个文件
执行 rz 之后在文件选择窗口可以按住 Ctrl 多选。
sz 下载文件(从服务器下载到本地)
下载单个文件
sz 文件名
比如下载 index.html:
sz index.html
执行后会弹出保存窗口,选保存位置就开始下载了。
下载多个文件
sz 文件1 文件2 文件3
比如:
sz a.txt b.txt c.txt
下载整个目录(先打包)
sz 不能直接下载目录,要先打包:
tar -zcvf backup.tar.gz /var/www/html
sz backup.tar.gz
常见使用场景
场景1:上传配置文件
# 先进入要上传的目录
cd /etc/nginx/conf.d/
# 上传
rz
选本地的 nginx 配置文件,上传完就直接放到这个目录了。
场景2:下载日志文件
# 进入日志目录
cd /var/log/nginx/
# 下载访问日志
sz access.log
场景3:上传压缩包
本地把项目打包成 zip,然后:
cd /var/www/html/
rz
选本地的项目 zip 文件,上传完再解压。
常见坑
坑1:rz 命令找不到
执行 rz 提示 command not found。
原因: 没装 lrzsz 包。
解决:
yum install -y lrzsz
坑2:上传的文件不知道去哪了
执行了 rz,但不知道文件上传到哪了。
原因: 文件上传到你当前所在的目录。
解决: 上传前先 pwd 看一下当前目录在哪。
坑3:用了某些 SSH 工具不支持 rz/sz
有些 SSH 客户端(比如 Windows 自带的 cmd)不支持 rz/sz。
原因: rz/sz 需要终端支持 ZModem 协议。
解决: 用支持的 SSH 工具,比如 Xshell、SecureCRT、FinalShell、MobaXterm。
坑4:大文件上传慢
上传几百 M 的文件特别慢。
原因: rz/sz 走的是 SSH 通道,速度不快。
解决: 大文件用 SFTP 或者专门的传输工具(FileZilla)更快。
坑5:上传的文件是乱码
上传完中文文件名变成乱码。
原因: 编码问题。
解决: 用英文文件名,或者先打包成 zip 再上传。
总结
| 命令 | 作用 |
|---|---|
rz |
从本地上传文件到服务器 |
sz 文件名 |
从服务器下载文件到本地 |
前提: 要先装 lrzsz 包,而且 SSH 工具要支持 ZModem 协议。
小文件用 rz/sz 很方便,大文件还是用 SFTP 吧。
遇到问题加QQ23979811 协助处理
CentOS7 关闭防火墙,永久关闭firewalld
前言
刚装好的 CentOS7 默认开了防火墙,很多端口都访问不了。有时候为了省事想直接关掉防火墙,本文讲一下怎么操作,以及临时关和永久关的区别。
查看防火墙状态
先看看防火墙现在是开还是关:
systemctl status firewalld
输出里有 active (running) 就是开着,inactive (dead) 就是关了。
或者用:
firewall-cmd --state
输出 running 就是开着,not running 就是关了。
临时关闭防火墙
只关掉当前运行状态,重启后又会自动开起来:
systemctl stop firewalld
永久关闭防火墙
永久关闭,重启后也不会再开:
systemctl stop firewalld
systemctl disable firewalld
两条命令一起执行,先停掉当前的,再禁止开机自启。
重新开启防火墙
如果以后又想开起来:
systemctl start firewalld
systemctl enable firewalld
验证是否关闭成功
执行完之后再看一下状态:
systemctl status firewalld
应该显示 inactive (dead)。
常见坑
坑1:只 stop 了但没 disable
执行了 systemctl stop firewalld,但重启后防火墙又自己开起来了。
原因: 只停了当前运行,没关开机自启。
解决: 还要执行 systemctl disable firewalld。
坑2:关了防火墙但还是访问不了
防火墙关了,但外部还是访问不了网站。
原因: 除了系统防火墙,云服务商还有一层安全组。
解决: 去云服务商控制台(阿里云、腾讯云、华为云等)的安全组里,把对应的端口也开放了。
坑3:Ubuntu 用的不是 firewalld
在 Ubuntu 上执行 firewalld 命令报错。
原因: Ubuntu 默认用 ufw 防火墙,不是 firewalld。
解决: Ubuntu 关防火墙用:
ufw stop
ufw disable
坑4:关闭防火墙不安全
直接关了防火墙,服务器暴露在公网上很危险。
建议: 生产环境不要直接关防火墙,而是只开放需要的端口。比如只开 80、443、22 端口,其他都关。
总结
| 操作 | 命令 |
|---|---|
| 查看防火墙状态 | systemctl status firewalld |
| 临时关闭 | systemctl stop firewalld |
| 永久关闭 | systemctl stop firewalld && systemctl disable firewalld |
| 开启防火墙 | systemctl start firewalld |
| 开机自启 | systemctl enable firewalld |
注意: 生产环境建议不要直接关防火墙,用开放指定端口的方式更安全。
遇到问题加QQ23979811 协助处理
Linux查看所有服务,systemctl命令大全
前言
CentOS7 之后用 systemctl 管理服务,代替了老的 service 和 chkconfig 命令。本文把常用的 systemctl 命令都整理出来,新手照着用就行。
查看服务
查看所有服务
systemctl list-units --type=service
列出所有正在运行的服务。
查看所有服务(包括没运行的)
systemctl list-unit-files --type=service
查看正在运行的服务
systemctl list-units --type=service --state=running
查看某个服务状态
systemctl status 服务名
比如看 nginx 状态:
systemctl status nginx
输出会显示服务是否在运行、PID、内存占用、最近日志等。
启动和停止服务
启动服务
systemctl start 服务名
比如:
systemctl start nginx
停止服务
systemctl stop 服务名
比如:
systemctl stop nginx
重启服务
systemctl restart 服务名
比如:
systemctl restart nginx
重载配置(不重启)
修改了配置文件后,用 reload 重载,不用重启:
systemctl reload 服务名
比如:
systemctl reload nginx
平滑重启(reload 不行就用这个)
systemctl reload-or-restart 服务名
设置开机自启
开启开机自启
systemctl enable 服务名
比如:
systemctl enable nginx
关闭开机自启
systemctl disable 服务名
比如:
systemctl disable nginx
查看是否开机自启
systemctl is-enabled 服务名
输出 enabled 就是开机自启,disabled 就是不是。
其他常用操作
查看服务是否在运行
systemctl is-active 服务名
输出 active 就是在运行,inactive 就是没运行。
查看服务日志
journalctl -u 服务名
比如看 nginx 日志:
journalctl -u nginx
实时看服务日志
journalctl -u 服务名 -f
和 tail -f 一样,实时输出最新日志。
查看服务开机自启状态
systemctl list-unit-files --type=service | grep enabled
列出所有开机自启的服务。
常见服务名
| 服务名 | 对应服务 |
|---|---|
| nginx | Nginx 网页服务 |
| httpd | Apache 网页服务 |
| mysqld | MySQL 数据库 |
| firewalld | 防火墙 |
| crond | 定时任务 |
| sshd | SSH 远程连接 |
| php-fpm | PHP 解析 |
| redis | Redis 缓存 |
常见坑
坑1:服务名写错了
执行 systemctl 报错 Unit xxx.service could not be found。
原因: 服务名写错了。
解决: 先列一下所有服务,找到正确的服务名:
systemctl list-unit-files --type=service | grep 关键词
坑2:启动了服务但还是访问不了
服务显示 active,但外部还是访问不了。
原因: 防火墙没开端口,或者服务本身配置错了。
解决: 先看防火墙开了没,再看服务日志:
journalctl -u 服务名
坑3:开机自启了但重启后没起来
设置了 enable,但重启后服务还是没启动。
原因: 服务配置有问题,启动失败了。
解决: 看日志找原因:
journalctl -u 服务名
坑4:用了老命令 service
执行 service nginx start 能用,但不推荐了。
原因: 这是老命令,新系统都用 systemctl。
解决: 换成 systemctl start nginx。
总结
| 操作 | 命令 |
|---|---|
| 查看所有服务 | systemctl list-units --type=service |
| 查看服务状态 | systemctl status 服务名 |
| 启动服务 | systemctl start 服务名 |
| 停止服务 | systemctl stop 服务名 |
| 重启服务 | systemctl restart 服务名 |
| 重载配置 | systemctl reload 服务名 |
| 开机自启 | systemctl enable 服务名 |
| 关闭开机自启 | systemctl disable 服务名 |
| 查看日志 | journalctl -u 服务名 |
systemctl 就这几个常用命令,记住就够用了。
遇到问题加QQ23979811 协助处理
Linux怎么修改主机名,永久生效方法
前言
新装好的服务器默认主机名是 localhost 或者一串乱七八糟的字符,不好区分。怎么改成自己想要的名字,而且重启后还生效?本文讲一下。
查看当前主机名
先看看现在主机名是什么:
hostname
输出类似:
localhost.localdomain
修改主机名的方法
方法一:hostnamectl(推荐,CentOS7+)
这是最方便的方法,一条命令永久生效:
hostnamectl set-hostname 新主机名
比如改成 web-server:
hostnamectl set-hostname web-server
注意: 主机名只能用小写字母、数字、横杠(-),不能用下划线、中文、空格。
方法二:修改配置文件
编辑 /etc/hostname 文件:
vi /etc/hostname
把里面的内容改成新的主机名,保存退出。
然后重启生效:
reboot
或者不重启,手动让它生效:
hostname 新主机名
方法三:临时修改
只改当前运行时的主机名,重启后就恢复:
hostname 新主机名
注意: 这个只是临时生效,重启后还是原来的。
修改后验证
重新登录一下 SSH,或者执行:
bash
让当前 shell 生效,然后看:
hostname
应该显示新的主机名了。
同时修改 hosts
有些服务(比如 MySQL)会用到主机名,最好同时把 /etc/hosts 也改一下:
vi /etc/hosts
找到类似这一行:
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
在后面加上新主机名:
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 web-server
保存退出。
常见坑
坑1:改了主机名但命令行没变
执行了 hostnamectl set-hostname,但命令行前面还是显示旧的。
原因: 当前 shell 会话没刷新。
解决: 退出重新登录 SSH,或者执行 bash 开个新 shell。
坑2:重启后又变回原来的
改了主机名,重启之后又变回去了。
原因: 只执行了 hostname 新名字(临时修改),没改配置文件。
解决: 用 hostnamectl set-hostname 新名字 永久修改。
坑3:主机名有大写或者下划线
主机名用了大写字母或者下划线,有些服务报错。
原因: 主机名规范要求只能用小写字母、数字、横杠。
正确写法: web-server-01
错误写法: Web_Server_01
坑4:改了主机名导致服务连不上
改了主机名之后,某些服务连不上数据库或者其他服务。
原因: 那些服务里配置的还是旧主机名。
解决: 把相关服务的配置里的主机名也改过来,或者同时改 /etc/hosts 做映射。
总结
| 方法 | 命令 | 是否永久 |
|---|---|---|
| hostnamectl | hostnamectl set-hostname 新名字 |
是(推荐) |
| 修改 /etc/hostname | 编辑文件后重启 | 是 |
| hostname | hostname 新名字 |
否(临时) |
推荐用 hostnamectl set-hostname,一条命令搞定,永久生效。
遇到问题加QQ23979811 协助处理
Linux查看网卡信息,ip addr和ifconfig命令
前言
想知道服务器有几块网卡、IP 地址是多少、MAC 地址是多少?Linux 下有两个常用命令:ip addr 和 ifconfig。本文讲一下它们的用法和区别。
ip addr 命令(推荐)
现在新的 Linux 系统都推荐用 ip 命令,功能比 ifconfig 更强大。
查看所有网卡信息
ip addr
或者简写:
ip a
输出类似:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:16:3e:12:34:56 brd ff:ff:ff:ff:ff:ff
inet 192.168.1.100/24 brd 192.168.1.255 scope global eth0
valid_lft forever preferred_lft forever
输出解释
| 字段 | 含义 |
|---|---|
lo |
本地回环网卡 |
eth0 |
物理网卡名称 |
state UP |
网卡状态是开启的 |
link/ether |
MAC 地址 |
inet |
IP 地址 |
/24 |
子网掩码(255.255.255.0) |
只看 IP 地址
不想看那么多信息,只看 IP:
ip addr show eth0 | grep inet
或者更简洁:
ip a s eth0
只看 IPv4 地址
ip -4 addr
查看网卡统计信息
ip -s link
能看到收发包、丢包等统计信息。
ifconfig 命令(老命令)
ifconfig 是老命令,来自 net-tools 包,现在慢慢被淘汰了,但很多老用户还是习惯用。
安装
CentOS 最小化安装可能没有:
yum install -y net-tools
查看所有网卡
ifconfig
输出类似:
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.100 netmask 255.255.255.0 broadcast 192.168.1.255
ether 00:16:3e:12:34:56 txqueuelen 1000 (Ethernet)
RX packets 12345 bytes 1234567 (1.2 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 6789 bytes 987654 (987.6 KB)
TX errors 0 dropped 0 overruns 0 carrier 0
只看某个网卡
ifconfig eth0
查看 lo 网卡
ifconfig 默认不显示 lo(回环网卡),要看的话加 -a:
ifconfig -a
ip 和 ifconfig 的区别
| 对比项 | ip addr | ifconfig |
|---|---|---|
| 新老 | 新命令,推荐 | 老命令,逐步淘汰 |
| 包 | iproute2 包(默认装) | net-tools 包(要单独装) |
| 功能 | 更强大 | 基本够用 |
| 输出 | 信息更全 | 简洁 |
其他常用命令
查看路由表
ip route
或者老命令:
route -n
查看 DNS 配置
cat /etc/resolv.conf
查看网卡连接状态
cat /sys/class/net/eth0/carrier
输出 1 表示网线插着,0 表示没插。
查看网卡速率
ethtool eth0 | grep Speed
常见坑
坑1:ifconfig 命令找不到
执行 ifconfig 提示 command not found。
原因: CentOS7 最小化安装不带 net-tools 包。
解决:
yum install -y net-tools
或者直接用 ip addr,不用装包。
坑2:网卡名不是 eth0
新系统网卡名可能是 ens33、ens33、enp0s3 之类的。
原因: CentOS7 用了新的网卡命名规则。
解决: 先 ip addr 看一下实际网卡名是什么,然后替换。
坑3:IP 地址不对
看到的 IP 地址和你预期的不一样。
原因: 可能有多个网卡,或者是云服务器的内网 IP,不是公网 IP。
解决: 先 ip addr 看所有网卡,确认哪个是你要的。
坑4:网卡 down 了
网卡状态是 DOWN,连不上网。
解决: 手动开启:
ip link set eth0 up
或者:
ifconfig eth0 up
总结
| 命令 | 作用 |
|---|---|
ip addr |
查看所有网卡信息(推荐) |
ip a |
简写 |
ip a s eth0 |
只看 eth0 网卡 |
ifconfig |
老命令查看网卡 |
ifconfig eth0 |
只看 eth0 |
ip route |
查看路由表 |
现在都推荐用 ip addr,功能更全,还不用额外装包。
遇到问题加QQ23979811 协助处理
CentOS7 防火墙开放端口范围,一次性开放多个端口
前言
有时候需要一次性开放很多端口,或者开放一段端口范围,一个个加太麻烦。CentOS7 的 firewalld 支持开放端口范围,也支持一次性开放多个端口,本文讲一下怎么操作。
查看当前开放的端口
先看看现在防火墙开了哪些端口:
firewall-cmd --list-ports
输出类似:
80/tcp 443/tcp 8080/tcp
开放单个端口
永久开放
firewall-cmd --permanent --add-port=80/tcp
然后重载生效:
firewall-cmd --reload
开放 UDP 端口
把 tcp 改成 udp 就行:
firewall-cmd --permanent --add-port=53/udp
firewall-cmd --reload
一次性开放多个端口
方法一:一条命令加多个
firewall-cmd --permanent --add-port=80/tcp --add-port=443/tcp --add-port=8080/tcp
firewall-cmd --reload
多个 --add-port 用空格分开就行。
方法二:开放端口范围
如果要开放一段连续的端口,比如 8000 到 9000:
firewall-cmd --permanent --add-port=8000-9000/tcp
firewall-cmd --reload
这样 8000 到 9000 之间所有 TCP 端口都开放了。
方法三:同时开放 TCP 和 UDP
firewall-cmd --permanent --add-port=53/tcp --add-port=53/udp
firewall-cmd --reload
移除端口
移除单个端口
firewall-cmd --permanent --remove-port=8080/tcp
firewall-cmd --reload
移除端口范围
firewall-cmd --permanent --remove-port=8000-9000/tcp
firewall-cmd --reload
常见场景
场景1:开放 20 到 80 之间的所有端口
firewall-cmd --permanent --add-port=20-80/tcp
firewall-cmd --reload
场景2:同时开放 HTTP 和 HTTPS 服务
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
场景3:只允许某个 IP 访问某个端口
这个更安全,只允许 192.168.1.100 访问 3306 端口:
firewall-cmd --permanent --add-rich-rule="rule family='ipv4' source address='192.168.1.100' port protocol='tcp' port='3306' accept"
firewall-cmd --reload
常见坑
坑1:加了端口没生效
执行了 add-port 但还是访问不了。
原因: 没加 --permanent,或者没 reload。
解决: 一定要加 --permanent 参数,然后执行 firewall-cmd --reload。
坑2:端口范围写反了
写成了 9000-8000,报错。
原因: 范围必须从小到大写。
正确写法: 8000-9000/tcp
错误写法: 9000-8000/tcp
坑3:防火墙没启动
执行 firewall-cmd 报错。
原因: firewalld 服务没启动。
解决:
systemctl start firewalld
systemctl enable firewalld
坑4:开了端口但还是访问不了
防火墙开了端口,但是外部还是访问不了。
原因: 除了防火墙,还有可能是云服务商的安全组没开。
解决: 去云服务商控制台(阿里云、腾讯云等)的安全组里,把对应的端口也开放了。
总结
| 操作 | 命令 |
|---|---|
| 开放单个端口 | firewall-cmd --permanent --add-port=80/tcp |
| 开放多个端口 | firewall-cmd --permanent --add-port=80/tcp --add-port=443/tcp |
| 开放端口范围 | firewall-cmd --permanent --add-port=8000-9000/tcp |
| 移除端口 | firewall-cmd --permanent --remove-port=8080/tcp |
| 查看已开放端口 | firewall-cmd --list-ports |
| 重载生效 | firewall-cmd --reload |
记住:加完端口一定要 --reload 才生效。
遇到问题加QQ23979811 协助处理
Linux查看历史命令,history命令使用技巧
前言
在服务器上敲了很多命令,突然想找之前执行过的某条命令,怎么办?history 命令就能帮你查看历史命令。本文讲一下 history 的常用用法和技巧。
基本用法
查看历史命令
history
输出会列出你执行过的所有命令,每条前面有个编号,比如:
100 yum install -y wget
101 systemctl restart nginx
102 df -h
执行第 N 条历史命令
用 !编号 直接执行:
!101
就会重新执行第 101 条命令。
执行上一条命令
几种方式都行:
!!
或者按向上方向键,再回车。
搜索历史命令
输入开头几个字母,然后按 Ctrl + R:
(reverse-i-search)`sys': systemctl restart nginx
会搜索你输入过的包含这个关键词的历史命令,找到之后回车执行。
再按一次 Ctrl + R 会继续往前找下一个匹配的。
常用技巧
只看最近 N 条
history 10
只看最近 10 条命令。
搜索历史命令里的关键词
history | grep 关键词
比如找所有包含 nginx 的命令:
history | grep nginx
执行最近一条以某个关键词开头的命令
!关键词
比如最近一条以 yum 开头的命令:
!yum
会直接执行最近一次输入的 yum 开头的命令。
替换上一条命令里的内容
比如上一条命令是 cp file1.txt file2.txt,想改成 mv file1.txt file2.txt:
^cp^mv
这样就把上一条命令里的 cp 替换成 mv 再执行。
清空历史命令
清空当前会话的历史
history -c
注意:这个只是清空当前内存里的历史,还没写入文件。
清空历史文件
history -w
把当前内存里的历史写入文件(覆盖掉旧的)。
彻底清空
history -c
history -w
两步一起执行,彻底清空所有历史命令。
删除某一条历史命令
history -d 编号
比如删除第 100 条:
history -d 100
历史命令保存在哪
历史命令默认保存在:
~/.bash_history
每个用户都有自己的 .bash_history 文件。
查看历史文件
cat ~/.bash_history
修改历史记录条数
默认记录 1000 条,可以改:
echo "HISTSIZE=10000" >> /etc/profile
echo "HISTFILESIZE=10000" >> /etc/profile
source /etc/profile
改成记录 10000 条。
常见坑
坑1:history 看不到刚执行的命令
刚执行完的命令,history 里没有。
原因: 历史命令是在退出会话的时候才写入文件的,当前会话的历史在内存里。
解决: 执行 history 就能看到当前会话的所有命令,包括刚执行的。
坑2:history -c 清不干净
执行了 history -c,但重新登录后又出来了。
原因: 只清了内存,没清文件。
解决: 还要执行 history -w 写入文件。
坑3:别人 history 能看到我输的密码
不小心在命令行里输了密码,比如 mysql -uroot -p123456,history 里就有了。
解决:
- 执行完之后赶紧删掉那条历史:
history -d 编号 - 以后输密码不要直接写在命令里,用交互式输入:
mysql -uroot -p,然后回车再输密码
坑4:多开几个终端,历史命令对不上
开了多个 SSH 窗口,在这个窗口执行的命令,另一个窗口 history 里看不到。
原因: 每个终端会话有自己的内存历史,退出时才写入文件。
解决: 这是正常现象,不用纠结。
总结
| 命令 | 作用 |
|---|---|
history |
查看历史命令 |
history 10 |
只看最近 10 条 |
!101 |
执行第 101 条历史命令 |
!! |
执行上一条命令 |
!yum |
执行最近一条 yum 开头的命令 |
history \| grep nginx |
搜索包含 nginx 的历史命令 |
history -c |
清空当前会话历史 |
Ctrl + R |
搜索历史命令 |
日常用得最多的就是 Ctrl + R 搜索 和 history | grep,效率很高。
遇到问题加QQ23979811 协助处理
Linux 压缩命令大全
Linux 常用压缩命令大全
Linux 下压缩解压命令很多,记不住每次都要搜,整理一份常用清单。
1. tar 命令(最常用)
tar 是 Linux 最常用的打包工具,可以配合不同压缩格式。
打包不压缩:
# 打包
tar -cvf blog.tar /www/wwwroot/blog/
# 解包
tar -xvf blog.tar
参数说明:
- -c:创建打包文件
- -x:解包
- -v:显示过程
- -f:指定文件名
2. tar.gz 格式(最常用)
# 压缩
tar -zcvf blog.tar.gz /www/wwwroot/blog/
# 解压
tar -zxvf blog.tar.gz
# 解压到指定目录
tar -zxvf blog.tar.gz -C /data/backup/
-z 就是用 gzip 压缩,这是最常用的格式。
3. tar.bz2 格式(压缩率更高)
# 压缩(体积更小,但更慢)
tar -jcvf blog.tar.bz2 /www/wwwroot/blog/
# 解压
tar -jxvf blog.tar.bz2
4. zip 格式
# 压缩
zip -r blog.zip /www/wwwroot/blog/
# 解压
unzip blog.zip
# 解压到指定目录
unzip blog.zip -d /data/backup/
Windows 用户常用 zip 格式,跨平台方便。
5. gz 格式
# 压缩(单个文件)
gzip file.log
# 解压
gunzip file.log.gz
gzip 只能压单个文件,一般和 tar 配合用。
6. 查看压缩包内容
# 查看 tar 包内容
tar -tvf blog.tar
# 查看 tar.gz 包内容
tar -ztvf blog.tar.gz
# 查看 zip 包内容
unzip -l blog.zip
7. 常用场景示例
备份网站文件:
tar -zcvf backup_$(date +%Y%m%d).tar.gz /www/wwwroot/blog/
备份数据库:
mysqldump -u root -p123456 blog | gzip > db_$(date +%Y%m%d).sql.gz
传输文件压缩:
tar -zcvf - /www/wwwroot/blog/ | ssh root@192.168.1.100 "tar -zxvf - -C /backup/"
8. 总结
| 格式 | 压缩率 | 速度 | 命令 |
|---|---|---|---|
| tar.gz | 中 | 快 | tar -z |
| tar.bz2 | 高 | 慢 | tar -j |
| zip | 中 | 中 | zip/unzip |
| tar | 无 | 最快 | tar |
日常用 tar.gz 就够了,跨平台用 zip,追求小体积用 bz2。
Linux怎么查看当前在线用户,踢出用户命令
前言
服务器是多人用的,有时候需要看看谁在线上,或者把某个挂机的用户踢下线。Linux 自带了几个命令可以查看当前在线用户,本文就讲一下怎么用。
查看在线用户
who 命令
最简单的命令,直接看谁在线:
who
输出类似:
root pts/0 2026-10-06 09:00 (192.168.1.100)
zhangsan pts/1 2026-10-06 09:15 (192.168.1.101)
lisi pts/2 2026-10-06 09:20 (192.168.1.102)
每一列分别是:用户名、终端、登录时间、来源 IP。
w 命令
比 who 更详细,还能看到用户在干嘛:
w
输出类似:
09:30:00 up 2:15, 3 users, load average: 0.00, 0.01, 0.05
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
root pts/0 192.168.1.100 09:00 5.00s 0.03s 0.01s w
zhangsan pts/1 192.168.1.101 09:15 15.00s 0.02s 0.00s bash
lisi pts/2 192.168.1.102 09:20 1:30 0.01s 0.01s top
多了几列:
LOGIN@:登录时间IDLE:空闲时间JCPU:该终端所有进程用的 CPU 时间PCPU:当前进程用的 CPU 时间WHAT:当前在跑什么命令
users 命令
只看用户名,重复的就重复显示:
users
输出:
root zhangsan lisi
last 命令
看历史登录记录:
last
会列出最近登录过的用户,包括用户名、终端、来源 IP、登录时间、退出时间、总共在线多久。
踢出用户
pkill 踢出某个用户
比如把 zhangsan 踢下线:
pkill -u zhangsan
这会杀掉 zhangsan 的所有进程,直接把他踢下线。
杀掉某个终端
先看 who,找到要踢的人的终端,比如 pts/1:
who
然后杀掉这个终端:
pkill -t pts/1
或者:
kill -9 $(ps -t pts/1 -o pid=)
踢所有非 root 用户
警告!这会把其他所有登录的人都踢掉:
pkill -u -root
常见坑
坑1:把自己踢下线了
执行 pkill 的时候把自己的终端也杀掉了,SSH 直接断开。
解决: 踢人的时候确认一下自己的终端,别把自己踢了:
who am i
先看自己是哪个终端,然后踢别人的时候避开自己的终端。
坑2:踢了用户但他又登回来了
踢了之后用户马上又登上来了,没用。
解决: 如果是恶意用户,直接改密码或者封 IP:
# 改密码
passwd 用户名
# 封 IP
firewall-cmd --permanent --add-rich-rule="rule family='ipv4' source address='192.168.1.101' reject"
firewall-cmd --reload
坑3:看不到图形界面登录的用户
who 只看终端登录的,图形界面登录的可能看不到。
解决: 用 last 看历史记录,或者看 /var/log/auth.log(CentOS 是 /var/log/secure)。
总结
查看在线用户的常用命令:
| 命令 | 作用 |
|---|---|
who |
看谁在线 |
w |
看谁在线 + 在干嘛 |
users |
只看用户名 |
last |
看历史登录记录 |
pkill -u 用户名 |
踢出某个用户 |
pkill -t pts/1 |
杀掉某个终端 |
日常运维用 w 就够了,信息最全。
遇到问题加QQ23979811 协助处理
PHP 错误处理最佳实践
PHP 错误处理最佳实践
错误处理是 PHP 开发最容易忽略但最重要的环节,写好了排查问题事半功倍,写不好线上出问题抓瞎。
1. 错误级别
PHP 有不同的错误级别:
- E_ERROR:致命错误,脚本停止
- E_WARNING:警告,脚本继续
- E_NOTICE:注意,一般是不规范写法
- E_PARSE:语法解析错误
// 开发环境显示所有错误
error_reporting(E_ALL);
ini_set('display_errors', 1);
// 生产环境不显示错误,只记日志
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php/error.log');
2. 错误 vs 异常
PHP 里错误和异常是两回事:
- 错误:语法错误、内存不足这种,用 try/catch 抓不住
- 异常:业务层面的错误,比如数据库连接失败
PHP 7+ 大部分错误会抛 Error 异常,也可以抓了。
3. try/catch 用法
try {
// 可能出错的代码
$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
} catch (PDOException $e) {
// 捕获异常
echo "数据库连接失败:" . $e->getMessage();
// 记日志
error_log("DB Error: " . $e->getMessage());
}
4. 自定义错误处理
// 设置自定义错误处理器
set_error_handler(function($errno, $errstr, $errfile, $errline) {
// 把错误转成异常抛出
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});
// 设置异常处理器
set_exception_handler(function($e) {
// 所有未捕获的异常都到这里
error_log("Uncaught: " . $e->getMessage());
echo "系统繁忙,请稍后再试";
});
// 注册关闭函数,抓致命错误
register_shutdown_function(function() {
$error = error_get_last();
if ($error && in_array($error['type'], [E_ERROR, E_PARSE])) {
error_log("Fatal: " . $error['message']);
}
});
5. 开发 vs 生产环境
开发环境:
- 打开 display_errors,错误直接显示在页面
- 打开所有错误级别
- 详细的报错信息方便调试
生产环境:
- 关闭 display_errors,不要把错误暴露给用户
- 错误全部写到日志文件
- 页面显示友好的"系统错误"提示
6. 常用错误处理技巧
// 1. 用 @ 抑制错误不好,应该判断
// 不好:
$file = @file_get_contents('config.php');
// 好:
$file = file_exists('config.php') ? file_get_contents('config.php') : null;
// 2. 不要把数据库错误信息直接返回给前端
// 不好:
echo $e->getMessage();
// 好:
error_log($e->getMessage());
echo json(['code' => 500, 'msg' => '系统错误']);
// 3. 日志要分级
error_log('信息:用户登录', 3, '/var/log/info.log');
error_log('警告:登录失败次数过多', 3, '/var/log/warn.log');
error_log('错误:数据库连接失败', 3, '/var/log/error.log');
7. 总结
PHP 错误处理核心:
- 开发环境显示错误,生产环境只记日志
- try/catch 捕获业务异常
- set_exception_handler 兜底
- 不要把错误细节暴露给用户
- 日志分级,方便排查
搞懂这些,线上出问题排查效率翻倍。
MySQL覆盖索引是什么,为什么能提升查询性能
前言
做SQL优化的同学肯定听说过覆盖索引,很多文章里都说用覆盖索引能提升查询性能。但是很多新手同学搞不懂覆盖索引到底是什么,为什么能提升性能。
这篇文章就把覆盖索引的知识点从头到尾讲清楚:什么是覆盖索引?为什么能提升性能?怎么判断是不是用了覆盖索引?怎么利用覆盖索引优化查询?看完之后你就彻底搞懂了。
一、什么是覆盖索引
先说说回表是什么。
MySQL的InnoDB引擎,主键索引是聚簇索引,叶子节点直接存整行数据。普通索引的叶子节点存的是主键值。
如果你用普通索引查询,查到主键值之后,还要再去主键索引里查一遍,拿到完整的行数据。这个过程就叫回表。
那什么是覆盖索引呢?就是:你要查询的字段,在索引里就都有了,不用再回表去主键索引里查了。这就叫覆盖索引,或者叫索引覆盖。
简单来说:查询需要的所有数据,索引里都有,不用回表,就是覆盖索引。
二、为什么覆盖索引能提升性能
为什么覆盖索引能提升性能?因为少了回表这一步。
我们对比一下:
没有覆盖索引的情况
- 先在普通索引里查,找到主键值
- 再拿着主键值去主键索引里查,拿到完整的行数据
这样要查两棵B+树,查了两次,当然慢。
有覆盖索引的情况
- 直接在索引里查到所有需要的数据,就完事了
这样只查了一棵B+树,少了回表这一步,当然快。
尤其是表很大的时候,回表要做随机IO,性能很差。如果不用回表,直接在索引里就能拿到数据,那性能提升就非常明显了。
三、怎么判断是不是覆盖索引
用explain看执行计划的时候,如果Extra列里显示 Using index,就说明用了覆盖索引,不用回表。
比如:
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | users | ref | name | name | 767 | const| 10 | Using index |
+----+-------------+-------+-------+---------------+------+---------+------+------+-------------+
看到Extra里的Using index了吗?这就说明用了覆盖索引。
四、怎么利用覆盖索引优化查询
知道了覆盖索引的好处,那怎么利用它来优化查询呢?
1. 查询的时候只查需要的字段,不要SELECT *
很多同学写SQL的时候喜欢写SELECT *,把所有字段都查出来。这样你要查的字段太多了,索引里不可能全都有,就必须回表。
如果你只查需要的几个字段,那就有可能把这几个字段都加到索引里,这样就能用到覆盖索引了。
比如:
-- 不好的写法
SELECT * FROM users WHERE name = '张三';
-- 好的写法
SELECT id, name, age FROM users WHERE name = '张三';
2. 建联合索引,把经常查询的字段都加进去
如果你经常根据name查询,并且经常查name和age,那就建一个(name, age)的联合索引。
这样当你查SELECT name, age FROM users WHERE name = '张三'的时候,索引里就有name和age了,不用回表,直接就能拿到数据。
3. 把常用的查询字段加到索引里
除了WHERE条件的字段,SELECT里的字段也可以加到索引里,这样就能实现覆盖索引。
比如你经常查SELECT id, name, age FROM users WHERE name = ?,那就建一个(name, age)的联合索引,因为id是主键,InnoDB的普通索引叶子节点本来就存主键值,所以索引里就有id、name、age三个字段了,正好覆盖你的查询。
五、覆盖索引的注意点
1. 不是所有查询都能用覆盖索引
只有你要查询的字段都在索引里的时候,才能用覆盖索引。如果有字段不在索引里,那就必须回表。
2. 覆盖索引也不是万能的
不要为了覆盖索引,把所有字段都加到索引里。索引太多了,会影响插入和更新的性能。要权衡一下,哪些查询是高频的,值得为它建覆盖索引。
3. 主键索引天然就是覆盖索引
如果你是用主键查询,那本来就是在主键索引里查,直接就能拿到所有数据,天然就是覆盖索引。
常见坑
坑1:以为只要用了索引就是覆盖索引
很多同学以为只要查询用到了索引,就是覆盖索引。其实不是,用到索引只是说用索引找到了主键值,如果你还要查其他字段,还是要回表。只有Extra里显示Using index,才是真的覆盖索引。
坑2:写SELECT *,用不了覆盖索引
很多同学写SQL的时候喜欢SELECT *,结果要查的字段太多了,索引里根本放不下,就用不了覆盖索引。记住:只查需要的字段,别查所有字段。
坑3:为了覆盖索引,索引建得太多
很多同学为了让某个查询用上覆盖索引,把那个查询的所有字段都加到索引里。结果一个表建了好几十个索引,插入更新的时候要维护这么多索引,性能很差。覆盖索引是好,但是不要滥用,要权衡。
坑4:不知道Using index就是覆盖索引
很多同学看explain的时候,看到Extra里的Using index不知道是什么意思。记住:Using index就是覆盖索引,不用回表,性能很好。
坑5:联合索引的顺序不对
很多同学建联合索引的时候,顺序建反了,结果用不了覆盖索引。记住:联合索引要满足最左前缀原则,WHERE条件里的字段要放在前面,SELECT的字段放在后面。
总结
MySQL覆盖索引的核心知识点:
- 什么是覆盖索引:查询需要的所有数据,索引里都有,不用回表
- 为什么能提升性能:少了回表这一步,不用查两棵B+树,减少IO
- 怎么判断:explain的Extra列里显示Using index,就是覆盖索引
- 怎么利用:
- 不要写SELECT *,只查需要的字段
- 建联合索引,把WHERE和SELECT的字段都加进去
- 注意点:不要为了覆盖索引建太多索引,要权衡性能
记住:覆盖索引是SQL优化里很重要的一个手段,很多时候只要把SELECT *改成只查需要的字段,再建个合适的联合索引,性能就能提升好几倍。
遇到问题加QQ23979811 协助处理
MySQL自增主键用完了怎么办,一次讲清楚
前言
做后端开发的同学,建表的时候都喜欢用自增主键,觉得简单方便。但是很多同学不知道,自增主键是有最大值的,用完了就不能再插入数据了。
很多新手同学建表的时候,主键直接用INT,也不知道INT的最大值是多少。结果项目跑了几年,数据量涨上来了,自增主键快用完了,才发现要出问题了。
这篇文章就把MySQL自增主键的知识点讲清楚:不同整型的最大值是多少?用完了会怎么样?怎么避免?已经用完了怎么办?看完之后你就彻底搞懂了。
一、不同整型的最大值
MySQL的整型类型有好几种,每种的最大值都不一样。我们常用的有这几个:
| 类型 | 字节数 | 有符号最大值 | 无符号最大值 |
|---|---|---|---|
| TINYINT | 1 | 127 | 255 |
| SMALLINT | 2 | 32767 | 65535 |
| MEDIUMINT | 3 | 8388607 | 16777215 |
| INT | 4 | 2147483647 | 4294967295 |
| BIGINT | 8 | 9223372036854775807 | 18446744073709551615 |
重点看INT和BIGINT
我们建表的时候,主键一般用INT或者BIGINT。
INT的最大值
INT有符号的最大值是 21亿多(2147483647),无符号的是 42亿多(4294967295)。
很多同学觉得21亿很多,肯定用不完。但是对于一些业务量大的网站,比如电商、社交,用户量很大,一张表可能就有几亿条数据,INT真的不够用。
BIGINT的最大值
BIGINT有符号的最大值是 922亿亿(9223372036854775807),这个数太大了,基本上一辈子都用不完。
所以建议:主键直接用BIGINT,不要用INT,省得以后不够用了还要改。
二、自增主键用完了会怎么样
如果自增主键用到了最大值,再插入数据会怎么样?
MySQL会报错:
ERROR 1467 (HY000): Failed to read auto-increment value from storage engine
或者:
ERROR: Duplicate entry '2147483647' for key 'PRIMARY'
意思就是:自增主键已经到最大值了,不能再插入新数据了。
这时候业务就受影响了,不能插入新数据了,必须赶紧处理。
三、怎么避免自增主键用完
最好的方法就是:建表的时候主键直接用BIGINT,不要用INT。
BIGINT的最大值太大了,基本上不可能用完。这样就不用以后担心主键不够用的问题了。
很多同学觉得BIGINT占空间大,其实BIGINT是8字节,INT是4字节,一张表也就多了几个字节,根本不算什么。但是换来的是永远不用担心主键不够用,这太值了。
怎么查看当前自增值到多少了
想知道当前自增主键已经用到多少了,可以用这个命令:
SHOW TABLE STATUS LIKE '表名';
执行完之后,看Auto_increment那列,就是当前自增值。
然后你对比一下这个类型的最大值,看看还有多少余量。如果已经用了一半了,那就得注意了。
四、已经用完了怎么办
如果你的主键已经快用完了,或者已经用完了,怎么办?
方法1:把INT改成BIGINT
这是最直接的方法。把主键的类型从INT改成BIGINT,这样最大值就变成922亿亿了,够用很久了。
执行的SQL:
ALTER TABLE 表名 MODIFY id BIGINT AUTO_INCREMENT;
注意:这个操作会重建表,如果表很大的话,会花很长时间,而且会锁表。最好在业务低峰期执行,或者用pt-online-schema-change这种工具在线改。
方法2:分库分表
如果你的表已经很大了,改主键类型也不能根本解决问题,那就分库分表,把数据分散到多个表或者多个数据库里。
不过分库分表成本很高,一般都是到万不得已才做。
五、自增主键的其他坑
除了用完的问题,自增主键还有一些其他的坑:
坑1:删除数据之后自增值不回收
很多同学以为删除了数据,自增值会重新开始。其实不是,自增值只会涨,不会降。就算你把表清空了,自增值还是接着之前的继续涨。
比如你插入了100条数据,id到了100,然后你把这100条都删了,再插入一条新数据,id还是101,不是1。
坑2:自增值不连续
因为有事务回滚的情况,自增值可能会不连续。比如一个事务插入了一条数据,id是100,然后事务回滚了,那100这个id就浪费了,下一条数据的id是101。
所以不要指望自增主键是连续的,它只是唯一的,不是连续的。
坑3:用了INT UNSIGNED,觉得够大了
很多同学觉得用了INT UNSIGNED,最大值是42亿,肯定够了。其实对于一些大业务,42亿也不够。还是用BIGINT最保险。
常见坑
坑1:建表的时候用INT主键,后来不够用了
很多同学建表的时候图简单,直接用INT主键,结果项目跑了几年,数据量涨上来了,INT快用完了,才慌慌张张要改成BIGINT。这时候表已经很大了,改起来很麻烦。一开始就用BIGINT,省得以后麻烦。
坑2:不知道自增主键的最大值
很多新手同学根本不知道INT的最大值是多少,觉得反正自增,肯定用不完。其实21亿听起来很多,但是对于大业务来说,真的可能用完。
坑3:改主键类型的时候锁表了
很多同学直接在线上执行ALTER TABLE改主键类型,结果表很大,改了几个小时,一直锁表,业务直接卡住了。改之前一定要评估表大小,最好在业务低峰期执行,或者用在线改表工具。
坑4:以为删除数据之后自增值会降
很多同学删了很多数据,以为自增值会降下来,就不用改类型了。其实不是,自增值只会涨,不会降。删了数据之后,自增值还是原来的,不会变。
坑5:用UUID做主键
很多同学觉得自增主键不够用,就想用UUID做主键。其实UUID做主键问题更多:占空间大、无序导致索引分裂、查询慢。真的不够用的话,改BIGINT就行,没必要用UUID。
总结
MySQL自增主键的核心知识点:
- 不同整型的最大值:INT是21亿(有符号)/42亿(无符号),BIGINT是922亿亿
- 主键直接用BIGINT:不要用INT,省得以后不够用了还要改
- 自增主键用完会报错:不能再插入新数据了
- 怎么查看当前自增值:SHOW TABLE STATUS,看Auto_increment列
- 用完了怎么办:把INT改成BIGINT,或者分库分表
- 自增主键的坑:删除数据自增值不回收、自增值不连续
记住:建表的时候主键直接用BIGINT,这是最稳妥的做法。不要一开始图省事用INT,以后改起来更麻烦。
遇到问题加QQ23979811 协助处理
MySQL InnoDB和MyISAM引擎区别,怎么选
前言
很多新手同学刚学MySQL的时候,根本不知道存储引擎是什么,建表的时候直接用默认的。其实存储引擎很重要,不同的存储引擎有不同的特点,适合不同的场景。
MySQL最常用的两个存储引擎就是InnoDB和MyISAM。很多同学搞不清楚它们有什么区别,也不知道应该选哪个。这篇文章就把这两个引擎的区别讲清楚,看完之后你就知道怎么选了。
一、什么是存储引擎
存储引擎就是MySQL里,真正存储数据、管理数据的东西。MySQL是把数据和引擎分开的,不同的表可以用不同的存储引擎。
就像你有一个仓库,你可以选择用货架放东西,也可以用箱子放东西,不同的存放方式有不同的特点。存储引擎就是这个"存放方式"。
二、InnoDB和MyISAM的核心区别
我们先看一个对比表,一目了然:
| 特点 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | ✅ 支持 | ❌ 不支持 |
| 外键支持 | ✅ 支持 | ❌ 不支持 |
| 锁粒度 | 行锁 | 表锁 |
| 崩溃恢复 | ✅ 支持 | ❌ 不支持 |
| 全文索引 | ✅ 5.6以后支持 | ✅ 支持 |
| 主键索引 | 聚簇索引 | 非聚簇索引 |
| 读写性能 | 写性能好,读也不错 | 读性能好,写性能差 |
三、InnoDB的特点
InnoDB是MySQL默认的存储引擎,从MySQL 5.5开始,默认就是InnoDB了。
1. 支持事务
这是InnoDB最大的特点。支持ACID事务,也就是原子性、一致性、隔离性、持久性。如果你需要事务,那就必须用InnoDB。
2. 支持行锁
InnoDB的锁粒度是行锁。也就是说,一个事务锁了某一行,其他事务还可以操作表里的其他行。这样并发性能就很好。
3. 支持外键
InnoDB支持外键约束,可以保证数据的一致性。
4. 支持崩溃恢复
InnoDB有redo log,MySQL崩溃之后重启,能够自动恢复数据,不容易丢数据。
5. 聚簇索引
InnoDB的主键索引是聚簇索引,叶子节点直接存数据。这样按主键查询速度很快。
InnoDB适合什么场景
- 需要事务的场景
- 读写都比较频繁的场景
- 对数据一致性要求高的场景
- 大部分互联网业务场景
四、MyISAM的特点
MyISAM是MySQL早期默认的存储引擎,现在用的越来越少了。
1. 不支持事务
MyISAM不支持事务,执行SQL就是直接执行,没有事务的概念。
2. 表锁
MyISAM的锁粒度是表锁。也就是说,一个操作锁了整张表,其他操作都要等。这样并发性能就很差。
3. 读性能好
MyISAM的读性能很好,因为它的结构简单,查询的时候直接读文件就行。
4. 不支持崩溃恢复
MyISAM没有redo log,MySQL崩溃之后,很可能会损坏数据,需要手动修复。
5. 非聚簇索引
MyISAM的索引是非聚簇索引,叶子节点存的是数据的地址,按主键查询要回表。
MyISAM适合什么场景
- 读多写少的场景
- 不需要事务的场景
- 一些日志表、统计报表表
- 对数据一致性要求不高的场景
五、怎么选InnoDB和MyISAM
很多同学问:到底应该选哪个?
其实现在大部分场景,直接选InnoDB就对了。因为InnoDB是默认的,功能也全,支持事务、行锁、崩溃恢复,大部分业务都适用。
MyISAM现在基本上只有一些特殊场景才会用,比如:
- 纯读的表,比如一些静态配置表
- 一些历史遗留的老项目,原来是用MyISAM的
记住:新项目直接用InnoDB,不用考虑MyISAM。
六、怎么查看表用的是什么引擎
查看某张表的引擎
SHOW TABLE STATUS LIKE '表名';
执行完之后,看Engine那列,就是这张表用的存储引擎。
查看数据库里所有表用的引擎
SELECT TABLE_NAME, ENGINE
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = '数据库名';
七、怎么修改表的引擎
如果一张表原来是用MyISAM,想改成InnoDB,可以执行:
ALTER TABLE 表名 ENGINE = InnoDB;
注意:这个操作会把整张表重建一遍,如果表很大的话,会花很长时间,而且会锁表。最好在业务低峰期执行。
常见坑
坑1:以为MyISAM性能更好
很多同学觉得MyISAM读性能好,就想用MyISAM。其实现在InnoDB的性能已经很好了,尤其是读多写少的场景,InnoDB也不差。而且MyISAM不支持事务、表锁,并发高了性能很差。现在新项目直接用InnoDB就对了。
坑2:不知道MySQL默认引擎变了
很多老教程说MySQL默认是MyISAM,其实从MySQL 5.5开始,默认就是InnoDB了。不要被老教程误导。
坑3:用MyISAM做业务表
很多同学不知道MyISAM不支持事务,结果把业务表用MyISAM,出了问题才发现。只要是业务表,需要事务的,一定要用InnoDB。
坑4:修改引擎的时候没注意锁表
很多同学直接在线上执行ALTER TABLE改引擎,结果锁表了,业务直接卡住了。改引擎之前一定要评估表大小,最好在业务低峰期执行,或者用pt-online-schema-change这种工具在线改。
坑5:以为InnoDB一定比MyISAM慢
很多同学觉得InnoDB因为支持事务、行锁,所以性能比MyISAM差。其实不是,现在InnoDB的性能已经非常好了,尤其是读写都有并发的场景,InnoDB的行锁比MyISAM的表锁性能好很多。
总结
InnoDB和MyISAM的核心区别:
- InnoDB支持事务,MyISAM不支持
- InnoDB是行锁,MyISAM是表锁,InnoDB并发性能好
- InnoDB支持崩溃恢复,MyISAM不支持
- InnoDB支持外键,MyISAM不支持
- InnoDB是聚簇索引,按主键查询快
- 现在MySQL默认就是InnoDB,新项目直接用InnoDB
- MyISAM只有一些特殊场景才用,比如纯读的表
记住:现在不用纠结选哪个,新项目直接用InnoDB就对了,不用考虑MyISAM。
遇到问题加QQ23979811 协助处理
MySQL乐观锁和悲观锁,业务中怎么选
前言
做后端开发的同学肯定都遇到过并发修改数据的问题:两个人同时改同一条数据,结果后提交的把先提交的覆盖了,数据就不对了。这时候怎么办?就需要用到并发控制。
常见的并发控制方案有两种:乐观锁和悲观锁。很多同学搞不懂这两个有什么区别,也不知道业务里应该选哪个。这篇文章就把乐观锁和悲观锁的概念、实现方法、怎么选讲清楚,看完之后你就知道什么时候用哪个了。
一、什么是悲观锁
悲观锁,顾名思义,就是很悲观。它认为并发冲突的概率很高,所以在操作数据之前,一定要先把数据锁住,然后再操作。
就像你要拿一个贵重物品,你怕别人也拿,所以先把它锁起来,你操作完了再放开。
悲观锁的特点
- 先锁再操作:操作之前先加锁
- 并发度低:同一时间只能有一个事务操作
- 适合冲突多的场景:如果并发冲突很频繁,悲观锁比较合适
二、什么是乐观锁
乐观锁,顾名思义,就是很乐观。它认为并发冲突的概率很低,所以操作数据的时候不加锁,等更新的时候再检查一下,看看有没有人在这期间改过数据。
就像你要拿一个东西,你觉得没人会跟你抢,所以直接去拿,拿的时候再看一眼,如果发现已经被别人拿了,那就重新来。
乐观锁的特点
- 操作的时候不加锁:先查出来,更新的时候再检查
- 并发度高:不用提前加锁,多个事务可以同时操作
- 适合冲突少的场景:如果并发冲突不频繁,乐观锁性能更好
三、悲观锁怎么实现
悲观锁一般用数据库的行锁来实现。MySQL里有两种方式:
1. SELECT ... FOR UPDATE
这个语句会给查询到的行加排他锁,其他事务就不能再改这些行了。
举个例子:
-- 开启事务
BEGIN;
-- 查询商品库存,并且加锁
SELECT stock FROM goods WHERE id = 1 FOR UPDATE;
-- 如果库存够的话,扣减库存
UPDATE goods SET stock = stock - 1 WHERE id = 1;
-- 提交事务
COMMIT;
这个流程就是:先把这行锁住,然后查库存,然后扣减库存,提交事务的时候释放锁。在这个过程中,其他事务想要修改这行数据,就必须等你释放锁。
2. 用UPDATE语句自带的锁
其实UPDATE语句本身就会加排他锁。所以你也可以不用SELECT FOR UPDATE,直接用UPDATE:
UPDATE goods SET stock = stock - 1 WHERE id = 1 AND stock > 0;
这样的话,如果库存够,就更新成功;如果库存不够,就更新0行。这其实也是一种悲观锁的实现。
四、乐观锁怎么实现
乐观锁一般用版本号或者时间戳来实现。
1. 版本号方式
在表里加一个version字段,每次更新的时候,把version加1。更新的时候检查一下version是不是跟之前查出来的一样,如果一样就更新,不一样就说明已经被别人改过了。
举个例子:
-- 先查出来数据,得到version
SELECT stock, version FROM goods WHERE id = 1;
-- 假设查出来version是1,stock是10
-- 更新的时候,带上version条件
UPDATE goods
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1;
如果这个UPDATE语句执行之后,影响行数是1,说明更新成功了;如果影响行数是0,说明这期间已经有人改过数据了,更新失败,这时候就需要重试。
2. 时间戳方式
跟版本号方式差不多,只不过用时间戳代替版本号。每次更新的时候,把更新时间改成当前时间,更新的时候检查一下时间是不是跟之前查出来的一样。
五、乐观锁和悲观锁怎么选
很多同学问:业务里到底应该选乐观锁还是悲观锁?
其实没有绝对的答案,要看具体场景:
选悲观锁的场景
- 并发冲突很频繁:如果很多人同时操作同一条数据,冲突很频繁,用悲观锁比较好。不然乐观锁每次都更新失败,要不断重试,反而更麻烦
- 事务很长:如果事务里要做很多事情,持锁时间长,用悲观锁可以避免别人在你操作的期间改数据
- 不能重试的场景:如果业务上不能重试,那就用悲观锁,提前锁住,保证能成功
选乐观锁的场景
- 并发冲突很少:如果并发冲突的概率很低,用乐观锁性能更好,不用提前加锁,并发度高
- 事务很短:如果事务里只做一点点事情,那用乐观锁就够了
- 读多写少:如果大部分是读,只有很少的写,用乐观锁比较合适
总结一下: 冲突多、事务长 → 悲观锁;冲突少、读多写少 → 乐观锁。
六、乐观锁的ABA问题
乐观锁有一个经典的问题,就是ABA问题。
什么是ABA问题?比如:
- 事务A查出来数据是version=1,值是100
- 这时候事务B把数据改成了200,又改回了100,version变成了3
- 然后事务A更新的时候,它以为值还是100,所以更新成功了
但是中间数据其实已经被改过了,只是最后又改回来了。这就是ABA问题。
怎么解决ABA问题
一般用版本号就能解决ABA问题,因为每次修改版本号都会加1,就算最后改回来了,版本号也不一样了。
如果用时间戳的话,也能解决ABA问题。
常见坑
坑1:以为乐观锁是不加锁
很多同学以为乐观锁就是不加锁,其实不是。乐观锁是更新的时候再加锁,只是操作之前不加锁。它本质上还是要靠数据库的行锁来保证更新的原子性。
坑2:乐观锁更新失败了不知道重试
很多同学用乐观锁,更新失败了就直接报错给用户了。其实更新失败是正常的,这时候应该自动重试几次,重试成功了就没事了。如果重试几次都失败,再报错。
坑3:悲观锁加的锁太大
很多同学用SELECT FOR UPDATE,结果WHERE条件没有用到索引,导致锁了整张表,而不是某一行。这样并发度就太低了,性能很差。一定要确保WHERE条件用到了索引,这样才会加行锁。
坑4:事务太长,持锁时间久
用悲观锁的时候,如果事务很长,持锁时间久,其他事务都在等锁,并发性能就很差。所以一定要把事务尽量缩短,不要在事务里做耗时操作。
坑5:版本号用的不对
很多同学用版本号,但是更新的时候忘了把version加1,或者更新的时候没有带上version条件,这样乐观锁就失效了。一定要确保更新的时候,version加了1,并且WHERE条件里带上了原来的version。
总结
乐观锁和悲观锁的核心知识点:
- 悲观锁:认为冲突多,先加锁再操作,并发度低,适合冲突多的场景
- 乐观锁:认为冲突少,操作的时候不加锁,更新的时候再检查,并发度高,适合冲突少的场景
- 悲观锁实现:SELECT ... FOR UPDATE,或者直接用UPDATE语句自带的锁
- 乐观锁实现:版本号方式,或者时间戳方式
- 怎么选:冲突多、事务长用悲观锁;冲突少、读多写少用乐观锁
- ABA问题:乐观锁的经典问题,用版本号可以解决
- 乐观锁更新失败要自动重试:不要直接报错给用户,重试几次就好了
记住:没有绝对的好和坏,看具体场景选。大部分互联网场景,读多写少,用乐观锁比较多,性能更好。
遇到问题加QQ23979811 协助处理
MySQL死锁排查和解决方法,快速定位死锁
前言
做后端开发的同学肯定都遇到过死锁:两个事务互相等对方释放锁,结果都卡住了,数据库报错"Deadlock found when trying to get lock"。这时候整个业务都卡住了,用户都在骂娘,你急得满头大汗不知道怎么办。
很多新手同学遇到死锁就慌了,不知道怎么排查,也不知道怎么解决。其实死锁不可怕,只要掌握了正确的排查方法,很快就能定位到问题。这篇文章就把MySQL死锁的知识点从头到尾讲清楚:什么是死锁?怎么查看死锁?怎么排查?怎么避免?看完之后你就彻底搞懂了。
一、什么是死锁
死锁就是两个或者多个事务,互相持有对方需要的锁,又互相等对方释放锁,结果谁也动不了,就卡住了。
举个最简单的例子:
- 事务A锁了id=1的行,想要锁id=2的行
- 事务B锁了id=2的行,想要锁id=1的行
- 这时候A等B释放id=2的锁,B等A释放id=1的锁
- 两个事务互相等,就死锁了
MySQL检测到死锁之后,会自动回滚其中一个事务,让另一个事务继续执行。所以死锁不会一直卡住,但是被回滚的那个事务就失败了,业务上会报错。
二、死锁产生的原因
死锁一般都是因为这几种情况:
1. 多个事务以不同的顺序访问同一批资源
这是最常见的死锁原因。比如两个事务,一个先操作表A再操作表B,另一个先操作表B再操作表A,就容易死锁。
2. 事务太长,持锁时间久
如果一个事务里做了很多事情,持锁时间很长,其他事务都在等锁,时间一长就容易死锁。
3. 没有用到索引,锁了太多的行
如果WHERE条件没有用到索引,InnoDB就会扫描全表,把所有行都锁了,这样锁的范围很大,就容易和其他事务的锁冲突,产生死锁。
4. 间隙锁导致的死锁
间隙锁虽然是为了解决幻读,但是也会增加死锁的概率。两个事务都往同一个范围里插数据,间隙锁互相冲突,就容易死锁。
三、怎么查看死锁
MySQL默认会自动检测死锁,并且把死锁的信息记录下来。我们可以用下面的命令查看最近一次死锁的信息:
SHOW ENGINE INNODB STATUS;
执行完之后,会输出一大段信息,找到里面的 LATEST DETECTED DEADLOCK 部分,就是最近一次死锁的详细信息。
这个部分会告诉你:
- 哪个事务持有了什么锁
- 哪个事务在等什么锁
- 最后MySQL回滚了哪个事务
开启死锁日志
如果想要记录所有的死锁信息,不仅是最近一次,可以开启死锁日志:
# my.cnf里配置
innodb_print_all_deadlocks = 1
开启之后,所有的死锁信息都会写到MySQL的错误日志里。
四、怎么排查死锁
拿到死锁日志之后,怎么分析呢?一般按这个步骤来:
步骤1:看两个事务分别执行了什么SQL
死锁日志里会显示两个事务的SQL语句。你要看看这两个SQL分别操作了哪些表、哪些行。
步骤2:看锁的信息
日志里会显示每个事务持有了什么锁,在等什么锁。你要搞清楚:
- 事务A持有了哪些行的锁
- 事务A想要拿哪些行的锁
- 事务B持有了哪些行的锁
- 事务B想要拿哪些行的锁
这样就能搞清楚为什么会死锁了。
步骤3:分析业务逻辑
看完SQL和锁的信息之后,再结合业务逻辑,看看这两个事务为什么会以不同的顺序操作同一批数据。
比如:
- 事务A是扣库存的逻辑
- 事务B是修改订单的逻辑
- 它们都操作了商品表和订单表,但是顺序不一样,就死锁了
五、怎么避免死锁
排查完死锁之后,最重要的是怎么避免下次再出现。常见的避免死锁的方法:
1. 按相同的顺序访问资源
这是最有效的方法。所有的事务都按相同的顺序访问表和行,比如所有事务都先操作商品表,再操作订单表,就不会出现两个事务互相等的情况了。
2. 大事务拆成小事务
事务越小,持锁时间越短,死锁的概率就越低。把大事务拆成多个小事务,每个事务只做一点点事情。
比如不要在一个事务里调用RPC接口、查Redis、做复杂计算,这些耗时操作都放到事务外面。
3. 尽量用索引访问数据
如果WHERE条件没有用到索引,就会扫描全表,把所有行都锁了,这样锁的范围很大,死锁概率就高。一定要确保查询都用到了索引。
4. 降低隔离级别
如果对幻读不是很敏感,可以把隔离级别从可重复读(RR)降到读提交(RC)。这样间隙锁就没了,死锁的概率也会低很多。
很多互联网公司都是用RC隔离级别,就是为了减少锁冲突和死锁。
5. 设置锁等待超时时间
如果真的出现死锁了,不要让事务一直等。可以设置锁等待超时时间:
SET GLOBAL innodb_lock_wait_timeout = 5;
这样如果一个事务等锁超过5秒,就自动报错,不会一直卡住。
六、死锁和锁等待的区别
很多同学分不清死锁和锁等待:
锁等待
一个事务等另一个事务释放锁,但是不存在循环等待,等一会儿就好了。
比如事务A锁了id=1的行,事务B也想锁id=1的行,那B就要等A释放锁,这就是锁等待。
死锁
两个事务互相等对方释放锁,形成了循环等待,谁也动不了。
MySQL会自动检测到死锁,然后回滚其中一个事务。
常见坑
坑1:遇到死锁就重启MySQL
很多同学一遇到死锁就慌了,直接重启MySQL。这是最笨的方法,重启解决不了根本问题,下次还会出现。一定要先排查死锁的原因,从根本上解决。
坑2:以为死锁是MySQL的bug
很多同学觉得死锁是MySQL的问题,其实不是。死锁一般都是业务代码写的有问题,多个事务访问资源的顺序不一致导致的。MySQL只是帮你检测到了死锁,罪魁祸首是你的业务代码。
坑3:死锁日志里的信息看不懂
很多同学拿到死锁日志,里面全是锁的信息,看不懂。其实不用看那么细,重点看两个事务分别执行了什么SQL,操作了哪些表和行,搞清楚它们为什么会互相等就行。
坑4:只靠MySQL自动检测死锁
很多同学觉得MySQL会自动检测死锁、自动回滚,就不用管了。其实不是,死锁虽然不会一直卡住,但是被回滚的那个事务业务上会报错,影响用户体验。最好还是从源头上避免死锁。
坑5:间隙锁导致的死锁
很多同学在可重复读隔离级别下,两个事务往同一个范围插数据,结果死锁了。这就是间隙锁的问题。如果对幻读不是很敏感,降到读提交隔离级别就好了。
总结
MySQL死锁的核心知识点:
- 死锁是什么:两个事务互相持有对方需要的锁,又互相等对方释放,形成循环等待
- 死锁产生的原因:访问资源顺序不一致、事务太长、没用索引锁太多行、间隙锁
- 怎么查看死锁:SHOW ENGINE INNODB STATUS,看LATEST DETECTED DEADLOCK部分
- 怎么排查死锁:看两个事务的SQL、看锁的信息、结合业务逻辑分析
- 怎么避免死锁:
- 按相同的顺序访问资源
- 大事务拆成小事务
- 尽量用索引访问数据
- 降低隔离级别到读提交
- 设置锁等待超时时间
- 死锁和锁等待的区别:锁等待是单向的,等一会儿就好;死锁是循环的,MySQL会自动回滚一个
记住:死锁不可怕,可怕的是不知道为什么会死锁。遇到死锁不要慌,先看死锁日志,找到原因,然后从业务代码上避免。
遇到问题加QQ23979811 协助处理