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压缩解压命令大全,tar、zip、unzip用法
前言
服务器上经常需要压缩文件或者解压上传的压缩包,tar、zip、unzip 这些命令怎么用?本文把最常用的压缩解压命令都整理出来,一次讲清楚。
tar 命令(最常用)
tar 是 Linux 下最常用的打包压缩工具,支持 gzip、bzip2、xz 等多种压缩格式。
参数速记
| 参数 | 含义 |
|---|---|
-c |
创建新的打包文件 |
-x |
解压 |
-z |
用 gzip 压缩(.tar.gz) |
-j |
用 bzip2 压缩(.tar.bz2) |
-J |
用 xz 压缩(.tar.xz) |
-v |
显示过程 |
-f |
指定文件名(必须放最后) |
压缩成 .tar.gz
最常用的,把目录打包成 tar.gz:
tar -zcvf 打包名.tar.gz 要打包的目录
比如把 /var/www/html 打包成 backup.tar.gz:
tar -zcvf backup.tar.gz /var/www/html
解压 .tar.gz
tar -zxvf 文件名.tar.gz
比如:
tar -zxvf backup.tar.gz
解压到指定目录
加 -C 参数:
tar -zxvf backup.tar.gz -C /目标目录
比如解压到 /tmp 目录:
tar -zxvf backup.tar.gz -C /tmp
只看不解压
tar -ztvf 文件名.tar.gz
列出压缩包里的文件列表,不解压。
.tar.bz2 格式
比 gzip 压缩率更高,但更慢:
# 压缩
tar -jcvf backup.tar.bz2 /var/www/html
# 解压
tar -jxvf backup.tar.bz2
.tar.xz 格式
压缩率最高,最慢:
# 压缩
tar -Jcvf backup.tar.xz /var/www/html
# 解压
tar -Jxvf backup.tar.xz
zip 和 unzip
安装
CentOS 最小化安装可能没有 zip/unzip:
yum install -y zip unzip
压缩成 zip
zip -r 打包名.zip 要打包的目录
比如:
zip -r backup.zip /var/www/html
-r 是递归,把目录下所有文件都打进去。
解压 zip
unzip 文件名.zip
比如:
unzip backup.zip
解压到指定目录
unzip 文件名.zip -d /目标目录
比如解压到 /tmp:
unzip backup.zip -d /tmp
只看不解压
unzip -l 文件名.zip
列出 zip 包里的文件列表。
解压到指定目录并覆盖
unzip -o 文件名.zip -d /目标目录
-o 是覆盖,不提示。
其他压缩格式
gz 格式(单文件压缩)
# 压缩
gzip 文件名
# 解压
gunzip 文件名.gz
bz2 格式
# 压缩
bzip2 文件名
# 解压
bunzip2 文件名.bz2
常见坑
坑1:解压后目录结构不对
解压完发现多套了一层目录。
原因: 打包的时候是带路径打的。
解决: 解压前先看一下压缩包里的结构:
tar -ztvf 文件名.tar.gz
或者 zip 用:
unzip -l 文件名.zip
坑2:解压时磁盘空间不足
解压到一半报错 No space left on device。
原因: 磁盘满了。
解决: 先看一下磁盘空间:
df -h
清理空间后再解压。
坑3:tar 参数顺序不对
执行 tar 报错。
原因: -f 参数必须放最后,后面紧跟文件名。
正确写法: tar -zcvf backup.tar.gz /目录
错误写法: tar -fzc backup.tar.gz /目录
坑4:zip 命令找不到
执行 zip 提示 command not found。
原因: 没装 zip 包。
解决:
yum install -y zip unzip
总结
| 格式 | 压缩命令 | 解压命令 |
|---|---|---|
| .tar.gz | tar -zcvf 名.tar.gz 目录 |
tar -zxvf 名.tar.gz |
| .tar.bz2 | tar -jcvf 名.tar.bz2 目录 |
tar -jxvf 名.tar.bz2 |
| .zip | zip -r 名.zip 目录 |
unzip 名.zip |
| .gz | gzip 文件名 |
gunzip 文件名.gz |
最常用的就是 tar -zcvf 压缩 和 tar -zxvf 解压,记住这两个就够用了。
遇到问题加QQ23979811 协助处理
Linux查看系统启动时间,uptime命令详解
前言
想知道服务器从上次开机到现在跑了多久?或者想看系统负载?uptime 命令一行就能搞定。本文详细讲一下这个命令的用法。
uptime 命令
基本用法
直接输入:
uptime
输出类似:
07:49:21 up 2:15, 1 user, load average: 0.00, 0.01, 0.05
输出解释
| 字段 | 含义 |
|---|---|
07:49:21 |
当前系统时间 |
up 2:15 |
系统已经运行了 2 小时 15 分钟 |
1 user |
当前有 1 个用户登录 |
load average: 0.00, 0.01, 0.05 |
1分钟、5分钟、15分钟的平均负载 |
常用参数
显示漂亮一点
uptime -p
输出类似:
up 2 hours, 15 minutes
显示系统启动时间
uptime -s
输出类似:
2026-10-06 05:34:21
这就是系统上次启动的具体时间。
查看系统启动时间的其他方法
who 命令
who -b
输出类似:
system boot 2026-10-06 05:34
last 命令
last reboot | head -5
列出最近几次重启记录:
reboot system boot 3.10.0-1160.el7. Fri Oct 6 05:34 - 07:49 (02:15)
reboot system boot 3.10.0-1160.el7. Thu Oct 5 09:12 - 22:00 (12:47)
systemd-analyze
systemd-analyze
显示系统启动花了多长时间:
Startup finished in 1.234s (kernel) + 2.345s (initrd) + 10.123s (userspace) = 13.702s
负载(load average)怎么看
uptime 最后三个数字是负载,很多人不知道怎么判断高不高。
三个数字的含义
- 第一个:1 分钟平均负载
- 第二个:5 分钟平均负载
- 第三个:15 分钟平均负载
怎么判断高不高
负载和 CPU 核心数有关:
- 负载 < CPU核心数:正常
- 负载 ≈ CPU核心数:刚好饱和
- 负载 > CPU核心数:过载了
查看 CPU 核心数
nproc
输出就是核心数。
比如你的服务器是 4 核 CPU:
- 负载 1.0:很闲
- 负载 4.0:刚好跑满
- 负载 8.0:过载了
常见坑
坑1:负载高但 CPU 不高
负载很高,但 top 看 CPU 使用率不高。
原因: 可能是 I/O 等待高(磁盘读写慢),或者有大量进程在排队。
解决: 用 top 看 %wa 列(I/O 等待),如果高就是磁盘问题。
坑2:up 时间不对
uptime 显示的运行时间和你印象中的不一样。
原因: 系统可能自动重启过,或者你记错了。
解决: 用 last reboot 看历史重启记录。
坑3:1 user 不对
uptime 显示只有 1 个用户,但你明明看到好几个人登录。
原因: 显示的是登录会话数,不是用户数。同一个用户开多个 SSH 连接会算多个会话。
解决: 用 w 命令看详细的在线用户。
总结
| 命令 | 作用 |
|---|---|
uptime |
看运行时间和负载 |
uptime -p |
漂亮格式显示运行时间 |
uptime -s |
显示系统启动时间 |
who -b |
看系统启动时间 |
last reboot |
看历史重启记录 |
nproc |
看 CPU 核心数 |
日常运维用 uptime 一行就能看个大概,很方便。
遇到问题加QQ23979811 协助处理
CentOS7 安装wget、vim、net-tools常用工具包
前言
新装好的 CentOS7 最小化系统,很多常用命令都没有,比如 wget、vim、ifconfig 都用不了。本文就讲一下怎么一次性把常用工具包装上。
为什么要装这些工具
CentOS7 最小化安装(minimal install)为了节省空间,默认不带很多常用工具。刚装完系统你会发现:
wget命令找不到vim命令找不到ifconfig命令找不到netstat命令找不到
所以刚装完系统第一件事,就是把这些常用工具装上。
安装常用工具包
一键安装
最省事的方式,一条命令全装上:
yum install -y wget vim net-tools lrzsz unzip telnet
这一条命令就把最常用的都装了。
各工具说明
| 工具包 | 作用 |
|---|---|
| wget | 命令行下载文件 |
| vim | 文本编辑器 |
| net-tools | 包含 netstat、ifconfig 等网络工具 |
| lrzsz | 包含 rz、sz 上传下载文件 |
| unzip | 解压 zip 文件 |
| telnet | 测试端口连通性 |
分开装
如果你只想装某一个:
# 只装 wget
yum install -y wget
# 只装 vim
yum install -y vim
# 只装 net-tools(ifconfig、netstat)
yum install -y net-tools
常用工具包推荐
基础工具包(必装)
yum install -y wget vim net-tools lrzsz unzip telnet curl
开发工具包
yum groupinstall -y "Development Tools"
这个包包含了 gcc、make 等编译工具,装东西经常需要。
其他常用工具
# 查看进程树
yum install -y psmisc
# 实时监控网络
yum install -y nload
# 查看端口占用
yum install -y lsof
# 查看磁盘性能
yum install -y iotop
# 查看内存
yum install -y htop
常见坑
坑1:ifconfig 命令找不到
执行 ifconfig 提示 command not found。
原因: CentOS7 最小化安装不带 net-tools 包。
解决:
yum install -y net-tools
装完之后 ifconfig 就能用了。
坑2:vim 命令找不到
执行 vi 能用,但 vim 找不到。
原因: 最小化安装只有 vi,没有 vim。
解决:
yum install -y vim
坑3:rz 命令找不到
想从本地上传文件到服务器,执行 rz 提示找不到。
原因: 没装 lrzsz 包。
解决:
yum install -y lrzsz
装完之后:
rz:从本地上传文件到服务器sz 文件名:从服务器下载文件到本地
坑4:yum 安装报错
执行 yum install 的时候报错。
原因: yum 源没配好,或者网络不通。
解决: 先换国内源(参考之前的 yum 源更换教程),然后再装。
总结
新系统刚装完,先跑这一条命令:
yum install -y wget vim net-tools lrzsz unzip telnet curl
把常用工具一次性装齐,后面省得每次都报 command not found。
遇到问题加QQ23979811 协助处理
Linux查看文件大小,du和df命令区别
前言
服务器磁盘满了,不知道哪个目录占的空间最大?du 和 df 是两个最常用的查看磁盘空间的命令,但很多人搞不清它们的区别。本文一次讲清楚。
du 和 df 的区别
du(Disk Usage)
看的是文件和目录占用的磁盘空间。
比如某个目录下有多少文件,总共占多少空间。
df(Disk Free)
看的是磁盘分区的整体使用情况。
比如整个硬盘总共有多大,用了多少,还剩多少。
一句话总结
- du:看文件/目录占了多少
- df:看磁盘分区总共多大、剩多少
df 命令
查看整体磁盘使用情况
df -h
-h 是 human-readable 的意思,输出成 K、M、G 的易读格式。
输出类似:
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 30G 18G 63% /
tmpfs 3.9G 0 3.9G 0% /dev/shm
/dev/sdb1 100G 50G 45G 53% /data
只看某个分区
df -h /
只看根分区的使用情况。
显示文件系统类型
df -Th
多了一列 Type,显示是 ext4、xfs 还是其他类型。
du 命令
查看某个目录占多大空间
du -sh 目录路径
比如看 /var 目录:
du -sh /var
-s 是 summary,只显示总计;-h 是易读格式。
输出类似:
5.2G /var
查看当前目录下每个子目录的大小
du -sh *
输出当前目录下每个子目录占多大空间。
按大小排序
du -sh * | sort -hr
sort -hr 是按数字从大到小排,这样就能一眼看到哪个子目录最大。
找当前目录下最大的10个文件
du -ah . | sort -hr | head -10
-a 是包括文件,不只是目录。
只看一层深度
du -h --max-depth=1 /var
只看 /var 下第一层的子目录大小,不往下钻。
常见场景
场景1:磁盘满了,找大文件
# 先看哪个分区满了
df -h
# 然后去那个分区找大目录
cd /
du -sh * | sort -hr | head -10
# 找到大目录后继续往下钻
cd /var
du -sh * | sort -hr | head -10
一步步找到占空间最大的文件,然后删掉。
场景2:看日志占了多少
du -sh /var/log
场景3:看网站占了多少
du -sh /var/www/html
常见坑
坑1:du 和 df 显示的已用空间不一样
明明删了大文件,但 df 显示还是满的。
原因: 文件被进程占用着,删了之后空间没真正释放。
解决: 找到占用这个文件的进程,重启或者杀掉:
lsof | grep deleted
找到 PID 后重启对应的服务。
坑2:du 命令跑了半天还在跑
du 一个大目录,半天没反应。
原因: 目录太大,文件太多。
解决: 加 --max-depth=1 限制深度,或者先确认一下目录有多大再决定要不要跑。
坑3:df 显示已用空间和 du 对不上
df 显示用了 80G,但 du 加起来才 50G。
原因: 可能有隐藏文件、挂载点下面的文件,或者有进程占用着已删除的文件。
解决: 检查:
# 看有没有挂载点
mount
# 看有没有已删除但还被占用的文件
lsof | grep deleted
坑4:du 显示的大小和 ls 不一样
ls 显示文件是 10M,du 显示是 11M。
原因: du 看的是磁盘块占用,文件系统按块分配空间,实际占用会比文件大小略大一点。
解决: 正常现象,不用纠结。
总结
| 命令 | 作用 |
|---|---|
df -h |
看磁盘分区整体使用情况 |
du -sh 目录 |
看某个目录占多大空间 |
du -sh * | sort -hr |
看当前目录下每个子目录大小并排序 |
du -h --max-depth=1 目录 |
只看一层深度 |
lsof | grep deleted |
看已删除但被进程占用的文件 |
日常运维记住:df 看分区,du 看目录。
遇到问题加QQ23979811 协助处理
Linux文件权限755、644含义,chmod修改权限
前言
Linux 文件权限是新手最头疼的东西之一,经常遇到 403 权限不足的报错。755、644 这些数字到底是什么意思?怎么改?本文一次讲清楚。
权限基础
三种身份
Linux 把用户分三类:
- 文件所有者(u):文件的主人
- 同组用户(g):和主人在同一个组的人
- 其他用户(o):所有人
三种权限
每种身份有三种权限:
- 读(r):4
- 写(w):2
- 执行(x):1
加起来就是权限数字。
755、644 是什么意思
755
7 = rwx(读+写+执行)= 4+2+1
5 = r-x(读+执行)= 4+1
5 = r-x(读+执行)= 4+1
合起来就是:所有者能读写执行,同组和其他用户只能读和执行。
常用于: 目录、可执行脚本
644
6 = rw-(读+写)= 4+2
4 = r--(只读)= 4
4 = r--(只读)= 4
合起来就是:所有者能读写,其他人只能读。
常用于: 普通文件、网页
常用权限速查表
| 数字 | 含义 | 用途 |
|---|---|---|
| 755 | rwxr-xr-x | 目录、脚本 |
| 644 | rw-r--r-- | 普通文件、网页 |
| 700 | rwx------ | 私人目录 |
| 600 | rw------- | 私人文件(比如密钥) |
| 777 | rwxrwxrwx | 所有人都能读写执行(不推荐) |
chmod 修改权限
数字方式(最常用)
chmod 755 文件名
比如把 index.php 改成 644:
chmod 644 index.php
字母方式
chmod u+x 文件名
意思是给所有者(u)加执行权限(x)。
常用的:
u+x:所有者加执行权限u-w:所有者去掉写权限g+w:同组加写权限o+r:其他用户加读权限a+x:所有人加执行权限
递归修改目录下所有文件
加 -R 参数:
chmod -R 755 /var/www/html
警告: 递归改权限要小心,别把系统目录改坏了。
chown 修改所有者
有时候不是权限数字的问题,是文件属主不对。
查看文件属主
ls -l 文件名
输出第一列后面就是属主:
-rw-r--r-- 1 www www 1234 Oct 6 10:00 index.php
前面的 www 是用户,后面的 www 是组。
修改文件属主
chown 用户:组 文件名
比如把文件改成 www 用户:
chown www:www index.php
递归修改目录属主
chown -R www:www /var/www/html
常见坑
坑1:网站 403 权限不足
网页打不开,报 403 Forbidden。
原因: 文件权限不对,或者属主不对。
解决:
- 文件改成 644:
chmod 644 文件 - 目录改成 755:
chmod 755 目录 - 属主改成运行用户:
chown www:www 文件
坑2:777 万能解决?
一遇到权限问题就 chmod 777。
原因: 图省事,但不安全。
解决: 777 意味着所有人都能读写执行,恶意文件可以直接往你的目录里写木马。生产环境绝对不要用 777。
坑3:改了权限还是不行
执行了 chmod 还是报权限不足。
原因: SELinux 拦截了。
解决: 检查 SELinux:
getenforce
如果是 Enforcing,临时关掉试试:
setenforce 0
坑4:上传文件没有执行权限
写了个脚本想运行,报 Permission denied。
原因: 文件没有执行权限。
解决:
chmod +x 脚本.sh
总结
| 命令 | 作用 |
|---|---|
chmod 755 文件名 |
改成 755 权限 |
chmod 644 文件名 |
改成 644 权限 |
chmod -R 755 目录 |
递归改目录权限 |
chown www:www 文件名 |
改文件属主 |
chown -R www:www 目录 |
递归改目录属主 |
记住两个常用的:目录 755,文件 644,基本就够用了。
遇到问题加QQ23979811 协助处理
Linux创建用户、删除用户、修改密码完整命令
前言
服务器上要加新用户、删旧用户、改密码,这些都是日常操作。本文把常用的用户管理命令都整理出来,新手也能照着做。
创建用户
useradd 命令
最简单的创建用户:
useradd 用户名
比如创建一个叫 zhangsan 的用户:
useradd zhangsan
创建完之后,这个用户还不能登录,因为没设密码。
创建用户并设密码
一行搞定:
useradd zhangsan && passwd zhangsan
执行 passwd zhangsan 之后会提示输入两次密码。
创建用户并指定家目录
默认家目录是 /home/用户名,想改的话用 -d:
useradd -d /data/zhangsan zhangsan
创建用户并指定用户组
useradd -g 用户组 用户名
比如把 zhangsan 加到 www 组:
useradd -g www zhangsan
创建用户并指定 UID
useradd -u 1001 zhangsan
修改密码
passwd 命令
修改某个用户的密码:
passwd 用户名
比如修改 zhangsan 的密码:
passwd zhangsan
执行后会提示输入两次新密码。
自己改密码
passwd
直接执行,会提示输入旧密码,然后输入新密码两次。
免交互改密码(脚本用)
写脚本的时候不能交互式输入,用这个:
echo "新密码" | passwd --stdin 用户名
比如:
echo "123456" | passwd --stdin zhangsan
删除用户
userdel 命令
只删用户,不删家目录:
userdel 用户名
比如:
userdel zhangsan
删除用户并删家目录
加 -r 参数,把家目录也删掉:
userdel -r zhangsan
警告: -r 会把 /home/zhangsan 整个删掉,里面的文件都没了,确认清楚再执行。
查看用户信息
查看用户 UID、GID、组
id 用户名
比如:
id zhangsan
输出类似:
uid=1001(zhangsan) gid=1001(zhangsan) 组=1001(zhangsan)
查看用户信息
grep 用户名 /etc/passwd
查看所有用户
cat /etc/passwd
每行一个用户,格式是:
用户名:x:UID:GID:注释:家目录:登录Shell
常见坑
坑1:创建用户后不能登录
创建了用户但 SSH 登录不了。
原因: 没设密码,或者 Shell 是 /sbin/nologin。
解决:
- 先设密码:
passwd 用户名 - 检查 Shell:
grep 用户名 /etc/passwd,最后一列应该是/bin/bash
如果是 /sbin/nologin,改回来:
usermod -s /bin/bash 用户名
坑2:删了用户但家目录还在
执行了 userdel zhangsan,但 /home/zhangsan 还在。
原因: 没加 -r 参数。
解决: 手动删:
rm -rf /home/zhangsan
坑3:普通用户没权限 sudo
新用户不能用 sudo。
原因: 没加到 sudoers 里。
解决: 加到 wheel 组(CentOS)或者 sudo 组(Ubuntu):
usermod -aG wheel 用户名
坑4:密码过期了
用户突然不能登录了,提示密码过期。
解决: root 改密码:
passwd 用户名
或者设置密码永不过期:
chage -M 99999 用户名
总结
| 操作 | 命令 |
|---|---|
| 创建用户 | useradd 用户名 |
| 设密码 | passwd 用户名 |
| 删除用户 | userdel 用户名 |
| 删除用户+家目录 | userdel -r 用户名 |
| 查看用户信息 | id 用户名 |
| 加 sudo 权限 | usermod -aG wheel 用户名 |
日常运维就这几个命令,够用了。
遇到问题加QQ23979811 协助处理
CentOS7 yum安装软件报错,yum源更换教程
前言
CentOS7 默认的 yum 源是国外的,装软件特别慢,有时候还报错 "Could not resolve host" 或者 "Failed to connect"。换个国内源就好了,速度飞快。本文就讲一下怎么换 yum 源。
报错现象
执行 yum install xxx 的时候,报类似这样的错:
Could not retrieve mirrorlist http://mirrorlist.centos.org/...
或者:
Could not resolve host: mirrorlist.centos.org
或者下载速度特别慢,几 KB/s。
原因
默认 yum 源是 CentOS 官方的,服务器在国外,国内访问慢。有时候官方源还会挂。
更换国内源
第一步:备份原来的源
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
第二步:下载阿里源
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
或者用 wget:
wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
第三步:清理缓存并重建
yum clean all
yum makecache
第四步:测试
yum install -y wget
应该秒装完成。
其他国内源
清华源
curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.tuna.tsinghua.edu.cn/repo/Centos-7.repo
网易源
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.163.com/.help/CentOS7-Base-163.repo
中科大源
curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.ustc.edu.cn/repo/Centos-7.repo
安装 EPEL 源
EPEL 源是 Extra Packages for Enterprise Linux,很多常用软件(比如 nginx)都在 EPEL 里。
yum install epel-release -y
装完之后也可以换成阿里的 EPEL 源:
mv /etc/yum.repos.d/epel.repo /etc/yum.repos.d/epel.repo.backup
curl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
yum clean all
yum makecache
常见坑
坑1:换完源还是慢
换了阿里源还是慢。
原因: DNS 解析慢,或者服务器本身带宽小。
解决: 换个源试试(清华、网易),或者检查 DNS:
cat /etc/resolv.conf
坑2:yum makecache 报错
执行 yum makecache 的时候报错。
原因: 源文件没下好,或者网络不通。
解决: 检查源文件内容对不对:
cat /etc/yum.repos.d/CentOS-Base.repo
或者重新下一遍。
坑3:CentOS7 已经 EOL 了
CentOS7 官方已经停服了(2024年6月30日),官方源已经不能用了。
解决: 用 vault 源(存档源):
阿里 vault 源:
curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-7.repo
坑4:404 Not Found
装软件的时候报 404。
原因: 源文件里的路径不对,或者软件包被删了。
解决: 换个源,或者清理缓存:
yum clean all
yum makecache
总结
换 yum 源就三步:
- 备份:
mv /etc/yum.repos.d/CentOS-Base.repo 备份 - 下载新源:curl 下载阿里/清华源
- 清缓存:
yum clean all && yum makecache
国内服务器一定要换国内源,不然装软件能急死。
遇到问题加QQ23979811 协助处理
PHP 会话管理详解
PHP 会话管理详解
Session 和 Cookie 是 PHP 做用户登录、状态保持的基础,搞懂原理才不会出问题。
1. Cookie 和 Session 的区别
Cookie:存在客户端浏览器里,每次请求自动带在请求头里。
- 大小限制 4KB
- 不安全,用户可以改
- 适合存不敏感的数据
Session:存在服务器端,客户端只存一个 session_id。
- 大小没有限制
- 更安全
- 适合存用户登录信息
2. Cookie 基本用法
// 设置 Cookie(7天过期)
setcookie('user_id', '123', time() + 3600 * 24 * 7);
// 读取 Cookie
echo $_COOKIE['user_id'];
// 删除 Cookie(过期时间设为过去)
setcookie('user_id', '', time() - 3600);
3. Session 基本用法
// 开启 Session(必须在最前面)
session_start();
// 设置 Session
$_SESSION['user_id'] = 123;
$_SESSION['user_name'] = '张三';
// 读取 Session
echo $_SESSION['user_name'];
// 删除单个 Session
unset($_SESSION['user_id']);
// 销毁所有 Session
session_destroy();
4. Session 工作原理
- 用户第一次访问,PHP 生成唯一 session_id
- 把 session_id 通过 Cookie 发给浏览器
- 浏览器下次访问自动带上这个 Cookie
- 服务器根据 session_id 找到对应 Session 数据
5. Session 配置
php.ini 里常用配置:
; Session 保存路径
session.save_path = "/tmp"
; Session 过期时间(分钟)
session.gc_maxlifetime = 1440
; Cookie 有效期(0 表示浏览器关闭就过期)
session.cookie_lifetime = 0
6. 常见问题
1)Session 丢失:
- 没有 session_start()
- Cookie 被禁用
- session 文件目录权限不对
2)多台服务器 Session 共享:
- 默认 Session 存在本地文件,多台服务器不共享
- 解决方案:存在 Redis 里
// Session 存 Redis
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://127.0.0.1:6379');
7. 安全建议
- 登录成功后重新生成 session_id,防止会话固定攻击
- 敏感数据不要存在 Cookie 里
- Session 过期时间不要太长
- HTTPS 下设置 Cookie 的 secure 属性
// 登录成功后重新生成 ID
session_regenerate_id(true);
总结
PHP 会话管理核心:
- Cookie 存客户端,Session 存服务器
- Session 靠 Cookie 里的 session_id 识别用户
- 多服务器部署 Session 存 Redis
- 登录后重新生成 session_id 防攻击
搞懂这些,用户登录功能就不会出问题了。
Linux查看进程占用内存最高,kill进程方法
前言
服务器内存用满了,不知道哪个进程占的?找到之后怎么杀掉?本文就讲一下怎么查看占用内存最高的进程,以及怎么 kill 掉。
查看内存占用
top 命令(最常用)
top
打开后默认按 CPU 排序,按 M 键(大写)就会按内存排序。
输出里重点看:
PID:进程号USER:运行用户%MEM:内存占用百分比RES:实际用的物理内存(KB)
按 q 退出。
ps 命令
一行命令直接看内存占用前 10:
ps aux --sort=-%mem | head -10
输出按内存从大到小排,前 10 个就是最占内存的。
只看进程名和 PID
ps aux --sort=-%mem | head -10 | awk '{print $2, $11}'
只显示 PID 和进程名,更简洁。
kill 杀掉进程
kill 命令
先从 top 或 ps 里找到进程 PID,然后:
kill 进程号
比如杀掉 PID 为 1234 的进程:
kill 1234
kill -9 强制杀
有时候 kill 没用,进程就是不死,用 -9 强制杀:
kill -9 1234
注意: -9 是强杀,进程没机会清理资源,可能导致数据丢失。尽量先试普通 kill,不行再 -9。
pkill 按名字杀
不用找 PID,直接按进程名杀:
pkill nginx
杀掉所有叫 nginx 的进程。
killall 杀同名进程
killall nginx
和 pkill 类似,也是按名字杀。
常用操作示例
找出最占内存的进程并杀掉
# 找前5个最占内存的
ps aux --sort=-%mem | head -5
# 假设 PID 是 1234,杀掉
kill 1234
杀掉所有 php-fpm 进程
pkill php-fpm
杀掉某个用户的所有进程
pkill -u www
常见坑
坑1:杀错进程了
把系统关键进程杀了,服务器直接挂。
解决: 杀之前先确认一下这个进程是干嘛的:
ps -p 进程号 -o comm=
或者:
ls -l /proc/进程号/exe
坑2:kill 了进程又自动起来了
杀掉之后过几秒又自动启动了。
原因: 有服务管理工具(systemd、supervisor)在监控,挂了就自动拉起。
解决: 先停服务再杀:
systemctl stop nginx
或者:
supervisorctl stop nginx
坑3:kill 没权限
普通用户杀不了 root 启动的进程。
解决: 加 sudo 或切 root:
sudo kill 1234
总结
| 命令 | 作用 |
|---|---|
top |
实时看进程,按 M 按内存排序 |
ps aux --sort=-%mem |
按内存排序看进程 |
kill 进程号 |
正常杀进程 |
kill -9 进程号 |
强制杀进程 |
pkill 进程名 |
按名字杀进程 |
killall 进程名 |
杀同名所有进程 |
服务器内存爆了就用 top 按 M 排序,找到最占内存的杀掉就行。
遇到问题加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 协助处理
Redis 缓存雪崩解决
Redis 缓存雪崩解决方案
缓存雪崩是 Redis 最常见的线上事故之一,搞懂怎么预防,关键时刻能救命。
1. 什么是缓存雪崩
大量缓存同时过期,或者 Redis 整个挂了,所有请求都打到数据库,数据库瞬间压力爆炸,直接崩溃。
就像雪崩一样,一层压一层,全完了。
2. 和缓存穿透、击穿的区别
穿透:查不存在的数据,缓存挡不住,一直打数据库。
击穿:一个热点 key 突然过期,瞬间大量请求打数据库。
雪崩:大量 key 同时过期,或者 Redis 挂了,整个数据库被打垮。
3. 解决方案一:过期时间加随机值
最常用的方法。不要所有 key 都设一样的过期时间,在基础时间上加随机数:
// 不好:所有 key 都 1 小时过期
$redis->setex('product:1', 3600, $data);
// 好:基础 1 小时,加 0-600 秒随机
$ttl = 3600 + rand(0, 600);
$redis->setex('product:1', $ttl, $data);
这样过期时间分散开,不会一起失效。
4. 解决方案二:热点数据永不过期
热点数据不要主动设过期,后台异步更新:
// 商品详情这种热点数据,不设过期
$redis->set('product:1', $data);
// 后台定时任务更新
// 不依赖过期时间,由程序控制刷新
5. 解决方案三:Redis 高可用
Redis 自己要高可用,不能单点:
- 主从架构 + 哨兵,主挂了自动切从
- Redis Cluster 集群,分片存储,一个节点挂了不影响全局
# Redis Sentinel 哨兵配置
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
6. 解决方案四:熔断降级
数据库撑不住的时候,要能自动降级:
- 限流:用令牌桶、漏桶算法,限制请求量
- 降级:返回默认数据或者"系统繁忙"
- 熔断:数据库连续报错多了,直接不访问数据库
常用工具:Sentinel、Hystrix。
7. 解决方案五:多级缓存
不要只靠 Redis 一层缓存:
用户请求 → 浏览器缓存 → CDN → Nginx 本地缓存 → Redis → 数据库
多级缓存,就算 Redis 挂了,前面还有 Nginx 缓存挡着。
8. 应急处理
真出问题了怎么办:
- 先限流,别让数据库被打死
- 恢复 Redis 服务
- 重新预热热点数据
- 慢慢放开流量
总结
缓存雪崩解决方案:
- 过期时间加随机值,别一起失效
- 热点数据永不过期,后台更新
- Redis 做高可用集群
- 加熔断限流兜底
- 多级缓存层层防护
记住这几点,缓存雪崩就不怕了。
CentOS7重启网络报错,network与NetworkManager冲突处理
前言
CentOS7 里有两套网络管理工具:传统的 network 服务和新的 NetworkManager。两个同时用就会打架,重启网络的时候报错,甚至把网络搞断。本文就讲一下这个冲突怎么解决。
报错现象
执行 systemctl restart network 或者 service network restart 的时候,报类似这样的错:
Restarting network (via systemctl): Job for network.service failed. See 'systemctl status network.service' and 'journalctl -xn' for details.
或者:
RTNETLINK answers: File exists
原因
CentOS7 默认装了 NetworkManager(简称 NM),它也在管理网络接口。传统的 network 服务也在管。两个同时管同一个网卡,配置就会冲突。
解决方法
方法一:关掉 NetworkManager(推荐服务器用)
服务器一般不需要图形界面的网络管理工具,直接关掉:
systemctl stop NetworkManager
systemctl disable NetworkManager
然后重启 network:
systemctl restart network
方法二:让 network 忽略 NetworkManager
编辑网络配置文件:
vi /etc/sysconfig/network-scripts/ifcfg-eth0
在文件里加一行:
NM_CONTROLLED=no
意思是这个网卡不归 NetworkManager 管,只由 network 服务管理。
然后重启网络:
systemctl restart network
方法三:全部交给 NetworkManager 管
如果你想用 NM 管理,那就把传统 network 关掉:
systemctl stop network
systemctl disable network
然后用 NM 的命令管理网络:
nmcli connection show
nmcli connection up eth0
验证
重启网络之后,看看网卡 IP 还在不在:
ip addr
如果 IP 还在,能 ping 通外网,就说明搞定了。
常见坑
坑1:改了配置把自己踢下线
远程 SSH 连上去改网络配置,改完重启网络,结果 SSH 断了,再也连不上。
解决:
- 改配置之前先开个备用 SSH 连接,别关
- 或者直接用云服务器控制台的 VNC 操作
- 不要在生产环境随便重启网络
坑2:重启服务器后网络又坏了
临时重启 network 好了,但重启服务器后又不行了。
原因: NetworkManager 又自动启动了,和 network 冲突。
解决: 把 NetworkManager 禁用掉:
systemctl disable NetworkManager
坑3:网卡名不是 eth0
新一点的系统网卡名可能是 ens33、enp0s3 这种。
解决: 先看一下网卡叫什么:
ip addr
然后改对应的配置文件,比如 ifcfg-ens33。
总结
CentOS7 网络冲突的解决思路很简单:
| 方案 | 操作 | 适用场景 |
|---|---|---|
| 关 NM | systemctl disable NetworkManager |
服务器,推荐 |
| NM_CONTROLLED=no | 网卡配置里加这行 | 只用 network 服务 |
| 全用 NM | 关掉 network,用 nmcli | 桌面环境 |
服务器上推荐直接关掉 NetworkManager,用传统的 network 服务管理网络,简单稳定。
遇到问题加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 协助处理