作者: zhengadmin

  • Redis 数据库介绍和环境准备

    Redis 启动和连接

    Redis 服务启动

    使用 systemd 管理

    
    [root@localhost ~]# ls -l /usr/lib/systemd/system/redis.service
    # 重新加载 systemd
    systemctl daemon-reload
    # 启动 Redis 服务
    systemctl start redis
    # 设置开机自启
    systemctl enable redis
    # 查看服务状态
    systemctl status redis

    手动启动

    # 前台启动(用于调试)
    redis-server /etc/redis/redis.conf
    # 后台启动
    redis-server /etc/redis/redis.conf --daemonize yes
    # 指定端口启动
    redis-server --port 6380
    # 指定配置参数启动
    redis-server --maxmemory 1gb --maxmemory-policy allkeys-lru

    image-20261017091917034

    image-20261017092204354

    Redis 客户端连接

    本地连接

    # 默认连接
    redis-cli
    # 指定主机和端口
    redis-cli -h 127.0.0.1 -p 6379
    # 使用密码连接
    redis-cli -h 127.0.0.1 -p 6379 -a your_password
    # 连接后认证
    redis-cli
    127.0.0.1:6379> AUTH your_password
    OK
    # 选择数据库
    127.0.0.1:6379> SELECT 1
    OK
    127.0.0.1:6379[1]>

    远程连接配置

    # 服务器端配置
    # 修改配置文件
    vim /etc/redis/redis.conf
    # 修改绑定地址
    bind 0.0.0.0
    # 设置密码
    requirepass your_strong_password
    # 重启服务
    systemctl restart redis
    
    # 客户端连接
    # 远程连接
    redis-cli -h 192.168.1.100 -p 6379 -a your_password

    Redis 客户端连接

    分为两大块:本地连接 和 远程连接配置。

    一、本地连接(本机连本机 Redis)

    前提:Redis 服务已经在本机启动,默认端口 6379

    # 1. 默认最简连接,本地 127.0.0.1:6379
    redis-cli
    # 2. 手动指定主机和端口(写法更明确)
    redis-cli -h 127.0.0.1 -p 6379
    # 3. 连接时直接带上密码
    redis-cli -h 127.0.0.1 -p 6379 -a your_password
    • -a:auth 认证密码,your_password替换成你自己设置的密码
    # 4. 先连上redis,之后再输入密码认证
    redis-cli
    127.0.0.1:6379> AUTH your_password
    OK

    ==两种密码方式区别==:

    • -a:连接一次性带入密码
    • AUTH:登录到 redis 命令行之后,再做身份校验
    # 5. 切换数据库
    127.0.0.1:6379> SELECT 1
    OK
    127.0.0.1:6379[1]>

    Redis 默认有16 个数据库(0~15),默认在 db0;SELECT 数字切换数据库。 提示符末尾 [1] 代表当前在db1 库。

    Redis 内置 16 个逻辑数据库,编号 0 ~ 15 SELECT N,N 就是库编号:

    • SELECT 0 → 切换到 db0(默认库,新连接上来默认就在 db0)
    • SELECT 1 → 切换到 db1
    • …
    • SELECT 15 → 切换到 db15

    提示符变化:

    127.0.0.1:6379>        # 当前db0,不显示编号
    127.0.0.1:6379[1]>     # 当前db1,[数字]就是数据库编号

    ⚠️ 重点:这 16 个只是逻辑分区,不是独立数据库

    1. 所有 db共用同一套 Redis 服务、内存、RDB/AOF 持久化
    2. 只是 key 的命名空间隔离:db0 里的 key 和 db1 里同名 key 互不干扰,不能做用户权限隔离
    3. Redis 官方其实不推荐用多个 db 做业务隔离,更推荐单实例单 db,不同业务直接部署独立 Redis 实例
    4. FLUSHDB:清空当前选中的 db;FLUSHALL:一次性清空全部 16 个 db

    举个例子:

    # 默认db0
    127.0.0.1:6379> set name zhangsan
    OK
    
    127.0.0.1:6379> SELECT 1
    OK
    127.0.0.1:6379[1]> get name
    (nil)    # db1里没有这个key
    
    127.0.0.1:6379[1]> SELECT 0
    OK
    127.0.0.1:6379> get name
    "zhangsan" # 切回db0,key又回来了

    补充小知识点

    这个 16 库的数量可以在 redis.conf 修改:

    databases 16

    改完重启 Redis 就会变成对应数量,但一般没人去改。

    一句话总结:SELECT 数字就是切换逻辑 db0~15,只是 key 隔离,不是真正独立数据库。

    image-20261017101243899


    二、远程连接配置(别的机器访问这台 Redis)

    ⚠️ 默认 Redis 只允许本机访问,想要外部电脑连接,需要修改 redis 配置文件

    1. 服务端(Redis 所在服务器操作)

    # 编辑redis配置文件
    vim /etc/redis/redis.conf

    ① 修改绑定地址

    bind 0.0.0.0
    • 默认是 bind 127.0.0.1:只允许本机访问
    • bind 0.0.0.0:允许任意 IP访问(生产环境注意安全)

    ② 设置访问密码

    requirepass your_strong_password

    ==去掉前面注释#,your_strong_password改成你自定义强密码==

    ③ 重启 redis,让配置生效

    systemctl restart redis

    ✅ 额外注意(容易踩坑)

    1. 服务器防火墙要放行 6379 端口,否则外部连不上
    2. 云服务器(阿里云 / 腾讯云)还要在安全组开放 6379 端口
    3. 生产环境不建议直接 bind 0.0.0.0,尽量限制指定 IP

    2. 客户端(其他电脑远程连接)

    redis-cli -h 192.168.226.10 -p 6379 -a your_password
    • 192.168.226.10:Redis 服务器的 IP 地址
    • -p 6379:Redis 端口
    • -a:Redis 配置里设置的密码

    image-20261017100321031

    image-20261017100423137


    总结

    1. 远程连不上:优先检查 bind 配置、防火墙、云服务器安全组
    2. AUTH 认证失败:密码写错,区分大小写
    3. SELECT 切换库只对当前连接生效,新连接默认回到 db0
    4. redis-cli 是 Redis 自带命令行客户端,Java/Python 是另外的编程语言客户端

    Redis 配置文件

    配置文件详解

    Redis 的主配置文件通常位于 /etc/redis/redis.conf,包含了所有的配置选项。

    配置文件结构:

    # Redis 配置文件主要部分
    1. 网络配置 (NETWORK)
    2. 通用配置 (GENERAL)
    3. 快照配置 (SNAPSHOTTING)
    4. 复制配置 (REPLICATION)
    5. 安全配置 (SECURITY)
    6. 客户端配置 (CLIENTS)
    7. 内存管理 (MEMORY MANAGEMENT)
    8. 惰性释放 (LAZY FREEING)
    9. 线程 I/O (THREADED I/O)
    10. 内核透明大页 (KERNEL TRANSPARENT HUGEPAGE)
    11. 追加模式 (APPEND ONLY MODE)
    12. LUA 脚本 (LUA SCRIPTING)
    13. Redis 集群 (REDIS CLUSTER)
    14. 慢日志 (SLOW LOG)
    15. 延迟监控 (LATENCY MONITOR)
    16. 事件通知 (EVENT NOTIFICATION)
    17. 高级配置 (ADVANCED CONFIG)

    常用配置参数

    网络配置

    # 绑定地址
    bind 127.0.0.1 ::1
    # 允许所有地址访问(生产环境需谨慎)
    # bind 0.0.0.0
    
    # 端口号
    port 6379
    
    # TCP 监听队列长度
    tcp-backlog 511
    
    # 客户端空闲超时时间(秒)
    timeout 0
    
    # TCP keepalive
    tcp-keepalive 300

    通用配置

    # 以守护进程方式运行
    daemonize yes
    
    # 进程文件
    pidfile /var/run/redis_6379.pid
    
    # 日志级别:debug, verbose, notice, warning
    loglevel notice
    
    # 日志文件
    logfile /var/log/redis/redis-server.log
    
    # 数据库数量
    databases 16
    
    # 显示 Redis logo
    always-show-logo no

    内存管理

    # 最大内存限制
    maxmemory 2gb
    
    # 内存淘汰策略
    # noeviction: 不淘汰,内存满时报错
    # allkeys-lru: 所有键 LRU 淘汰
    # volatile-lru: 有过期时间的键 LRU 淘汰
    # allkeys-random: 所有键随机淘汰
    # volatile-random: 有过期时间的键随机淘汰
    # volatile-ttl: 有过期时间的键按 TTL 淘汰
    # allkeys-lfu: 所有键 LFU 淘汰
    # volatile-lfu: 有过期时间的键 LFU 淘汰
    maxmemory-policy allkeys-lru
    
    # LRU 和 LFU 算法样本数量
    maxmemory-samples 5

    安全配置

    密码认证

    # 设置密码
    requirepass your_strong_password_here
    
    # 重命名危险命令
    rename-command FLUSHDB ""
    rename-command FLUSHALL ""
    rename-command KEYS ""
    rename-command CONFIG "CONFIG_9a8b7c6d5e4f"

    ACL 用户管理

    # 启用 ACL 日志
    acllog-max-len 128
    
    # ACL 配置文件
    # aclfile /etc/redis/users.acl
    
    # 示例 ACL 配置
    # user default on nopass ~* &* -@all +@read +@write
    # user app_user on >app_password ~app:* +@read +@write -@dangerous
    # user readonly_user on >readonly_password ~* +@read -@write -@dangerous

    网络安全

    # 保护模式(默认开启)
    protected-mode yes
    
    # 绑定到特定接口
    bind 127.0.0.1 192.168.1.100
    
    # 禁用某些命令
    rename-command DEBUG ""
    rename-command EVAL ""
    rename-command SCRIPT ""

    性能调优配置

    持久化优化

    # RDB 配置
    save 900 1      # 900秒内至少1个键变化
    save 300 10     # 300秒内至少10个键变化
    save 60 10000   # 60秒内至少10000个键变化
    
    # RDB 文件压缩
    rdbcompression yes
    
    # RDB 文件校验
    rdbchecksum yes
    
    # RDB 文件名
    dbfilename dump.rdb
    
    # 工作目录
    dir /var/lib/redis
    
    # AOF 配置
    appendonly yes
    appendfilename "appendonly.aof"
    
    # AOF 同步策略
    # always: 每个写操作都同步
    # everysec: 每秒同步一次
    # no: 由操作系统决定
    appendfsync everysec
    
    # AOF 重写配置
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb

    客户端连接优化

    # 最大客户端连接数
    maxclients 10000
    
    # 客户端输出缓冲区限制
    # client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
    client-output-buffer-limit normal 0 0 0
    client-output-buffer-limit replica 256mb 64mb 60
    client-output-buffer-limit pubsub 32mb 8mb 60
    
    # 客户端查询缓冲区限制
    client-query-buffer-limit 1gb

    慢查询配置

    # 慢查询阈值(微秒)
    slowlog-log-slower-than 10000
    
    # 慢查询日志长度
    slowlog-max-len 128

    实践操作

    需求描述

    在生产环境中,我们经常需要在同一台服务器上运行多个Redis实例,用于不同的业务场景或实现数据隔离。本实践将演示如何通过自定义配置文件部署多个Redis实例,包括主实例、缓存实例和会话实例。

    # 1. 创建多实例目录结构
    mkdir -p /etc/redis/instances/{main,cache,session}
    mkdir -p /var/lib/redis/{main,cache,session}
    mkdir -p /var/log/redis
    
    # 2. 创建主实例配置文件 (端口6380)
    vim /etc/redis/instances/main/redis.conf 
    # Redis 主实例配置 - 用于核心业务数据
    port 6380
    bind 127.0.0.1
    daemonize yes
    pidfile /var/run/redis/redis-main.pid
    logfile /var/log/redis/redis-main.log
    dir /var/lib/redis/main
    dbfilename dump-main.rdb
    
    # 内存配置
    maxmemory 1gb
    maxmemory-policy allkeys-lru
    
    # 持久化配置
    save 900 1
    save 300 10
    save 60 10000
    appendonly yes
    appendfilename "appendonly-main.aof"
    
    # 安全配置
    requirepass main_redis_2025
    rename-command FLUSHDB ""
    rename-command FLUSHALL ""
    
    # 客户端配置
    maxclients 1000
    timeout 300
    
    
    # 3. 创建缓存实例配置文件 (端口6381)
    vim  /etc/redis/instances/cache/redis.conf 
    # Redis 缓存实例配置 - 用于应用缓存
    port 6381
    bind 127.0.0.1
    daemonize yes
    pidfile /var/run/redis/redis-cache.pid
    logfile /var/log/redis/redis-cache.log
    dir /var/lib/redis/cache
    dbfilename dump-cache.rdb
    
    # 内存配置 - 缓存实例分配更多内存
    maxmemory 2gb
    maxmemory-policy allkeys-lru
    maxmemory-samples 10
    
    # 持久化配置 - 缓存数据可以不持久化
    save ""
    appendonly no
    
    # 安全配置
    requirepass cache_redis_2025
    
    # 客户端配置
    maxclients 2000
    timeout 0
    
    # 过期键删除配置
    hz 10
    
    
    # 4. 创建会话实例配置文件 (端口6382)
    vim  /etc/redis/instances/session/redis.conf 
    # Redis 会话实例配置 - 用于用户会话存储
    port 6382
    bind 127.0.0.1
    daemonize yes
    pidfile /var/run/redis/redis-session.pid
    logfile /var/log/redis/redis-session.log
    dir /var/lib/redis/session
    dbfilename dump-session.rdb
    
    # 内存配置
    maxmemory 512mb
    maxmemory-policy volatile-lru
    
    # 持久化配置 - 会话数据需要持久化但频率可以低一些
    save 1800 1
    save 300 100
    appendonly yes
    appendfilename "appendonly-session.aof"
    auto-aof-rewrite-percentage 100
    auto-aof-rewrite-min-size 64mb
    
    # 安全配置
    requirepass session_redis_2025
    
    # 客户端配置
    maxclients 500
    timeout 1800
    
    # 键空间通知 - 用于会话过期监控
    notify-keyspace-events Ex
    
    
    # 5. 创建运行时目录
    mkdir -p /var/run/redis
    chown redis:redis /var/run/redis
    chown -R redis:redis /var/lib/redis
    chown -R redis:redis /var/log/redis
    chown -R redis:redis /etc/redis/instances
    
    # 6. 创建systemd服务文件
    # 主实例服务
    vim  /etc/systemd/system/redis-main.service 
    [Unit]
    Description=Redis Main Instance
    After=network.target
    
    [Service]
    Type=notify
    ExecStart=/usr/bin/redis-server /etc/redis/instances/main/redis.conf --supervised systemd
    ExecStop=/usr/bin/redis-cli -p 6380 -a main_redis_2025 shutdown
    TimeoutStopSec=0
    Restart=always
    User=redis
    Group=redis
    RuntimeDirectory=redis
    RuntimeDirectoryMode=0755
    
    [Install]
    WantedBy=multi-user.target
    
    
    # 缓存实例服务
    vim  /etc/systemd/system/redis-cache.service 
    [Unit]
    Description=Redis Cache Instance
    After=network.target
    
    [Service]
    Type=notify
    ExecStart=/usr/bin/redis-server /etc/redis/instances/cache/redis.conf --supervised systemd
    ExecStop=/usr/bin/redis-cli -p 6381 -a cache_redis_2025 shutdown
    TimeoutStopSec=0
    Restart=always
    User=redis
    Group=redis
    RuntimeDirectory=redis
    RuntimeDirectoryMode=0755
    
    [Install]
    WantedBy=multi-user.target
    
    
    # 会话实例服务
    vim  /etc/systemd/system/redis-session.service  
    [Unit]
    Description=Redis Session Instance
    After=network.target
    
    [Service]
    Type=notify
    ExecStart=/usr/bin/redis-server /etc/redis/instances/session/redis.conf --supervised systemd
    ExecStop=/usr/bin/redis-cli -p 6382 -a session_redis_2025 shutdown
    TimeoutStopSec=0
    Restart=always
    User=redis
    Group=redis
    RuntimeDirectory=redis
    RuntimeDirectoryMode=0755
    
    [Install]
    WantedBy=multi-user.target
    
    
    # 7. 重新加载systemd并启动服务
     systemctl daemon-reload
     systemctl enable redis-main redis-cache redis-session
     systemctl start redis-main redis-cache redis-session
    
    # 检查服务状态
    systemctl status redis-main redis-cache redis-session --no-pager
    
    # 检查端口监听
    sudo netstat -tlnp | grep redis-server
    
    # 检查进程
    ps aux | grep redis-server | grep -v grep
    
    # 测试主实例
    redis-cli -p 6380 -a main_redis_2025 ping
    redis-cli -p 6380 -a main_redis_2025 set main:test "Main Instance Data"
    redis-cli -p 6380 -a main_redis_2025 get main:test
    
    # 测试缓存实例
    redis-cli -p 6381 -a cache_redis_2025 ping
    redis-cli -p 6381 -a cache_redis_2025 set cache:user:1001 "User Cache Data" EX 3600
    redis-cli -p 6381 -a cache_redis_2025 get cache:user:1001
    redis-cli -p 6381 -a cache_redis_2025 ttl cache:user:1001
    
    # 测试会话实例
    redis-cli -p 6382 -a session_redis_2025 ping
    redis-cli -p 6382 -a session_redis_2025 set session:user:1001 "User Session Data" EX 1800
    redis-cli -p 6382 -a session_redis_2025 get session:user:1001
    redis-cli -p 6382 -a session_redis_2025 ttl session:user:1001
    
    # 检查内存使用情况 - used_memory_human
    redis-cli -p 6380 -a main_redis_2025 info memory | grep used_memory_human
    redis-cli -p 6381 -a cache_redis_2025 info memory | grep used_memory_human
    redis-cli -p 6382 -a session_redis_2025 info memory | grep used_memory_human
    
    # 检查配置信息 - maxmemory
    redis-cli -p 6380 -a main_redis_2025 config get maxmemory
    redis-cli -p 6381 -a cache_redis_2025 config get maxmemory
    redis-cli -p 6382 -a session_redis_2025 config get maxmemory

    主实例配置—-用于核心业务数据

    image-20261017102659513

    缓存实例配置—-用于应用缓存

    image-20261017102912648

    会话实例配置—用于应户会话存储

    image-20261017103030376

    ​ 创建运行时目录

    image-20261017103822654

    创建systemd服务文件–主实例服务

    image-20261017104006385

    创建缓存实例服务

    image-20261017104115994

    创建会话实例服务

    image-20261017104306943

    重启加载systemd并启动服务

    image-20261017104547512


    Redis 多实例部署

    (一台机器跑 3 套独立 Redis:main 主库、cache 缓存、session 会话)

    ==原理:单台服务器,启动多个独立 redis-server 进程,各自占用不同端口、独立配置、独立数据目录、独立日志==。 三个实例端口:6380(main)、6381(cache)、6382(session)

    一、第 1 部分:创建目录结构

    mkdir -p /etc/redis/instances/{main,cache,session}
    mkdir -p /var/lib/redis/{main,cache,session}
    mkdir -p /var/log/redis
    • /etc/redis/instances/xxx:每个实例单独放自己的 redis.conf 配置文件
    • /var/lib/redis/xxx:RDB/AOF 持久化数据文件目录(每个实例数据隔离)
    • /var/log/redis:统一存放三个实例日志

    二、第 2~4 部分:分别写 3 份 redis.conf 配置(重点对比)

    注意脚本里写了 bind 127.0.0.1,代表只允许本机访问,外部机器不能远程连接;如果你想要别的服务器连,要改成 bind 0.0.0.0

    ✅ main 主实例(port 6380,核心业务数据)

    port 6380
    bind 127.0.0.1
    daemonize yes        # 后台守护进程运行
    pidfile /var/run/redis/redis-main.pid  # 进程id文件,区分多实例
    logfile /var/log/redis/redis-main.log
    dir /var/lib/redis/main
    dbfilename dump-main.rdb
    
    maxmemory 1gb                    # 最大可用内存1G
    maxmemory-policy allkeys-lru     # 内存满时:所有key,优先淘汰很久不用的
    # RDB定时持久化 + AOF日志持久化,双持久化,保证数据安全
    save 900 1
    save 300 10
    save 60 10000
    appendonly yes
    requirepass main_redis_2025      # 连接密码
    rename-command FLUSHDB ""
    rename-command FLUSHALL ""       # 禁用清空数据库高危命令,防止误删

    ✅cache 缓存实例(port 6381,应用缓存)

    port 6381
    maxmemory 2gb                     # 缓存分配更大内存2G
    maxmemory-policy allkeys-lru
    # 缓存:关闭持久化!
    save ""
    appendonly no
    requirepass cache_redis_2025
    timeout 0                         # 客户端不断开连接
    hz 10                             # 过期键清理频率

    ✅session 会话实例(port 6382,用户登录 session)

    port 6382
    maxmemory 512mb
    maxmemory-policy volatile-lru    # 只淘汰【带过期时间】的key,永久key不删
    # 开启持久化,但持久化频率低
    save 1800 1
    save 300 100
    appendonly yes
    timeout 1800                     # 空闲30分钟断开客户端
    notify-keyspace-events Ex        # 开启键过期事件,程序可以监听会话过期
    requirepass session_redis_2025

    ==会话特点:所有 key 一般都设置过期时间,volatile-lru 只淘汰带 TTL 的会话 key==。


    ==三个淘汰策略一句话区分==

    1. allkeys-lru:全部 key 都参与淘汰(适合主库、通用缓存)
    2. volatile-lru:只淘汰设置了过期时间的 key(适合 session 会话)

    三、第 5 部分:权限修改

    mkdir -p /var/run/redis
    chown redis:redis /var/run/redis
    chown -R redis:redis /var/lib/redis
    chown -R redis:redis /var/log/redis
    chown -R redis:redis /etc/redis/instances
    • /var/run/redis 存放 pid 文件
    • 把所有 redis 相关目录的属主改成 redis 用户,禁止 root 运行 redis(安全规范)

    四、第 6 部分:systemd 服务文件

    (redis-main.service/redis-cache.service/redis-session.service)

    ==systemd 用来管理这 3 个 redis 进程,实现开机自启、一键 start/stop/restart==

    [Unit]
    Description=Redis Main Instance
    After=network.target
    [Service]
    Type=notify
    ExecStart=/usr/bin/redis-server /etc/redis/instances/main/redis.conf --supervised systemd
    ExecStop=/usr/bin/redis-cli -p 6380 -a main_redis_2025 shutdown
    TimeoutStopSec=0
    Restart=always       # 进程意外挂掉自动重启
    User=redis
    Group=redis
    RuntimeDirectory=redis
    RuntimeDirectoryMode=0755
    [Install]
    WantedBy=multi-user.target
    • ExecStart:指定 redis-server 加载对应实例的配置启动
    • ExecStop:使用 redis-cli 优雅 shutdown 关闭 redis(不是 kill 强制杀进程)
    • Restart=always:进程崩溃自动拉起

    五、第 7 部分:加载并启动服务

    sudo systemctl daemon-reload          # 读取新增的3个service文件
    sudo systemctl enable redis-main redis-cache redis-session  # 设置开机自启
    sudo systemctl start redis-main redis-cache redis-session
    sudo systemctl status redis-main redis-cache redis-session --no-pager

    ==daemon-reload:新增 / 修改 systemd 服务文件必须执行,让系统识别新服务==

    六、检查命令

    sudo netstat -tlnp | grep redis-server  # 查看监听端口:6380、6381、6382
    ps aux | grep redis-server | grep -v grep # 查看3个redis进程

    执行成功后,会看到3 条 redis-server 进程,分别加载不同 conf。

    image-20261017114128679

    七、测试命令(验证读写)

    主实例 6380

    redis-cli -p 6380 -a main_redis_2025 ping
    redis-cli -p 6380 -a main_redis_2025 set main:test "Main Instance Data"
    redis-cli -p 6380 -a main_redis_2025 get main:test

    ping 返回 PONG 代表连通。

    cache 缓存 6381

    redis-cli -p 6381 -a cache_redis_2025 set cache:user:1001 "User Cache Data" EX 3600

    EX 3600:key 设置过期时间 3600 秒(1 小时),ttl查看剩余存活时间。

    session 会话 6382

    redis-cli -p 6382 -a session_redis_2025 set session:user:1001 "User Session Data" EX 1800

    EX 1800 = 30 分钟过期,模拟用户登录会话超时。


    image-20261017120713117

    八、查看运行参数

    # 查看当前redis占用内存
    redis-cli -p 6380 -a main_redis_2025 info memory | grep used_memory_human
    # 查询配置项maxmemory
    redis-cli -p 6380 -a main_redis_2025 config get maxmemory


    重要坑点

    脚本内所有实例都是:bind 127.0.0.1 👉 只能本机 redis-cli 连接,别的机器无法远程访问 如果你想要外部服务器远程连接这 3 个 redis 实例,需要修改 conf:

    bind 0.0.0.0

    改完,重启对应实例:systemctl restart redis-main


    ✅ 架构设计思想

    不把所有数据塞同一个 Redis 里,业务隔离

    1. main:核心业务,数据不能丢 → 双持久化、禁用危险命令
    2. cache:查询缓存,追求高性能 → 不持久化,内存大
    3. session:用户登录态,key 自带过期时间 → volatile-lru,监听 key 过期事件

    常用管理命令(课后)

    # 单独重启某一个实例
    systemctl restart redis-main
    # 停止
    systemctl stop redis-cache
    # 查看日志
    tail -f /var/log/redis/redis-main.log

    ==Redis 6.0 混合线程模型==

    背景

    Redis 在 6.0 之前是完全单线程:网络读写 + 命令执行全部由主线程一个人干。 当客户端并发非常大时,网络数据包收发会成为瓶颈,CPU 多核资源浪费。 所以 Redis6.0 引入混合 I/O 多线程模型。

    ⚠️ 重点:==命令执行依旧是单线程,只有网络收发变成多线程==

    1)核心逻辑线程(主线程,单线程)

    负责:

    • 解析客户端发来的命令:GET / SET / HSET
    • 操作内存里的数据结构(String、Hash、List 等)
    • 事务、Lua 脚本、过期键清理、RDB/AOF 持久化调度
    • 所有命令的实际执行

    为什么核心逻辑坚持单线程?

    • 如果多线程同时读写同一个 key,就需要加锁;锁会带来竞争、性能下降、死锁风险
    • 单线程天然保证:一条命令从头到尾原子执行,不会被打断,不用锁,实现简单且安全

    一句话:==命令串行执行,不存在并发修改同一个 key 的竞争问题==

    2)I/O 多线程(io-threads,可配置,默认 4 个)

    I/O 线程只做网络层面的数据搬运,不碰业务逻辑:

    1. 从 socket 读取客户端发来的网络数据(接收 TCP 数据包)
    2. 把 Redis 命令执行后的返回结果,写回 socket 发给客户端
    3. 只负责字节数据传输,不解析命令、不读写 Redis 数据

    工作完整流程:

    1. 客户端发命令 → 操作系统网卡收到数据
    2. I/O 线程并行把网络数据读到 Redis 内存缓冲区
    3. 全部读取完成后,把请求交给主线程
    4. 主线程单线程串行执行命令,得到结果
    5. 主线程把结果丢给 I/O 线程
    6. I/O 线程并行把结果通过网络发回客户端

    I/O 线程只是 “快递员”,只负责搬运包裹;主线程才是处理业务的 “办事窗口”。 可以用io-threads 4配置 I/O 线程数量,一般设置为 CPU 核数。


    Redis 高性能的完整原因拆解(面试题)

    1. 数据全部存内存 内存读写速度远远高于磁盘,避免磁盘 IO 的漫长等待。 内存带宽高,配合紧凑的数据结构,读写延迟极低。
    2. 针对场景优化的数据结构 不是简单的哈希表,根据不同类型选用合适底层结构(压缩列表、跳表、哈希表等),节省内存、减少拷贝,算法效率高。
    3. 事件驱动 + 非阻塞 IO(epoll) 采用 IO 多路复用,一个线程可以监听上万条 TCP 连接。 没有空闲轮询消耗 CPU;==有事件到来才处理==,高并发连接下资源消耗很小。
    4. 6.0 I/O 多线程优化网络瓶颈 多线程并行处理网络读写,多核 CPU 可以一起处理数据包收发,提升网络吞吐,解决大量客户端连接场景下的网络瓶颈。

    Redis 8 种内存淘汰策略

    前置前提

    配置maxmemory:限制 Redis 实例最大可用内存(你之前配置的 main=1G,cache=2G,session=512M) 当 used_memory ≥ maxmemory,再执行写入命令(set 等),触发内存淘汰策略。

    目的:腾出内存给新数据写入。 分为两大类别:allkeys- 和 volatile-

    前缀区分

    • allkeys-xxx:数据库里所有 key,不管有没有设置过期时间,都参与淘汰
    • volatile-xxx:只挑选设置过 expire 过期时间的 key;永久 key 永远不会被淘汰

    8 种策略逐条详解

    1. noeviction(默认策略) 内存满了之后,不删除任何 key,拒绝所有写操作,直接返回 OOM 错误。 读命令不受影响。 适用:核心业务,数据绝对不能丢失,宁可写入失败也不删数据。

    2. allkeys-lru 从全部 key里,淘汰最近最少使用(Least Recently Used)的 key。 思路:很久没人访问的数据优先删掉;经常访问的热点数据保留。 ✅适用:通用缓存(你的 cache 缓存实例、main 主实例)

    3. volatile-lru

    ​ 只在设置了过期时间的 key中,淘汰最近最少使用的 key。 ​ 永久存在的 key 不会被清理。 ​ ✅适用:你的 session 会话实例(所有 session 都带过期时间)

    1. allkeys-lfu 从全部 key,淘汰访问频率最低(Least Frequently Used)的 key。 LRU 看「多久没用」;LFU 看「被访问多少次」。 适合:热点长期稳定的缓存,有些 key 很久没访问但是访问频率很高,LRU 会误删,LFU 更合适。

    2. volatile-lfu

      仅带过期时间的 key,淘汰访问频率最低的 key。

    3. allkeys-random 所有 key 中随机挑选 key 删除,不看访问时间、频率。 性能最好,但是淘汰逻辑粗暴,几乎不生产使用。

    4. volatile-random

      只在带过期 key 里随机删。同样很少用。

    5. volatile-ttl

    ​ 只在带过期时间 key,优先淘汰剩余 TTL 最短(马上就要过期)的数据。 ​ 逻辑:谁最先到期就删谁。适合有统一过期生命周期的缓存。


    和 3 个 Redis 多实例对应回顾

    1. main(6380,核心业务):allkeys-lru 所有 key 都参与淘汰,优先删掉很久不用的数据。
    2. cache(6381,业务缓存):allkeys-lru 缓存数据,允许淘汰任意 key。
    3. session(6382,用户会话):volatile-lru 只淘汰带过期的 session,永久业务 key 不会被误删。

    面试深度问答题

    Q:Redis6 之后是多线程了吗? A:命令执行仍然单线程,只有网络 IO 收发是多线程。保证命令原子性,提升网络吞吐。

    Q:allkeys-lru 和 volatile-lru 最大区别? A:allkeys-lru 所有 key 都参与淘汰;volatile-lru 只淘汰带有过期时间的 key,永久 key 保留。

    Q:什么时候触发内存淘汰? A:Redis 使用内存达到 maxmemory 上限,执行新的写入命令时触发。

  • Mysql 单表查询和字符集介绍

    单表查询

    [root@server1 ~]# mysql -uroot -p123456 < world.sql 
    # 查询全部数据:如果数据表中的数据过多,一次性加载全部数据可能会造成拥堵
    mysql> use world;
    # 指定查询数据量
    mysql> select * from city;
    mysql> desc city;
    
    mysql> select countrycode,district from city limit 2;
    +-------------+----------+
    | countrycode | district |
    +-------------+----------+
    | AFG         | Kabol    |
    | AFG         | Qandahar |
    +-------------+----------+
    2 rows in set (0.00 sec)
    # 从第三条开始,往后查询四条记录,常用与分页查询
    mysql> select id,countrycode,district from city limit 2,4;
    +----+-------------+---------------+
    | id | countrycode | district      |
    +----+-------------+---------------+
    |  3 | AFG         | Herat         |
    |  4 | AFG         | Balkh         |
    |  5 | NLD         | Noord-Holland |
    |  6 | NLD         | Zuid-Holland  |
    +----+-------------+---------------+
    4 rows in set (0.00 sec)
    # 指定条件,查询出中国的所有数据
    mysql> select name,population from city where countrycode='CHN';
    # 多条件查询
    mysql> select name,population from city where countrycode='CHN' and district='heilongjiang';
    # 模糊查询
    mysql> select name,population,countrycode from city where countrycode like "%H%" limit 10;

    1. desc city;

    desc city;

    desc = describe,查看表结构(查看这张表有哪些字段、字段类型) 输出:

    表格

    Field Type Null Key Extra
    ID int(11) NO PRI auto_increment 主键,自增编号
    Name char(35) NO 城市名字
    CountryCode char(3) NO MUL 国家代码(AFG 阿富汗,NLD 荷兰)
    District char(20) NO 省份 / 地区
    Population int(11) NO 人口

    ✅ 看懂:city 表存全世界城市信息,一共 5 个字段。

    2. select CountryCode,District from city limit 2;

    select CountryCode,District from city limit 2;

    拆解:

    • select CountryCode,District:只查询这两个字段(列),不是全部*
    • from city:从 city 这张表查询
    • limit 2:限制只返回 2 条记录(行)

    结果:

    +-------------+-----------+
    | CountryCode | District  |
    +-------------+-----------+
    | AFG         | Kabol     |
    | AFG         | Qandahar  |
    +-------------+-----------+

    意思:阿富汗的两个地区。

    3. select CountryCode,District from city limit 10;

    limit 10 → 取出前 10 条记录,不会把 4079 行全部展示出来。

    重点:limit 作用:限制返回多少行数据 生产库数据量巨大,千万别直接 select * from city; 不加 limit,一次性读出上万行会卡!

    image-20261015161126312

    1. limit 偏移量,条数 分页写法

    select id,countrycode,district from city limit 2,4;

    语法:limit 起始偏移量, 获取多少条

    ⚠️ 偏移量从 0 开始计数!不是从 1!

    • limit 2,4:跳过前 2 条,从第 3 条开始,一共取 4 条
      • 第 0 行:id=1
      • 第 1 行:id=2
      • 跳过 0、1 这两行,从第 2 偏移位置开始拿 4 条 → id 3,4,5,6
    • 用途:分页
      • 第 1 页:limit 0,4 → 1~4 条
      • 第 2 页:limit 4,4 → 5~8 条

    简写区分: limit 4 = 只取前 4 条(偏移量默认为 0) limit 2,4 = 跳过 2 条,取 4 条

    2. 单条件 where:查询中国所有城市

    select name,population from city where countrycode='CHN';
    • where countrycode='CHN':筛选条件,只保留 countrycode 等于 CHN(中国)的数据
    • 单引号:字符串类型的值必须加单引号(国家代码是字符)
    • 取出字段:name城市名、population人口 逻辑:在 city 表里,只把中国城市的名字和人口查出来

    注意:大小写在 MySQL 默认配置下不敏感,chn 和 CHN 效果一样,但规范写大写。

    3. 多条件查询 and(并且,两个条件必须同时满足)

    select name,population from city where countrycode='CHN' and district='heilongjiang';

    and:左右两个条件全部成立才会筛选出来 条件 1:countrycode='CHN' 中国 条件 2:district='heilongjiang' 黑龙江省 含义:查询中国黑龙江省的城市名称和人口。

    补充:还有 or(或者,满足任意一个条件即可) 例:where countrycode='CHN' or countrycode='JPN' 中国 或者 日本城市

    4. 模糊查询 like + 通配符 %

    select name,population,countrycode from city where countrycode like "%H%" limit 10;

    like:模糊匹配,配合%通配符

    • %:匹配任意长度任意字符(0 个、1 个、多个都可以)
    • "%H%":字段里面只要包含字母 H,不管 H 在开头、中间、结尾都匹配
      • 例:HND、CHN、THA 都会匹配,因为里面带 H
      • H%:以 H 开头,如HUN
      • %H:以 H 结尾

    limit 10:匹配成功后,只返回前 10 条结果。

    小提醒:= 是精确匹配;like 才是模糊匹配,二者不要混用。

    总结一张小表

    语法 作用
    limit m,n 跳过 m 条,取 n 条,m 从 0 开始,分页用
    where 字段='值' 精确筛选
    and 多个条件同时成立
    or 多个条件满足任意一个
    like '%关键词%' 模糊查询,字段包含关键词

    image-20261015164222049

    image-20261015164500075


    5. order by 排序语句

    select id,name,population,countrycode from city order by countrycode ASC limit 10;
    select id,name,population,countrycode from city order by countrycode ASC limit 10;  # ASC可省略

    语法拆解

    • order by 字段:对查询出来的结果进行排序
    • ASC = ascending,升序(从小到大 / A→Z),默认就是 ASC,可以不写
      • order by countrycode 等价 order by countrycode ASC
    • DESC = descending,降序(从大到小 / Z→A) 示例:按人口从高到低排序
      select id,name,population from city order by population DESC limit 10;
    • limit 10:排序完成之后,只取前 10 条

    执行流程:

    1. 从 city 表取出数据
    2. 根据countrycode字母 A-Z 排序
    3. 取出指定 4 个字段
    4. 只返回前 10 行

    小提示:order by 写在 where 后面、limit 前面 例:查询中国城市,按人口降序取前 5 个

    select name,population from city where countrycode='CHN' order by population desc limit 5;

    image-20261015173938319

    MySQL 字符集与校对规则

    一、基础概念

    字符集(Character Set)

    字符集:一套字符 + 对应的二进制编码的集合。 字符包含:各国文字、数字、标点、符号。

    作用:数据库怎么把文字转成二进制存储。 例:utf8、gbk、latin1,不同字符集占用字节、支持文字不一样。

    校对规则(Collation)

    校对规则:字符集内,字符如何比较、排序的一套规则,依附于字符集。

    作用:查询排序、where条件比较时,判断字符是否相等。

    校对规则后缀含义

    • ci = case insensitive → 大小写不敏感(最常用) 'ABC' 和 'abc' 认为相等
    • cs = case sensitive → 大小写敏感 'ABC' 和 'abc' 不相等
    • bin = binary 二进制比较 → 严格大小写敏感,直接按二进制值对比

    举例:utf8_general_ci

    • utf8:字符集
    • general:通用比较算法
    • ci:大小写不敏感

    建表字段属性说明(column_definition)

    创建表时,在字段后可以加这些约束 / 属性,语法模板:

    字段名 数据类型 [NOT NULL | NULL] [DEFAULT 默认值]
    [AUTO_INCREMENT] [UNIQUE [KEY]] [[PRIMARY] KEY]
    [COMMENT '字符串注释']

    表格

    属性 含义
    PRIMARY KEY 主键,唯一标识一行;主键不能重复、不能为 NULL,一张表只能 1 个主键
    REFERENCES 外键,用来引用另一张表主键,保证数据参照完整性
    NULL 该字段允许为空(不填数据),默认就是 NULL
    NOT NULL 不允许为空,插入数据时这个字段必须填写值
    UNIQUE 唯一性约束,该字段的值不能重复;可以多个 NULL
    INDEX 索引,加快查询速度
    AUTO_INCREMENT 自增,一般配合主键 int 类型,新增记录自动 + 1(1,2,3…)
    DEFAULT 设置默认值,插入不填此字段时,自动使用默认值
    COMMENT 字段注释,方便看懂字段作用,不影响程序运行
    ENGINE 存储引擎,MySQL 常用 InnoDB(支持事务、外键)

    二、MySQL 常见字符集

    1. utf8 MySQL 里的utf8是utf8mb3,只支持最多 3 字节,不支持 emoji 表情。

    ⚠️ 生产推荐 utf8mb4(完整 utf-8,4 字节,支持表情),课件写 utf8 是简化教学。

    1. latin1(iso-8859-1) 西欧字符集,不支持中文;早期默认字符集,中文会乱码。
    2. gbk 中文专用字符集,汉字占 2 字节,存储空间小;只支持中日韩文字,国际通用性差。

    三.4 种指定字符集的方式(优先级:越往下优先级越高)

    优先级:连接级 > 表级 > 库级 > 服务端 my.cnf 配置 意思:表上指定字符集,会覆盖库的;库的覆盖服务端全局配置。

    1. 编译安装时指定(源码编译才用,二进制 rpm/yum 安装不用)

    源码编译 MySQL,编译参数里设置默认字符集,属于底层固定,日常运维很少改。

    2. 配置文件 my.cnf/my.ini 【服务全局默认】

    [mysqld]
    character-set-server=utf8

    作用:MySQL 服务启动后,新建库默认继承这个字符集。

    注意:修改后必须重启 MySQL 服务才生效;已经建好的库 / 表不会自动变。

    3. 创建库、创建表时,单独指定字符集(最常用)

    创建数据库

    create database db01 charset utf8 default collate = utf8_general_ci;
    • charset utf8:库字符集 utf8
    • default collate=utf8_general_ci:默认校对规则,大小写不敏感 含义:新建库 db01,这个库下面新建表,默认继承这套字符集和校对规则。

    创建数据表

    CREATE TABLE `test` (
    `id` int(4) NOT NULL AUTO_INCREMENT,
    `name` char(20) NOT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
    • ENGINE=InnoDB:存储引擎(支持事务)
    • DEFAULT CHARSET=utf8:这张表单独指定字符集,优先级高于库

    ==建表不写字符集,就继承所在数据库字符集==。

    4. 修改已存在的库 / 表字符集

    ⚠️重点坑:修改库 / 表字符集,不会自动修改表里已经存好的数据! 只修改后续新增数据的字符集;原有历史数据乱码不会修复。

    修改数据库字符集

    alter database db01 CHARACTER SET utf8 collate utf8_general_ci;

    修改数据表字符集

    alter table t1 CHARACTER SET utf8;

    四.补充实操小知识点

    1. 查看当前数据库字符集
    show variables like 'character_set%';
    1. 查看库的字符集
    show create database db01;
    1. 查看表字符集
    show create table test;
    1. 乱码根源:客户端字符集、连接字符集、表字符集三者不一致。

    五.一句话总结

    • 字符集:管文字怎么存(编码)
    • 校对规则:管文字怎么对比排序(大小写敏感)
    • 设置层级:全局配置 → 库 → 表,表的字符集优先级最高
    • ci大小写不敏感,cs/bin大小写敏感
    • alter修改字符集不能修复已有乱码数据
    • 配套的校对规则,字符集和校对规则名称必须匹配 : utf8mb4 对应utf8mb4_general_ci,utf8 对应utf8_general_ciimage-20261016111146290image-20261016111425927

    Mysql多表连接查询

    建表和准备数据
    #建表
    create table department(
    id int,
    name varchar(20)
    );
    create table employee2(
    id int primary key auto_increment,
    name varchar(20),
    sex enum('male','female') not null default 'male',
    age int,
    dep_id int
    );
    
    
    #插入数据
    insert into department values
    (200,'技术'),
    (201,'人力资源'),
    (202,'销售'),
    (203,'运营');
    insert into employee2(name,sex,age,dep_id) values
    ('egon','male',18,200),
    ('alex','female',48,201),
    ('wupeiqi','male',38,201),
    ('yuanhao','female',28,202),
    ('liwenzhou','male',18,200),
    ('jingliyang','female',18,204);
    

    两张表:

    1. department 部门表:存部门 id、部门名称
    2. employee2 员工表:存员工信息,dep_id 是外键,对应部门表的id

    ==关系:employee2.dep_id 关联 department.id==

    • 部门表:id (200 技术,201 人力,202 销售,203 运营)
    • 员工表:jingliyang 的 dep_id=204,部门表没有 204 这个部门

    表:employee2员工表(含dep_id),department部门表(主键id)

    1. 交叉连接

    select * from employee2,department;

    含义:笛卡尔积 把员工表每一行 和 部门表每一行全部两两组合。

    举个例子:员工 3 条、部门 2 条 → 返回 3×2=6 条记录。

    ❗问题:会产生大量无效数据,几乎业务不用。 特点:没有匹配条件,全部强行组合

    image-20261016122554633

    2. 内连接 方法一(标准 JOIN 写法,推荐)

    select
    employee2.id,employee2.name,employee2.age,employee2.sex,department.name 
    from employee2 
    inner join department 
    on employee2.dep_id=department.id;
    • inner join:内连接(inner可以省略,直接写join)
    • on employee2.dep_id=department.id:匹配条件

    ==规则:只保留 员工表dep_id = 部门表id 的行,两边匹配得上的数据才查出来。 👉 员工有部门、部门有员工,两边都存在才展示;员工 dep_id 为空 / 不存在的部门 id,这条员工直接不显示。==

    ✅ 规范写法:连接条件写在ON后面,过滤条件放WHERE。==工作优先用这种==

    3. 内连接 方法二(隐式内连接,老写法)

    select
    employee2.id,employee2.name,employee2.age,employee2.sex,department.name 
    from employee2,department 
    where employee2.dep_id=department.id;
    • 这里 from 表1,表2 本质先做交叉连接(笛卡尔积)
    • 再用where筛选:只留下满足dep_id=id的行
    • 效果和上面 inner join 完全一样!

    ⚠️缺点:

    1. 把连接条件和普通过滤条件全部混在 where 里,可读性差
    2. 多表联查的时候容易写错,企业开发不推荐

    image-20261016124421046

    一句话对比总结

    1. 交叉连接:不带条件,所有行两两相乘,笛卡尔积,基本不用
    2. ==内连接 inner join … on:显式写法,按 on 条件匹配,两边都有数据才出现 ✅推荐==
    3. from 表 1, 表 2 where 条件:隐式内连接,结果同上,只是语法老式

    关键

    • 内连接:只返回两张表能匹配上的数据
    • ON 和 WHERE 在 inner join 时,结果一样;但是外连接的时候不能混用!
    • 交叉连接 = 不带条件的多表逗号查询

    前置信息

    两张表:

    • employee2 员工表:id(员工编号)、name(员工名)、age、sex、dep_id(部门id)
    • department 部门表:id(部门编号)、name(部门名称) 关联条件:employee2.dep_id = department.id

    员工表的 dep_id,去匹配部门表主键 id,用来找到员工所属部门


    1. LEFT JOIN 左连接

    select employee2.id,employee2.name,department.name as depart_name 
    from employee2 left join department 
    on employee2.dep_id=department.id;

    关键字:left join 左连接

    左表 = left join 左边的表:employee2(员工表)

    规则:左表里所有记录一定会全部查出来;右表能匹配上就带出数据,匹配不上就填 NULL

    执行过程:

    1. 拿每一条员工记录,根据 on 后面的关联条件,去部门表匹配
    2. ✅匹配成功:员工存在对应部门 → 查出员工 + 部门名称
    3. ❌匹配失败:员工 dep_id 为空,或者这个部门不存在 → 员工信息保留,部门字段 depart_name = NULL

    业务场景: 查询全部员工,不管有没有分配部门,员工都要展示;有部门就显示部门名字,无部门则部门名是空。

    ==as depart_name:给 department.name 起别名 depart_name,方便查看,否则两个表都叫 name 会混淆==。

    image-20261017170053500


    left join执行流程图

    (适配你当前 employee2 左连 )

    flowchart TD
        A["开始 LEFT JOIN"] --> B["取出左表 employee2 的每一行,12行全部保留"]
        B --> C{"dep_id匹配department.id? 条件: employee2.dep_id = department.id"}
        C -->|"匹配成功"| D["拼接员工字段 + 部门name"]
        C -->|"匹配失败, 例: dep_id=204"| E["拼接员工字段, 部门字段填NULL"]
        D & E --> F["收集该行到结果集"]
        F --> G{"左表还有下一行?"}
        G -->|是| B
        G -->|否| H["输出结果集, 一共12行"]

    2. RIGHT JOIN 右连接

    select employee2.id,employee2.name,department.name as depart_name 
    from employee2 right join department 
    on employee2.dep_id=department.id;

    右表 = right join 右边的表:department(部门表)

    规则:右表里所有记录一定会全部查出来;左表匹配上带出员工,匹配不上,员工字段全部填 NULL

    执行过程:

    1. 遍历每一条部门记录,去员工表匹配员工
    2. ✅匹配成功:部门下面有员工,同时查出员工 + 部门
    3. ❌匹配失败:这个部门目前没有员工 → 部门保留,员工 id、name 全部为 NULL

    业务场景: 查询所有部门,哪怕是空部门(没有任何员工)也要展示出来。

    ==一句话区分左右连接:left 保左表全部;right保右表全部==。

    image-20261017170535263

    right join流程图

    (右连接,department 为右表)

    flowchart TD
        A["开始 RIGHT JOIN"] --> B["取出右表 department 的每一行,4个部门全部保留"]
        B --> C{"department.id匹配employee2.dep_id? 条件:employee2.dep_id = department.id"}
        C -->|"匹配成功"| D["拼接员工字段 + 部门字段"]
        C -->|"匹配失败,例:部门203运营"| E["员工字段填NULL + 部门字段"]
        D & E --> F["收集该行到结果集"]
        F --> G{"右表还有下一行?"}
        G -->|"是"| B
        G -->|"否"| H["输出结果集"]

    3. 全外连接 FULL OUTER JOIN

    ⚠️重点:MySQL 数据库不支持 FULL OUTER JOIN 语法 想要实现【两张表所有记录全部展示】,采用方案:左连接结果 UNION 右连接结果

    select * from employee2 left join department on employee2.dep_id = department.id
    union
    select * from employee2 right join department on employee2.dep_id = department.id;

    UNION 详解

    UNION:把上下两条 SQL 查询结果合并在一起,自动剔除重复的行,附带排序,性能更低 UNION ALL:直接拼接结果,不做去重,不排序,性能更好;确定没有重复行优先使用 UNION ALL

    这条 SQL 执行逻辑:

    1. 上面left join:拿到全部员工(有部门 / 无部门员工)
    2. 下面right join:拿到全部部门(有员工 / 空部门)
    3. UNION合并两份结果,自动去掉重复数据 最终结果包含 4 类数据:
    • 有部门的员工
    • 没有部门的员工
    • 有员工的部门
    • 没有员工的空部门

    也就是:两张表里所有行,全部展示出来,这就是全外连接效果。

    下面长 SQL(同逻辑,指定字段,不用 select *)

    select employee2.id ,employee2.name ,employee2.age ,employee2.sex ,department.name 
    from employee2 right join department on employee2.dep_id=department.id 
    union 
    select employee2.id ,employee2.name ,employee2.age ,employee2.sex ,department.name 
    from employee2 left join department on employee2.dep_id=department.id;
    • 和上面逻辑一模一样,只是不再使用select *
    • 手动指定需要查询的字段:员工 id、姓名、年龄、性别,部门名称

    ==开发规范里不推荐select *,建议明确写出需要的字段==。

    image-20261017172441717

    union 流程图

    flowchart TD
        A["全外连接 MySQL实现方案"] --> B["执行LEFT JOIN:拿到全部员工,匹配部门"]
        A --> C["执行RIGHT JOIN:拿到全部部门,匹配员工"]
        B & C --> D["UNION 合并两个结果集,自动去除重复行"]
        D --> E["输出:所有员工+所有部门,匹配不到的一侧填NULL"]

    4:内连接 INNER JOIN(对照记忆)

    select employee2.id,employee2.name,department.name depart_name
    from employee2 inner join department
    on employee2.dep_id = department.id;

    内连接:只保留两边能匹配成功的数据

    只有【员工存在,并且所属部门也存在】的数据才会查询出来;无部门员工、空部门,全部不显示。

    inner join 流程图

    flowchart TD
        A["开始 INNER JOIN"] --> B["取出左表 employee2 的每一行"]
        B --> C{"employee2.dep_id 匹配 department.id? 条件:employee2.dep_id = department.id"}
        C -->|"匹配成功"| D["拼接员工字段 + 部门字段"]
        C -->|"匹配失败(例:员工无部门 / 空部门)"| E["直接丢弃该行数据,不显示"]
        D --> F["收集该行到结果集"]
        E --> G{"还有下一行数据吗?"}
        F --> G
        G -->|"是"| B
        G -->|"否"| H["输出结果集:只保留两边都匹配成功的数据"]

    总结对比表

    连接类型 核心特点 保留哪些数据 匹配不上时
    INNER JOIN 内连接 取交集 只保留两边匹配成功记录 直接丢弃该行
    LEFT JOIN 左连接 保留左表全部 左表全部 + 右表匹配部分 右表字段 NULL
    RIGHT JOIN 右连接 保留右表全部 右表全部 + 左表匹配部分 左表字段 NULL
    FULL 全外连接 并集(MySQL 用 union 模拟) 左右两张表全部记录 缺失一侧字段 NULL

    考点背诵

    1. left join:左表全部数据;right join:右表全部数据。
    2. MySQL 不支持 full outer join,使用 left join union right join 模拟全外连接。
    3. union:合并并且去重;union all:直接拼接不去重,性能更高。

    SQL 讲解(内连接 inner join)

    练习示例方便下面10道练习

    select employee2.name,department.name 
    from employee2
    inner join department on employee2.dep_id = department.id 
    where age > 25;          

    需求:内连接查询 employee2 员工表和 department 部门表,查询年龄大于 25 岁的员工名字,以及员工对应的部门名称

    image-20261017174113810


    MySQL 上机练习:student 学生表 & score 成绩表

    ==整套包含:建库建表插入数据 + 10 道练习题(题干 + 完整 SQL + 注释,无表别名简写,可直接复制运行)==

    -- 1. 创建数据库,指定字符集
    create database my_db charset utf8 default collate = utf8_general_ci;
    use my_db;
    
    -- 2. 创建student学生表
    CREATE TABLE student (
        stu_id INT(10) PRIMARY KEY,
        stu_name VARCHAR(20) NOT NULL,
        sex VARCHAR(2),
        birth YEAR,
        department VARCHAR(20),
        addr VARCHAR(50)
    );
    
    -- 插入student测试数据
    INSERT INTO student VALUES
    (901,'张飞','男','1985','计算机系','河北省涿州市'),
    (902,'关羽','男','1986','中文系','山西省运城市'),
    (903,'貂蝉','女','1990','中文系','山西省忻州县'),
    (904,'刘备','男','1990','英语系','河北省涿州市'),
    (905,'小乔','女','1991','英语系','安徽省潜山市'),
    (906,'赵云','男','1988','计算机系','河北省正定市');
    
    -- 3. 创建score成绩表
    CREATE TABLE score (
        score_id INT(10) PRIMARY KEY AUTO_INCREMENT,
        stu_id INT(10) NOT NULL,
        c_name VARCHAR(20),
        grade INT(10)
    );
    
    -- 插入score成绩数据
    INSERT INTO score(stu_id,c_name,grade) VALUES
    (901,'计算机',98),
    (901,'英语',80),
    (902,'计算机',65),
    (902,'中文',88),
    (903,'中文',95),
    (904,'计算机',70),
    (904,'英语',92),
    (905,'英语',94),
    (906,'计算机',90),
    (906,'英语',85);

    练习题(10 题)

    1. 从 student 表中查询计算机系和英语系学生的信息。

    SELECT * FROM student WHERE department='计算机系' or department='英语系';

    2. 从 student 表中查询年龄 33~38 岁的学生信息。

    SELECT * FROM student WHERE year(curdate())-birth between 33 and 38;

    3. 在 student 表中统计每个院系各有几个学生。

    SELECT department AS 院系,count(*) AS 人数 FROM student GROUP BY department;

    4. 查询每个院系学生中的最高分。

    SELECT student.department, MAX(score.grade) AS max_score
    FROM student JOIN score ON student.stu_id = score.stu_id
    GROUP BY student.department;

    5. 查询学生貂蝉的考试科目(c_name)和考试成绩(grade)

    SELECT student.stu_name,score.c_name,score.grade
    FROM student INNER JOIN score ON student.stu_id=score.stu_id
    WHERE student.stu_name='貂蝉';

    6. 查询计算机成绩低于 95 的学生信息。

    写法 1(多表连接)

    SELECT student.stu_name , student.sex ,student.birth, student.department ,student.addr ,score.c_name,score.grade
    FROM student,score
    WHERE student.stu_id = score.stu_id and grade <95 and c_name='计算机';

    写法 2(子查询)

    SELECT * FROM student WHERE stu_id IN (SELECT stu_id FROM score WHERE c_name='计算机' and grade<95);

    7. 查询同时参加计算机和英语考试的学生的信息。

    SELECT * FROM student
    WHERE stu_id IN (
        SELECT s1.stu_id
        FROM score s1
        JOIN score s2 ON s1.stu_id = s2.stu_id
        WHERE s1.c_name = '计算机' AND s2.c_name = '英语'
    );

    8. 从 student 表和 score 表中分别查询出学生的学号,然后合并查询结果。

    SELECT stu_id FROM student UNION SELECT stu_id FROM score;

    UNION:自动去除重复学号;如果想保留重复,使用 UNION ALL

    9. 查询姓张或者姓王的同学的个人信息、院系和考试科目及成绩。

    SELECT student.stu_id,student.stu_name,student.department 院系,score.c_name 考试科目, score.grade 成绩
    FROM student INNER JOIN score ON student.stu_id=score.stu_id
    WHERE student.stu_name LIKE '张%' OR student.stu_name LIKE '王%';

    10. 查询山西省的学生的姓名、出生年份、院系、考试科目及成绩。

    SELECT student.stu_id,student.stu_name,student.birth 出生年份, student.department 院系,score.c_name 考试科目, score.grade 成绩
    FROM student INNER JOIN score ON student.stu_id=score.stu_id
    WHERE student.addr LIKE '山西省%';

    知识点汇总

    1. 建库建表、主键、非空、自增约束,INSERT批量插入
    2. 单表查询:WHERE、OR、BETWEEN、LIKE模糊查询
    3. 聚合函数count()、max() + GROUP BY分组统计
    4. 多表关联:INNER JOIN内连接
    5. 子查询 IN(子查询)、表自连接
    6. UNION 多个查询结果合并
  • MySQL 索引介绍

    这份笔记一共 5 种索引:B 树、B + 树、HASH 索引、FULLTEXT 全文索引、R 树 (RTREE) 空间索引,重点是 B + 树(InnoDB 默认)。

    1. B 树(多路平衡查找树)

    B + 树是 B 树的改良版本,先看懂 B 树更好理解。

    结构特点

    1. 每个节点可以存多个关键字 + 指针
    2. 所有叶子节点在同一层,树高度平衡,查询稳定
    3. 关键字有序,子树关键字范围和父节点对应

    B 树:非叶子节点、叶子节点都保存数据

    适用场景

    精确查询、范围查询。

    实际 MySQL 很少直接用原生 B 树,用的是它的变种:B + 树


    2. B + 树(⭐最重要,InnoDB 默认索引)

    B + 树是 B 树优化版本,InnoDB 引擎默认索引,面试最高频。

    结构特点

    1. 非叶子节点只存关键字 + 指针,不存真实数据;只有叶子节点存放完整数据
    2. 所有叶子节点用双向链表按顺序串起来,方便范围查找
    3. 全部叶子节点在同一层,查询时间稳定 O (log n)

    ✅ 优势

    • 精确查询 + 范围查询都很强(BETWEEN、>、<、ORDER BY排序)
    • 磁盘 IO 少:单个节点可以存放更多索引关键字,减少读磁盘次数
    • 适合大数据量,性能稳定

    适用场景: 主键索引、单列索引、联合索引(遵守最左前缀原则)、频繁范围查询 / 排序。

    InnoDB 主键索引是聚簇索引:叶子节点直接存放整行数据;普通二级索引叶子节点存放主键值。

    3. HASH 索引(哈希索引)

    基于哈希表实现

    结构特点

    对索引字段计算哈希值,哈希值直接映射数据地址。理想情况查找复杂度 O (1)

    ✅优点:等值精确匹配极快 where id=100 速度优于 B + 树 ❌缺点(高频考点)

    1. 不支持范围查询、不支持排序:哈希值是乱序,不能用> < BETWEEN ORDER BY
    2. 哈希冲突:不同关键字算出相同哈希,需要链表处理,性能下降
    3. 联合索引只能完整匹配全部字段,不能走前缀匹配

    适用场景:只做等值查询

    MySQL:Memory 引擎支持手动创建 HASH 索引;InnoDB 自适应哈希索引是内部自动维护,不能手动创建

    4. FULLTEXT 全文索引

    用于大文本关键词检索(文章、评论),底层是倒排索引 原理:把文本拆分成单词 (Term),记录哪个单词出现在哪些文档里。

    ✅优点: 文本关键词检索,远快于 LIKE '%关键词%'(%前缀通配无法走普通索引,全表扫描) 使用语法:MATCH(字段) AGAINST('关键词')

    ❌限制

    1. 只支持文本类型:CHAR/VARCHAR/TEXT
    2. 有最小词长限制,不同引擎支持不一样
    3. 不能做部分词语匹配:搜 “数据” 不能命中 “数据库”

    场景:博客、文章内容搜索

    -- 创建全文索引
    CREATE FULLTEXT INDEX idx_article_content ON article(content);
    -- 查询示例
    SELECT * FROM article
    WHERE MATCH(content) AGAINST('数据库 索引' IN BOOLEAN MODE);

    5. RTREE(R 树 空间索引 SPATIAL INDEX)

    专门用于地理空间数据(坐标、几何图形)

    • 多维空间索引,处理点、线、面
    • 可以做空间范围查询:查找某坐标附近的点

    适用场景:GIS 地理信息、经纬度,例如:查找距离某个坐标 1 公里内的店铺

    CREATE TABLE places (
      id INT,
      location POINT, -- 空间点类型
      SPATIAL INDEX idx_location(location) -- R树空间索引
    )ENGINE=InnoDB;

    查询函数:ST_Distance_Sphere 计算球面距离


    MySQL 索引创建与删除语法

    三种建索引方式

    1. 建表的时候直接定义索引
    CREATE TABLE 表名 (
    字段1 数据类型,
    字段2 数据类型,
    [UNIQUE | FULLTEXT | SPATIAL] INDEX 索引名(字段名)
    );
    1. 表已经存在,用 CREATE 创建索引
    CREATE [UNIQUE | FULLTEXT | SPATIAL] INDEX 索引名
    ON 表名 (字段名);
    1. ALTER TABLE 添加索引
    ALTER TABLE 表名 ADD [UNIQUE | FULLTEXT | SPATIAL] INDEX 索引名(字段名);

    删除索引

    DROP INDEX 索引名 ON 表名;

    索引类型快速对比总结(方便背诵)

    表格

    索引 底层 擅长 短板
    B + 树 多路平衡树 等值、范围、排序、联合索引 单纯等值查询略慢于 hash
    HASH 哈希表 等值精确查询 不支持范围、排序、前缀匹配、哈希冲突
    FULLTEXT 倒排索引 大文本关键词搜索 不能模糊部分匹配,仅限文本
    RTREE R 树 地理坐标空间范围查询 普通业务几乎不用

    面试重点一句话

    InnoDB 默认索引是 B + 树; B + 树叶子节点链表,适合范围查询; HASH 索引只能等值查询,不能范围和排序; 全文索引用来替代like '%xxx%'文本检索。

    总结:

    多路平衡树(B 树 / B + 树)就是 “综合型选手”,兼顾等值查找 + 范围查找 + 排序,通用性最强;而 Hash 索引是专精型选手,只擅长等值查询。

    多路平衡树(B + 树)设计目标:不要只专精单一查询,而是绝大多数 SQL 场景都能用,性能稳定。 Hash:单点查询极限速度快,但适用场景窄。

    考试背诵版: ==B + 树属于多路平衡查找树,多路可以降低树高减少磁盘 IO;平衡保证查询性能稳定。支持等值查询、范围查询、排序,综合能力强,是 InnoDB 默认索引==。


    Mysql的索引管理

    这一页是MySQL 索引的基础分类 + 增删查索引 SQL 命令,偏实操

    添加索引:
    alter table test add index index_name(name);
    #创建索引
    create index index_name on test(name);
    #创建索引
    desc table;
    #查看索引
    show index from table;
    #查看索引
    alter table test drop key index_name;
    #删除索引
    alter table student add unique key uni_xxx(xxx);
    #添加主键索引(略)
    #添加唯一性索引
    select count(*) from city;
    #查看表中数据行数
    select count(distinct(name)) from city;
    #查看去重数据行数

    一、基础概念

    1. 索引建立在表的列(字段)上 索引不是整张表的,是给某一个 / 多个字段单独建立的结构。
    2. 在 where 后面的列建立索引,才会加快查询速度

    举例:select * from test where name='张三' where 条件用了name,给name建索引,这条查询才能提速; 如果 where 用 age,索引建在 name 上,这条 SQL不会走索引。

    1. pages <---索引(属性)<---查数据 简单理解流程: SQL 查询 → 先去索引页快速定位记录位置 → 再根据索引找到真实数据页读取数据

    ==索引相当于书本的目录,目录(索引)只记录页码,真正内容在正文(数据表)==。

    二、索引分类

    (这里是从约束功能划分,不是底层 B+/HASH)

    注意区分:前面讲的 B+、HASH 是底层数据结构;这里主键 / 普通 / 唯一索引,是逻辑类型,InnoDB 底层默认都是 B + 树

    1. 主键索引(PRIMARY KEY)
      • 主键字段的值不能重复、不能为 NULL
      • 一张表只能有 1 个主键索引,InnoDB 主键是聚簇索引,叶子节点存整行数据
    2. *普通索引(INDEX)【笔记带 ,重点】**
      • 最基础索引,只用来加速查询,不限制字段是否重复、是否为空
      • 字段可以重复,可以 NULL;单纯提升 where 查询速度
    3. 唯一索引(UNIQUE KEY)
      • 索引字段不能重复,但是允许为 NULL(可以多个 null)
      • 两个作用:①加快查询;②自动约束,防止插入重复数据

    对比记忆: 主键:不可重复 + 不可 NULL,一张表只能 1 个 唯一索引:不可重复,允许 NULL,一张表可以多个 普通索引:无重复限制,只提速

    三、索引 SQL 命令(重点,上机默写)

    1. 添加【普通索引】两种写法

    写法 1:alter table 添加索引

    alter table test add index index_name(name);
    • test:表名
    • index_name:你自己起的索引名
    • name:建立索引的字段

    写法 2:create index

    create index index_name on test(name);

    效果和上面完全一样,两种都可以。

    2. 查看索引

    desc table;
    -- 简写查看表结构,能看到索引
    show index from table;
    -- 完整查看这张表所有索引详细信息(考试更推荐这个)

    把 table 替换成真实表名,如 show index from test;

    image-20261017201122273

    image-20261017201346449

    3. 删除索引

    alter table test drop index index_name;

    test 是表名,index_name 是要删除的索引名称

    image-20261017201720016

    4. 添加【唯一索引】

    alter table test add unique key uni_xxx(xxx);

    test:表名;uni_xxx:唯一索引名字;括号 xxx:字段名

    ==添加主键索引:alter table 表名 add primary key(字段);==

    image-20261017202340570

    5、前缀索引

    ==根据字符串字段的前 N 个字符建立索引==

    alter table test add index idx_name(name(10));
    • 含义:对name字段,只取前 10 个字符来构建索引,而不是把完整 name 全部放进索引。
    • 适用场景:varchar、text这类长文本字段。
    • 优点:索引体积更小、占用磁盘少,查询更快;
    • 原理举例:城市名称 Dallas、Denver,只拿开头字母 / 前几个字符做索引,就能快速过滤数据。
    • 一句话:不要直接给很长的字符串整列建索引,优先前缀索引

    ​ ⚠️ 缺点: ​ 只能匹配前缀,where name like '%xxx'(前缀模糊查询)用不上前缀索引; ​ 截取字符数量要选合适,太短区分度差,太长节约空间效果不明显。

    深入理解:

    name(10) 这里的(10)不是索引名字,是定义索引:只把 name 字段前 10 个字符存入索引树。

    • 索引名叫idx_name可以随便改(比如 aaaaaaa),不影响前缀索引功能;
    • ==真正决定它是前缀索引的,是字段后面括号里的长度(10);==
    • 存储:索引 B + 树里面只保存字符串开头 N 位,不是完整字符串。

    ✅ 一句话:前缀索引的本质是索引存储的数据变短,和索引命名无关

    image-20261017210103572


    6、联合索引(复合索引)

    多个字段,合并成一个索引,不是多个独立索引

    -- 建表
    create table people (id int,name varchar(20),age tinyint,money int ,gender enum('m','f'));
    -- 创建联合索引:idx_gam,包含 gender,age,money 三个字段
    alter table people add index idx_gam(gender,age,money);

    ✅ 最核心原则:最左前缀原则

    索引顺序 (gender,age,money) 能走索引的查询:

    1. where gender='m'
    2. where gender='m' and age=20
    3. where gender='m' and age=20 and money=1000

    ❌ 不能正常走这个联合索引: where age=20 或者 where money=1000

    ==跳过了最左边的gender,索引失效==。

    ==幻灯片原话:把最常用来作为条件查询的列放在最前面== ==设计联合索引,查询频率越高、区分度越高的字段,越靠左。==

    通俗理解

    联合索引相当于给书本建目录:先按 gender 分类,同 gender 内部再按 age 排序,age 相同再按 money 排序。 查找必须从目录第一层开始;跳过第一层直接找 age,就没法用这个目录。

    image-20261017210932789


    四、附带两条 count 统计 SQL

    select count(*) from city;
    -- 统计表一共有多少行数据(包含null行)
    select count(distinct(name)) from city;
    -- distinct 去重:统计name字段有多少种不重复的值

    例子:name 列:张三、李四、张三 count(distinct name) 结果 = 2(张三、李四)

    易错点小结

    1. 索引不是越多越好:索引会加快查询,但是减慢新增 / 修改 / 删除(增删改需要同步维护索引)
    2. ==索引建在 where 字段才生效;select 后面的字段建索引,一般不会提速==
    3. 分类区分:
      • 按底层结构:B + 树、HASH、全文索引
      • 按逻辑功能:主键索引、普通索引、唯一索引

    MySQL 索引考点

    1. 前缀索引 只支持字符串类型(varchar),数值类型不能建立前缀索引;语法字段(N),只截取字段前 N 个字符建立索引,show index中通过Sub_part列能看到截取长度。
    2. 联合索引(复合索引) 本质1 个索引,里面包含多个字段,≠ 分别给每个字段单独建 3 个独立单字段索引;字段顺序很关键。
    3. 最左前缀原则(高频考点) 联合索引生效规则:查询条件要从索引最左边开始连续匹配字段,跳过左边字段,索引后面部分就无法使用。

    一句话区分: 前缀索引:同一个字符串字段,只取前面一部分字符; 联合索引:多个不同字段打包成一个索引,遵守最左前缀。

  • MySQL 执行计划(explain)

    MySQL 8.0 官方文档解析:理解查询执行计划(10.8)

    1、 核心概念:什么是查询执行计划(EXPLAIN Plan)

    在 MySQL 中,查询执行计划(Query Execution Plan)是指优化器(Optimizer)为了高效执行一条 SQL 查询,在综合评估了表结构、列定义、索引细节以及 WHERE 子句条件后,所选择的一组最优操作集合。

    • 工作原理:优化器会在多种可用的查找技术中进行成本计算与抉择。例如,对于超大表的查询,优化器会尝试避免全表扫描;对于包含多个连接(JOIN)的查询,优化器会评估不同的表连接顺序,而不是穷举所有组合。
    • EXPLAIN Plan:优化器最终选择的这套最高效的操作集合,即为执行计划,官方也称之为 EXPLAIN 计划。

    2、 核心目标:为什么要使用 EXPLAIN

    EXPLAIN 语句的核心作用在于透视 SQL 的底层执行逻辑。开发者查阅执行计划的主要目标包括:

    1. 识别性能瓶颈:准确找出查询计划中指示需要优化的环节(如索引失效、全表扫描、文件排序等)。
    2. 指导优化动作:结合对 SQL 语法和索引原理的掌握,识别出低效的执行方案,并据此采取行动(如重构 SQL、调整或新增索引)来改进执行计划。

    3、 知识图谱:本节结构概览

    官方文档在“理解查询执行计划”大类下,按进阶逻辑划分为五个子模块:

    1. 10.8.1 用 EXPLAIN 优化查询:基础用法及适用场景。
    2. 10.8.2 EXPLAIN 输出格式:传统表格格式的字段解析(核心重点)。
    3. 10.8.3 扩展 EXPLAIN 输出格式:如 JSON 格式输出,提供更详尽的成本信息。
    4. 10.8.4 获取命名连接的执行计划信息:针对特定连接(Connection ID)的追踪。
    5. 10.8.5 查询性能估计:使用 EXPLAIN ANALYZE 进行真实执行耗时的评估。

    使用explain 优化查询

    1、 核心功能与适用范围

    EXPLAIN 语句是 MySQL 提供的用于获取 SQL 语句执行计划的核心工具。它用于解析 MySQL 优化器将如何执行该语句,包括表的连接方式及连接顺序等关键信息。

    • 支持的语句类型:EXPLAIN 可与 SELECT, DELETE, INSERT, REPLACE 以及 UPDATE 语句结合使用。
    • 针对 SELECT 的扩展:对于 SELECT 语句,EXPLAIN 还会生成额外的执行计划信息,可通过 SHOW WARNINGS 命令查看(详见 10.8.3 扩展输出格式)。
    • 分区表支持:在检查涉及分区表的查询时,EXPLAIN 同样能提供相关执行信息(详见 26.3.5 节)。

    2、 核心应用场景

    1. 索引优化指导:通过 EXPLAIN 的输出,可精准定位查询的瓶颈,从而判断应该在何处添加索引,使语句能够通过索引查找数据,显著提升执行速度。
    2. 表连接顺序优化:可用于检查优化器(Optimizer)是否以最优顺序连接了多张表。虽然可以通过 STRAIGHT_JOIN 提示词强制优化器按 SELECT 语句中表出现的顺序进行连接,但需注意,这可能会禁用半连接转换,从而阻止索引的使用(详见 15.2.13 和 10.2.2.1 节)。
    3. 诊断索引失效:若出现预期内的索引未被使用的情况,应运行 ANALYZE TABLE 命令。该命令用于更新表的统计信息(如键的基数等),统计信息的准确性直接影响优化器的决策(详见 15.7.3.1 节)。

    3、 输出格式配置 (FORMAT 选项)

    EXPLAIN 允许用户选择不同的输出格式:

    • 默认格式:传统表格格式(TRADITIONAL),直观易读。
    • JSON 格式:通过 FORMAT=JSON 选项输出。提供比传统表格更详细、结构化的执行计划信息,尤其适用于复杂查询的深度分析。

    4、 补充说明与高级特性

    • 获取命名连接的执行计划:结合 FOR CONNECTION connection_id 语法,可以获取特定命名连接中正在执行的语句的执行计划信息(详见 10.8.4 节)。
    • 优化器追踪(Optimizer Trace):优化器追踪可提供 EXPLAIN 之外的补充信息。但需注意,追踪输出的格式和内容可能因 MySQL 版本不同而存在差异(详见 10.15 节)。
    • 特殊用法(注释):EXPLAIN tbl_name 是 DESCRIBE tbl_name 或 SHOW COLUMNS FROM tbl_name 的同义词,也可用于获取表的列结构信息(详见 15.8.1 节)。

    explain 输出格式概述

    1. 核心机制与执行顺序

    EXPLAIN 语句的核心作用是解析 MySQL 优化器将如何执行一条语句(适用于 SELECT, DELETE, INSERT, REPLACE, UPDATE)。

    • 输出特征:EXPLAIN 会为语句中涉及到的每一张表返回一行信息。
    • 读取顺序判定:返回结果的行顺序,代表了 MySQL 在执行查询时读取这些表的顺序。
      • 即:MySQL 通常先从结果集的第一行对应的表中查找数据,然后嵌套到第二行、第三行对应的表中。
      • 在处理完所有表并输出选定列后,MySQL 会回溯表列表,寻找更多匹配的行,然后读取下一行数据,继续上述过程。
      • (此特性对于分析多表 JOIN 的驱动表选择至关重要)

    💡 辅助工具提示:MySQL Workbench 提供了可视化的解释功能,能够以图形化的方式直观呈现 EXPLAIN 的输出结果,有助于快速理解复杂的执行计划。

    2.输出字段字典

    EXPLAIN 的每一行输出都包含关于一张表的执行细节。传统的表格格式包含以下列(Column),如果使用 FORMAT=JSON 输出,则对应特定的 JSON 属性名(JSON Name)。

    以下是核心输出列的官方定义与专业解析:

    列名 (Column) JSON 属性名 (JSON Name) 核心含义解析
    id select_id SELECT 标识符。代表查询中 SELECT 的顺序号。
    select_type (无) SELECT 类型。用于区分简单查询、联合查询、子查询等复杂类型。
    table table_name 输出行对应的表名。可能包含派生表、联合结果的临时表名。
    partitions partitions 匹配的分区。记录查询命中的分区,非分区表显示为 NULL。
    type access_type 连接类型。极其重要,表示 MySQL 决定如何查找表中的行(如全表扫描 ALL、索引查找 ref 等)。
    possible_keys possible_keys 可选索引。查询可能使用到的索引列表。
    key key 实际选择的索引。优化器最终决定使用的索引。若为 NULL 则未走索引。
    key_len key_length 所选密钥的长度。表示使用的索引字节长度,可据此判断复合索引的利用情况。
    ref ref 列与索引的比较。显示哪些列或常量被用于与 key 列中的索引进行比较。
    rows rows 预计要检查的行数。优化器预估必须读取的行数,数值越小越好。
    filtered filtered 按表条件过滤的行百分比。表示经过 WHERE 条件过滤后剩余的行的百分比(最大值 100)。
    Extra (无) 附加信息。包含执行计划的额外细节(如 Using filesort, Using index 等),是性能优化的关键指标。

    ⚠️ 官方文档勘误说明:原意应为:“某些 JSON 属性可能没有对应的传统列名,且 JSON 格式输出中包含但在默认格式中不显示的属性(如成本信息),可通过指定 FORMAT=JSON 查看。”


    嵌套循环和行顺序即读取顺序(理解查询)

    真实SQL场景示例

    假设有两张表:

    • 表 A(users 用户表):有 3 条数据(id: 1, 2, 3)。
    • 表 B(orders 订单表):有 4 条数据(属于用户 1 的有 2 条,属于用户 2 的有 1 条,属于用户 3 的有 1 条)。
    • 执行一条关联查询:

    sql

    SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id;

    EXPLAIN 的输出结果

    在 MySQL 中输入 EXPLAIN 后,它会返回如下两行(简略版):

    text

    +----+-------+--------+
    | id | table | type   |  <-- (仅展示关键列)
    +----+-------+--------+
    |  1 | users | ALL    |  <-- 第一行:驱动表
    |  1 | orders| ref    |  <-- 第二行:被驱动表
    +----+-------+--------+

    核心结论:EXPLAIN 输出中的第一行(users)就是外层循环,第二行(orders)就是内层循环。


    ⭐ 动画拆解:MySQL 底层是如何执行的?

    这个过程就像写代码时的双层 for 循环。

    text

    【外层循环】读取第一行表 (users)       【内层循环】读取第二行表 (orders)
           |                                      |
           |--- 取出 user_id = 1 ---------------->| 扫描 orders 表,找到 user_id=1 的订单 (共2条)
           |                                      | 👉 输出第一行组合 (user1 + orderA)
           |                                      | 👉 输出第二行组合 (user1 + orderB)
           |                                      | 👉 当前用户的订单找完了,内层循环结束
           |
           |--- 取出 user_id = 2 ---------------->| 扫描 orders 表,找到 user_id=2 的订单 (共1条)
           |                                      | 👉 输出第三行组合 (user2 + orderC)
           |                                      | 👉 当前用户的订单找完了,内层循环结束
           |
           |--- 取出 user_id = 3 ---------------->| 扫描 orders 表,找到 user_id=3 的订单 (共1条)
           |                                      | 👉 输出第四行组合 (user3 + orderD)
           |                                      | 👉 当前用户的订单找完了,内层循环结束
           |
           |--- users 表的数据也读完了,整个查询结束。
    • 对应图解:在这个例子中,users 表就是驱动表(外层循环),orders 表是被驱动表(内层循环)。如果 users 表有 100 万行,MySQL 就会把 orders 表扫描 100 万次(如果没有索引的话)。所以优化器总是倾向于选择数据量小、过滤后结果少的表来作为第一行(驱动表)。

    3.核心输出列基准定义

    在执行 EXPLAIN 后,默认输出的每一行代表一个表的访问信息。下表定义了核心输出列的含义,以及在使用 FORMAT=JSON 格式输出时所对应的属性名称:

    列名 (Column) JSON 属性名 (JSON Name) 核心含义描述
    id select_id SELECT 标识符。即查询中 SELECT 的序号。
    select_type (无) SELECT 类型。见下文详细说明。
    table table_name 输出行对应的表名。
    partitions partitions 匹配的分区。非分区表为 NULL。
    type access_type 连接类型(极其重要,如 ALL, ref, index 等)。
    possible_keys possible_keys 可选索引。查询可能使用的索引列表。
    key key 实际选择的索引。优化器最终决定使用的索引。
    key_len key_length 所选密钥的长度。用于判断复合索引的利用情况。
    ref ref 列与索引的比较。显示哪些列或常量被用于与索引比较。
    rows rows 预计要检查的行数。预估值,越小越好。
    filtered filtered 按表条件过滤的行百分比。最大值 100。
    Extra (无) 附加信息。如 Using filesort 等关键诊断信息。

    ⚠️ 官方勘误说明: 原意为:“部分属性仅存在于 JSON 格式输出中,默认表格格式不显示;若为 NULL,则代表该字段在当前 EXPLAIN 输出中无对应值。”

         ┌──────────────────────────────────────────────┐
           │  EXPLAIN 输出结果 (相当于一张临时表)         │
           ├──────┬────────────┬──────────┬───────────────┤
     第1行 │ id=1 │ table:users│ type:ALL │ (读取表 users)│
           ├──────┼────────────┼──────────┼───────────────┤
     第2行 │ id=1 │ table:orders│ type:ref│ (读取表 orders)│
           └──────┴────────────┴──────────┴───────────────┘
              │        │
              │        └─ 每一行都有对应的 "table" 列,标明现在在访问哪张表
              │
              └─ 行与行之间的上下顺序,就是 MySQL 读表的先后顺序

    核心列深度解析

    ==1==. id (JSON 名称: select_id)

    • 概念:SELECT 查询的序列号。
    • 特殊情况:如果该行涉及子查询或 UNION 合并,id 列可能会显示为 NULL,或者显示为类似 <unionM,N> 的格式,表示这是 id 值为 M 和 N 的行的并集(Union)结果。

    2. select_type (JSON 名称: 无)

    • 概念:表示 SELECT 查询的类型,用于区分普通查询、联合查询、子查询等复杂场景。
    • JSON 映射说明:在 FORMAT=JSON 格式中,如果该值有对应的 JSON 属性(如 dependent(true)),则会暴露该属性;如果没有,则仅在传统表格中显示。
    • 类型枚举字典:
    select_type 值 JSON 属性名 含义说明
    SIMPLE 无 简单 SELECT。不使用 UNION 或子查询。
    PRIMARY 无 最外层 SELECT。如果包含子查询或 UNION,最外层的查询即为 PRIMARY。
    UNION 无 联合查询。UNION 中的第二个及之后的 SELECT 语句。
    DEPENDENT UNION dependent (true) 依赖联合。UNION 中的第二个或之后的 SELECT,依赖于外部查询(相关子查询)。
    UNION RESULT union_result 联合结果。UNION 合并后的最终结果集。
    SUBQUERY 无 子查询。子查询中的第一个 SELECT。
    DEPENDENT SUBQUERY dependent (true) 依赖子查询。子查询的第一个 SELECT,依赖于外部查询。
    DERIVED 无 派生表。在 FROM 子句中生成的临时表(子查询)。
    DEPENDENT DERIVED dependent (true) 依赖派生表。依赖于另一张表的派生表。
    MATERIALIZED materialized_from_subquery 物化子查询。优化器将子查询结果物化为临时表。
    UNCACHEABLE SUBQUERY cacheable (false) 不可缓存子查询。结果无法缓存,外层查询的每一行都需要重新评估该子查询。
    UNCACHEABLE UNION cacheable (false) 不可缓存联合。属于不可缓存子查询的 UNION 中的第二个或更晚的 SELECT。

    3. table (JSON 属性名: table_name)

    核心含义:当前输出行所访问的表名称。 除了真实的物理表名外,它还可能显示为以下三种特殊临时表格式:

    • <unionM,N>:表示该行指的是 id 值为 M 和 N 的查询结果进行 UNION 并集后的结果。
    • <derivedN>:表示该行指的是 id 值为 N 的派生表(Derived Table)结果。通常来源于 FROM 子句中的子查询。
    • <subqueryN>:表示该行指的是 id 值为 N 的子查询被物化(Materialized)后的结果。

    EXPLAIN 输出中 table 列的 4 种形态

    graph TD
        Root["EXPLAIN 输出中的 table 列"] --> Normal["1. 普通物理表"]
        Root --> Special["2. 特殊临时表 (尖括号包裹)"]
    
        Normal --> N1["形态: 真实的表名 (如 users, orders)"]
        Normal --> N2["含义: 数据库中实际存在的物理表"]
    
        Special --> U["< unionM,N >"]
        Special --> D["< derivedN >"]
        Special --> S["< subqueryN >"]
    
        U --> U1["含义: id 为 M 和 N 的查询 UNION 合并后的结果"]
        D --> D1["含义: 派生表。通常是 FROM 子句里的子查询结果"]
        S --> S1["含义: 物化子查询。WHERE 里被优化器缓存的子查询结果"]
    
        classDef root fill:#f9f2f4,stroke:#d9534f,stroke-width:2px;
        classDef normal fill:#d9edf7,stroke:#31708f,stroke-width:2px;
        classDef special fill:#dff0d8,stroke:#3c763d,stroke-width:2px;
        classDef detail fill:#fcf8e3,stroke:#8a6d3b,stroke-width:1px,stroke-dasharray: 5 5;
    
        class Root root;
        class Normal,N1,N2 normal;
        class Special,U,D,S special;
        class U1,D1,S1 detail;

    ==4==. type (JSON 属性名: access_type)

    核心含义:连接类型(访问类型)。这是极其关键的性能指标,代表了 MySQL 决定如何查找表中的行。

    • (注:具体包含哪些类型及其性能优劣,详见后续章节的“解释连接类型”)。

    ==5==. possible_keys (JSON 属性名: possible_keys)

    核心含义:MySQL 在优化阶段可能选择使用的索引列表(查询中涉及的列上如果有索引,就会列在这里)。

    ⚠️ 官方原文档纠偏与深度解析:

    1. 上下文独立:该列完全独立于 EXPLAIN 输出中显示的表格顺序。这意味着 possible_keys 中列出的一些索引,在实际的表连接操作中可能无法被使用。
    2. 值为 NULL 的情况:如果没有相关索引,该列显示为 NULL(或在 JSON 输出中未定义)。
      • 优化建议:如果该列为 NULL,你可以检查 WHERE 子句,确认查询条件是否涉及了某些列。如果涉及,可以考虑为这些列创建合适的索引,然后使用 EXPLAIN 再次检查查询。
    3. 查看现有索引:如果你想查看某张表上当前已经存在哪些索引,可以使用 SHOW INDEX FROM tbl_name 命令。
    📌 生活化类比:图书检索系统 (EXPLAIN)
    
    你要查的内容: "苹果" (对应你的 WHERE 条件)
    
    +-------------------------------------------------------------+
    | 步骤 1: 检索器(优化器)查阅总目录                          |
    | 发现有两个索引可能包含"苹果"的信息:                        |
    |                                                             |
    |   👉 possible_keys = [ 拼音索引, 偏旁部首索引 ]             |
    |                       (可能用得上,也可能用不上)            |
    +-------------------------------------------------------------+
                                  |
                                  v
    +-------------------------------------------------------------+
    | 步骤 2: 检索器最终决定怎么翻书                              |
    |                                                             |
    |   👉 key = 拼音索引                                         |
    |            (实际选择使用的索引,这里只选了一个)             |
    +-------------------------------------------------------------+
                                  |
                                  v
    +-------------------------------------------------------------+
    | 特殊情况:如果目录里根本没找到相关索引                      |
    |                                                             |
    |   👉 possible_keys = NULL  (警告:可能需要新建索引)         |
    |   👉 key = NULL            (实锤:正在全表扫描,极慢!)     |
    +-------------------------------------------------------------+
    graph TD
        Start["执行 EXPLAIN 分析 SQL"] --> Check{"检查 WHERE / JOIN 条件列"}
    
        Check -->|条件列上存在索引| HasIndex["possible_keys 列出候选索引"]
        Check -->|条件列上无索引| NullIndex["possible_keys 值为 NULL"]
    
        HasIndex --> Optimizer["优化器进行成本计算 (Cost)"]
    
        Optimizer -->|选择最佳方案| Chosen["key 列显示: 实际选用的索引"]
        Optimizer -->|认为全表扫描更快| Ignored["key 列显示: NULL (放弃使用索引)"]
    
        NullIndex --> Action1["💡 优化建议: 针对条件列创建新索引"]
        Action1 --> ReRun["再次执行 EXPLAIN 验证"]
    
        Chosen --> CheckTable["💡 查看表内现有索引: SHOW INDEX FROM tbl_name"]
        Ignored --> CheckTable
    
        classDef warn fill:#fff3cd,stroke:#ffc107,stroke-width:2px;
        classDef success fill:#d4edda,stroke:#28a745,stroke-width:2px;
        classDef process fill:#e2e3e5,stroke:#6c757d,stroke-width:2px;
        classDef info fill:#d1ecf1,stroke:#17a2b8,stroke-width:2px;
    
        class NullIndex,Ignored warn;
        class HasIndex,Chosen success;
        class Start,Check,Optimizer process;
        class Action1,ReRun,CheckTable info;
    ┌────────────────────────────────────────────────────────┐
    │ 🌟 possible_keys 的三大核心认知                        │
    ├────────────────────────────────────────────────────────┤
    │ 1. 它是“候选名单”:只代表有资格参与,不代表一定会被用。│
    │                                                        │
    │ 2. 它是“独立计算”的:不看其他表的连接顺序,只管自己这  │
    │    张表有哪些条件涉及了索引。                          │
    │                                                        │
    │ 3. 它和 key 的区别:possible_keys 是“海选”,key 是最终│
    │    “冠军”。如果海选有,但 key 是 NULL,说明全表扫描更  │
    │    划算;如果海选直接是 NULL,说明你必须加索引了。     │
    └────────────────────────────────────────────────────────┘

    ==6==. key (JSON 属性名: key)

    核心含义:实际使用的索引(密钥)。表示 MySQL 优化器最终决定在执行查询时采用的索引。

    ① 与 possible_keys 的关系与特例

    • 通常情况下,key 列的值必定包含在 possible_keys(候选索引)列表中。
    • 特例情况(极其重要):key 列可能会显示一个不在 possible_keys 列表中的索引。这通常发生在覆盖索引(Covering Index)的场景下。即:虽然没有任何索引能完美用于 WHERE 条件过滤(不用于确定要检索哪些行),但查询所需要读取的所有列都恰好包含在某一个索引中。此时,直接扫描该索引树(全索引扫描)比扫描真实的数据行更高效,因此优化器会直接使用该索引。

    ② 关于 InnoDB 引擎的特殊行为

    • 对于 InnoDB 表,即使查询没有显式选择二级索引(辅助索引),而是选择了主键,key 列也可能显示主键名。这是因为 InnoDB 的二级索引的叶子节点存储的就是主键值。如果 MySQL 找不到高效的可用索引来执行查询,key 列将显示为 NULL(代表未使用索引,通常伴随全表扫描)。

    ③ 强制索引提示 (Index Hints)

    • 如果你发现优化器没有选择最理想的索引,你可以使用强制索引提示来干预优化器的决策:
      • FORCE INDEX:强制使用指定索引。
      • USE INDEX:建议使用指定索引。
      • IGNORE INDEX:强制忽略指定索引。

    ④ 索引统计信息与优化

    • MyISAM 表:对于 MyISAM 存储引擎的表,运行 ANALYZE TABLE 语句有助于优化器收集更准确的数据分布统计信息,从而选择更合适的索引。(MyISAM 表也可以使用 myisamchk --analyze 命令达到同样效果)。

    text

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │ 📌 EXPLAIN 之  key  列核心逻辑拆解                                          │
    └─────────────────────────────────────────────────────────────────────────────┘
                                        │
           ┌────────────────────────────┴────────────────────────────┐
           ▼                                                         ▼
      [ 正常情况 ]                                              [ 特殊情况 ]
      key 存在于 possible_keys 列表中                            key 不在 possible_keys 中
           │                                                         │
           ├─ 🟢 key = 索引名                                        ├─ 🟣 覆盖索引 (Covering Index)
           │  说明:优化器在候选索引中,最终选中了它。              │  说明:虽然 WHERE 没用到,但
           │                                                         │  SELECT 的列全在索引里。
           │                                                         │  扫索引树比回表更高效!
           │                                                         │
           └─ 🔴 key = NULL                                          └─ ⚙️ 强制干预工具
              说明:没有合适索引,或全表扫描                          FORCE INDEX / IGNORE INDEX
              更划算(性能最差警示)。                                可以人为改变优化器选择。
    graph TD
        Start["查看 EXPLAIN 的 key 列"] --> CheckNull{"key 是否为 NULL?"}
    
        CheckNull -->|"是 (YES)"| NullKey["🔴 未使用索引"]
        NullKey --> ExplainNull["含义: 通常是全表扫描 (性能差)<br>或者优化器认为全表扫描更划算"]
        ExplainNull --> Action1["💡 优化建议: 检查统计信息<br>或尝试使用 FORCE INDEX 强制走索引"]
    
        CheckNull -->|"否 (NO)"| CompareKeys{"key 是否在<br>possible_keys 列表中?"}
    
        CompareKeys -->|"在 (YES)"| NormalKey["🟢 正常使用索引"]
        NormalKey --> ExplainNormal["含义: 优化器在候选名单中<br>选出了最优的索引"]
    
        CompareKeys -->|"不在 (NO)"| CoverIndex["🟣 覆盖索引 (Covering Index)"]
        CoverIndex --> ExplainCover["含义: 虽然 WHERE 没用到,<br>但 SELECT 需要的列全在索引中。<br>直接扫索引树,免去回表操作,极快!"]
    
        style Start fill:#f9f2f4,stroke:#d9534f,stroke-width:2px
        style NullKey fill:#f8d7da,stroke:#dc3545,stroke-width:2px
        style NormalKey fill:#d4edda,stroke:#28a745,stroke-width:2px
        style CoverIndex fill:#e2d9f3,stroke:#6f42c1,stroke-width:2px
        style Action1 fill:#fff3cd,stroke:#ffc107,stroke-width:1px,stroke-dasharray: 5 5

    7. ref (JSON 属性名: ref)

    核心含义:显示哪些列或常量被用于与 key 列中命名的索引进行比较,从而从表中读取数据行。

    • 常规情况:如果显示的是具体的列名或 const(常量),说明查询利用这些值去索引中匹配数据。

    • 函数特例:如果该列的值显示为 func,则表示用于比较的值是某个函数的结果。

      • 排查方法:想知道具体是什么函数,需要在执行 EXPLAIN 后,紧接着执行 SHOW WARNINGS 命令,通过查看扩展的 EXPLAIN 输出来获取详情。该函数可能是一个算术运算符等。
      ┌─────────────────────────────────────────────────────────────────────────────┐
      │ 📌 EXPLAIN 之 `ref` 列逻辑拆解 (谁在和索引“比大小”?)                       │
      └─────────────────────────────────────────────────────────────────────────────┘
                                          │
             ┌────────────────────────────┴────────────────────────────┐
             ▼                                                         ▼
        [ 常规情况 ]                                              [ 特殊情况 ]
        显示具体的列名或常量                                      显示为 'func'
             │                                                         │
             ├─ 🟢 ref = const                                         ├─ 🟣 ref = func
             │  含义:用一个固定的常量去索引里查。                    │  含义:用于比较的值,是某个
             │  例如:WHERE id = 1                                    │  函数计算结果。
             │                                                        │
             ├─ 🟢 ref = db.table.column                               │
             │  含义:拿前一张表(驱动表)的某列,                     └─ ⚙️ 如何排查具体是什么函数?
             │  来当前表的索引里查(JOIN 时最典型)。                   执行 EXPLAIN 后,立刻执行
             │  例如:ON A.id = B.user_id                             SHOW WARNINGS;
             │                                                        (查看扩展信息,会显示具体
             └─ 🟢 ref = NULL                                         算术运算符等细节)
                含义:没有用到等值比较(如用了范围查询)。
      graph TD
          Start["查看 EXPLAIN 的 ref 列"] --> TypeCheck{"ref 的值是什么?"}
      
          TypeCheck -->|"具体列名"| ColumnRef["🟢 常规比较 (比如 A.id)"]
          ColumnRef --> Detail1["含义: 拿驱动表的数据列<br>来匹配当前表的索引"]
      
          TypeCheck -->|"const"| ConstRef["🟢 常量比较"]
          ConstRef --> Detail2["含义: 使用固定的常量值<br>直接去索引中精确查找"]
      
          TypeCheck -->|"func"| FuncRef["🟣 函数计算特例"]
          FuncRef --> Detail3["含义: 比较的值是函数运算结果"]
          Detail3 --> Action["⚙️ 排查步骤:<br>执行 SHOW WARNINGS 查看具体函数"]
      
          TypeCheck -->|"NULL"| NullRef["⚪ 无等值比较"]
          NullRef --> Detail4["含义: 可能是范围查询 (如 >, <)<br>或者没有用到索引比较"]
      
          style Start fill:#f9f2f4,stroke:#d9534f,stroke-width:2px
          style ColumnRef fill:#d4edda,stroke:#28a745,stroke-width:2px
          style ConstRef fill:#d4edda,stroke:#28a745,stroke-width:2px
          style FuncRef fill:#e2d9f3,stroke:#6f42c1,stroke-width:2px
          style NullRef fill:#e2e3e5,stroke:#6c757d,stroke-width:2px
          style Action fill:#fff3cd,stroke:#ffc107,stroke-width:1px,stroke-dasharray: 5 5

    ==8==. rows (JSON 属性名: rows)

    核心含义:MySQL 预估为了执行此查询,必须检查的行数(即扫描行数)。

    • 注意(针对 InnoDB 引擎):对于 InnoDB 表,rows 列的数字是一个预估值,可能并不总是完全准确。它是基于统计信息计算出来的,数值越小越好。

    text

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │ 📌 EXPLAIN 之 `rows` 列核心逻辑拆解 (预估扫描行数)                          │
    └─────────────────────────────────────────────────────────────────────────────┘
                                        │
           ┌────────────────────────────┴────────────────────────────┐
           ▼                                                         ▼
      [ 本质含义 ]                                              [ 引擎差异与准确性 ]
           │                                                         │
           ├─ 🎯 它是什么?                                          ├─ 🛑 InnoDB 引擎 (最常用)
           │  优化器认为执行该查询,必须检查                      │  数值是【预估值】(Estimated)
           │  的行数 (即扫描了多少行数据)。                        │  基于采样统计得出,可能不准。
           │                                                         │
           ├─ 📉 评估标准                                            └─ ⚙️ 为什么不准?
           │  【数值越小越好】。                                     因为 InnoDB 面对高频增删改时,
           │  数值越大,说明扫描的数据量越大,                       不会实时更新精确的行数,而是
           │  磁盘 I/O 越高,性能越差。                             采用抽样估算来保证性能。
           │
           └─ ⚠️ 误区提示
              它不代表最终返回给用户的行数。
              它代表的是“为了找到结果,底层翻了多少行”。
    graph TD
        Start["查看 EXPLAIN 的 rows 列"] --> Core["🎯 核心含义: 预估需要扫描的行数"]
    
        Core --> Rule["📉 性能判断原则"]
        Rule --> Better["🟢 数值越小越好<br>(扫描行数少,I/O 压力小)"]
        Rule --> Worse["🔴 数值极大 (如百万级)<br>(扫描行数多,性能极差)"]
    
        Core --> Engine{"针对什么存储引擎?"}
    
        Engine -->|"InnoDB (默认)"| InnoDB["🛑 预估值 (Estimated)"]
        InnoDB --> Explain1["基于索引统计信息估算<br>并非 100% 精确"]
        InnoDB --> Action["💡 建议: 定期执行 ANALYZE TABLE<br>更新统计信息,帮助优化器算得更准"]
    
        Engine -->|"MyISAM"| MyISAM["✅ 精确值 (Exact)"]
        MyISAM --> Explain2["通常直接读取表的真实行数<br>相对准确"]
    
        style Start fill:#f9f2f4,stroke:#d9534f,stroke-width:2px
        style Core fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
        style Better fill:#d4edda,stroke:#28a745,stroke-width:2px
        style Worse fill:#f8d7da,stroke:#dc3545,stroke-width:2px
        style InnoDB fill:#fff3cd,stroke:#ffc107,stroke-width:2px
        style Action fill:#e2e3e5,stroke:#6c757d,stroke-width:1px,stroke-dasharray: 5 5

    ==9==. Extra (JSON 属性名: 无)

    核心含义:附加信息。本列包含 MySQL 如何解析查询的额外细节,是诊断 SQL 性能的黄金字段(例如 Using filesort, Using index 等)。

    • JSON 映射说明:该列没有单一对应的 JSON 属性。然而,这里出现的某些值(如 Using filesort)在 JSON 格式输出中,会作为独立的属性名暴露出来(值为 true),或者直接作为属性文本(message)展示。

    text

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │ 📌 EXPLAIN 之 `Extra` 列核心信息分类 (附加信息诊断)                         │
    └─────────────────────────────────────────────────────────────────────────────┘
                                        │
           ┌────────────────────────────┴────────────────────────────┐
           ▼                                                         ▼
      [ 🚨 性能“红灯”区 (危险) ]                                [ 🟢 性能“绿灯”区 (优秀) ]
           │                                                         │
           ├─ Using filesort                                         ├─ Using index
           │  说明:无法利用索引完成排序,必须在内存或磁盘中         │  说明:使用了【覆盖索引】。查询的列全在索引里,
           │  进行额外的文件排序。性能杀手!                         │  不需要回表查数据,速度极快。
           │                                                        │
           ├─ Using temporary                                        ├─ Using index condition
           │  说明:必须创建临时表来存储中间结果。常见于             │  说明:使用了【索引条件下推 (ICP)】。在存储引擎
           │  GROUP BY 或 DISTINCT 且无合适索引时。性能杀手!       │  层就提前过滤了数据,减少了回表次数。
           │                                                        │
           └─ Using join buffer (Block Nested Loop)                  └─ Select tables optimized away
              说明:表连接时没有用到索引,使用了 Join Buffer。          说明:优化器直接通过索引就拿到了结果
              通常意味着被驱动表缺乏合适的关联索引。                  (如 MIN/MAX),甚至不需要读取表数据。
    graph TD
        Start["查看 EXPLAIN 的 Extra 列"] --> TrafficLight{"信息诊断分类"}
    
        TrafficLight -->|"🚨 危险信号"| RedZone["需要立即优化"]
        TrafficLight -->|"🟢 良好信号"| GreenZone["表现优秀,无需干预"]
        TrafficLight -->|"⚪ 其他情况"| GrayZone["特定场景 / 正常提示"]
    
        %% 红灯区
        RedZone --> Filesort["Using filesort"]
        Filesort --> FS_Detail["含义: 额外排序<br>原因: ORDER BY 字段没走索引"]
        Filesort --> FS_Fix["💡 优化: 为 ORDER BY 字段建索引"]
    
        RedZone --> Temporary["Using temporary"]
        Temporary --> Tmp_Detail["含义: 使用了临时表<br>原因: GROUP BY 等操作没走索引"]
        Temporary --> Tmp_Fix["💡 优化: 优化 SQL 或建立联合索引"]
    
        %% 绿灯区
        GreenZone --> UsingIndex["Using index"]
        UsingIndex --> UI_Detail["含义: 覆盖索引<br>表现: 无需回表,极快!"]
    
        GreenZone --> ICP["Using index condition"]
        ICP --> ICP_Detail["含义: 索引条件下推<br>表现: 存储引擎层提前过滤,减少回表"]
    
        %% 灰灯区
        GrayZone --> UsingWhere["Using where"]
        UsingWhere --> UW_Detail["含义: 使用了 WHERE 过滤<br>说明: 需结合 type 列判断好坏"]
    
        style Start fill:#f9f2f4,stroke:#d9534f,stroke-width:2px
        style TrafficLight fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
    
        style RedZone fill:#f8d7da,stroke:#dc3545,stroke-width:2px
        style Filesort fill:#f8d7da,stroke:#dc3545,stroke-width:1px
        style Temporary fill:#f8d7da,stroke:#dc3545,stroke-width:1px
    
        style GreenZone fill:#d4edda,stroke:#28a745,stroke-width:2px
        style UsingIndex fill:#d4edda,stroke:#28a745,stroke-width:1px
        style ICP fill:#d4edda,stroke:#28a745,stroke-width:1px
    
        style GrayZone fill:#e2e3e5,stroke:#6c757d,stroke-width:2px
        style UsingWhere fill:#e2e3e5,stroke:#6c757d,stroke-width:1px
    
        style FS_Fix fill:#fff3cd,stroke:#ffc107,stroke-width:1px,stroke-dasharray: 5 5
        style Tmp_Fix fill:#fff3cd,stroke:#ffc107,stroke-width:1px,stroke-dasharray: 5 5

    ==explain连接类型(type/access_type)==

    1、 核心概念与评判标准

    核心含义:type 列(JSON 属性名为 access_type)描述了 MySQL 决定如何将表连接起来(即如何查找表中的行)。 性能排序:官方列出的连接类型,自上而下性能依次递减(越往下越差)。我们在优化 SQL 时,核心目标就是让 type 尽量靠前。

    2、 高优连接类型深度解析

    1. system (系统表)

    • 定义:表中只有一行数据(等于系统表)。
    • 性质:这是 const 连接类型极其特殊的一种情况,速度极快。

    ==2==. const (常量级查找)

    • 定义:表最多只有一行匹配的行,且在查询一开始就被读取。

    • 原理:因为只有一行,该行中列的值在查询的剩余部分可以被当作常量(Constant)来对待,由优化器直接优化掉。

    • 性能:极快,因为只读一次。常用于通过主键(PRIMARY KEY)或唯一索引(UNIQUE)与常量值进行比较。

    • 触发条件:将主键/唯一索引的所有部分与常量进行比较。

    • 官方示例:

      sql

      -- 示例 1:单列主键直接命中
      SELECT * FROM tbl_name WHERE primary_key = 1;
      
      -- 示例 2:复合主键的所有列均用常量等值匹配
      SELECT * FROM tbl_name WHERE primary_key_part1 = 1 AND primary_key_part2 = 2;

    3. eq_ref (等值引用 / 唯一索引关联)

    • 定义:对于前几张表的每一行组合,从当前表中最多只读取一行。

    • 性能:除了 system 和 const 之外,这是最佳(最优)的连接类型。

    • 触发条件:当所有被连接的列都是主键(PRIMARY KEY)或非空的唯一索引(UNIQUE NOT NULL),并且使用 = 操作符进行比较时。

    • 比较值来源:比较值可以是一个常量,也可以是外部表的列表达式。MySQL 会使用该表达式去 ref_table 中查找。

    • 官方示例:

      sql

      -- 示例 1:关联表使用主键/唯一索引
      SELECT * FROM ref_table, other_table
        WHERE ref_table.key_column = other_table.column;
      
      -- 示例 2:复合唯一索引的所有部分参与等值关联
      SELECT * FROM ref_table, other_table
        WHERE ref_table.key_column_part1 = other_table.column
        AND ref_table.key_column_part2 = 1;

    4. ref (非唯一索引等值查找)

    • 核心定义:从当前表中读取所有匹配某个索引值的行。这是基于普通(非唯一)索引进行的查找。

    • 触发条件:如果使用的索引仅包含最左前缀,或者该索引不是 PRIMARY KEY 或 UNIQUE 索引(即索引值允许重复)。

    • 性能评估:虽然无法像 eq_ref 那样只锁定一行,但如果该索引只匹配了少部分行,这种连接方式依然是非常不错的。

    • 特例:ref 也可以用于使用 = 或 <=>(NULL 安全等于)操作符的索引列比较。

    • 官方示例:

      sql

      SELECT * FROM ref_table WHERE key_column = expr;
      
      SELECT * FROM ref_table, other_table
        WHERE ref_table.key_column = other_table.column;
      
      -- 复合索引的最左前缀匹配,且部分列为常量
      SELECT * FROM ref_table, other_table
        WHERE ref_table.key_column_part1 = other_table.column
        AND ref_table.key_column_part2 = 1;

    5. ref_or_null (等值查找 + NULL 查找)

    • 核心定义:连接类型类似于 ref,但额外增加了对 NULL 值的搜索。

    • 原文档纠偏:实际含义是:MySQL 会专门对 IS NULL 的情况进行额外检索。

    • 适用场景:这种优化最常见于解析子查询。如果你的查询条件既包含等值匹配,又允许该列为 NULL。

    • 官方示例:

      sql

      -- 既要找等于 expr 的,也要找为空的行
      SELECT * FROM ref_table
      WHERE key_column = expr OR key_column IS NULL;

    ==6==. range (范围扫描)

    • 核心定义:仅检索在给定范围内的行,并使用一个索引来选择行。

    • 关键特征:

      • key 列显示实际使用的索引。
      • key_len 列包含使用的最长密钥部分的长度。
      • ref 列针对此类型通常为 NULL。
    • 触发条件:使用任何一个 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, LIKE 或 IN() 运算符将索引列与常量进行比较。

    • 官方示例:

      sql

      SELECT * FROM tbl_name WHERE key_column = 10;
      SELECT * FROM tbl_name WHERE key_column BETWEEN 10 and 20;
      SELECT * FROM tbl_name WHERE key_column IN (10,20,30);
      -- 复合索引的范围查询
      SELECT * FROM tbl_name WHERE key_part1 = 10 AND key_part2 IN (10,20,30);

    ==7==. index (全索引扫描)

    • 核心定义:连接类型与 ALL 相同,但扫描的是索引树而不是数据行。
    • 具体分两种情况:
      1. 覆盖索引扫描(好情况):如果该索引是覆盖索引,即查询所需要的所有列都在索引中,那么只扫描索引树即可。此时 Extra 列会显示 Using index。因为索引通常比表数据小得多,所以这种扫描通常比 ALL 快。
      2. 按索引顺序全表扫描(较差情况):利用索引按顺序查找数据行。此时 Extra 列中不会出现 Using index。
    • 触发条件:当查询仅选择属于单一索引的列时,MySQL 可能会选择这种方式。

    ==8==. ALL (全表扫描)

    • 核心定义:对之前表中的每个行组合进行全表扫描来查找匹配的行。
    • 性能评价:通常非常糟糕(Very Bad)。如果 table 是第一个没有标记为 const 的表,在大多数其他情况下,这意味着性能灾难。
    • 优化建议:通常,你应通过添加合适的索引来避免全表扫描,使得基于常量值或早期表中的列值来检索行成为可能。

    性能图

    text

    ┌───────────────────────────────────────────────────────────────────────────────────┐
    │ 📌 MySQL EXPLAIN `type` (连接类型) 性能全景精简版                                 │
    │ (优化目标:守住绿黄区,消灭红区!)                                                │
    └───────────────────────────────────────────────────────────────────────────────────┘
    
     🟢 【顶级性能区】 (极快,基本不用管)
     ───────────────────────────────────────────────────────────────────────────────────
       1. system            : 表只有一行(系统表)。极速。
       2. const             : 主键/唯一索引 等值查常量。只读一次,极速。
       3. eq_ref            : 多表 JOIN 时,使用主键/唯一非空索引关联。极优。
     ───────────────────────────────────────────────────────────────────────────────────
    
     🟡 【良好性能区】 (常规优秀查询,日常追求的目标)
     ───────────────────────────────────────────────────────────────────────────────────
       4. ref               : 普通索引等值查找。可能匹配多行,速度尚可。
       5. ref_or_null       : 类似 ref,但额外包含 IS NULL 的查找。
     ───────────────────────────────────────────────────────────────────────────────────
    
     🟠 【警告区】 (扫描范围开始变大,需警惕)
     ───────────────────────────────────────────────────────────────────────────────────
       6. range             : 索引范围扫描 (BETWEEN, >, <, IN 等)。
     ───────────────────────────────────────────────────────────────────────────────────
    
     🔴 【危险区】 (性能断崖式下跌,必须优化)
     ───────────────────────────────────────────────────────────────────────────────────
       7. index             : 全索引扫描。
          ├─ 🟢 覆盖索引 (Extra: Using index) -> 极快,不用回表。
          └─ 🔴 非覆盖索引 (Extra: 无)         -> 较慢,要按索引顺序回表。
       8. ALL               : 全表扫描。💀 性能最差,磁盘 I/O 极高,全力避免!
     ───────────────────────────────────────────────────────────────────────────────────
    graph TD
        subgraph 顶级性能区
            A1["1. system (单行系统表)"]
            A2["2. const (常量/唯一索引等值)"]
            A3["3. eq_ref (主键/唯一非空 JOIN)"]
        end
    
        subgraph 良好性能区
            B1["4. ref (普通索引等值)"]
            B2["5. ref_or_null (等值 + NULL)"]
        end
    
        subgraph 警告区
            C1["6. range (索引范围扫描)"]
        end
    
        subgraph 危险区
            D1["7. index (全索引扫描 - 覆盖快/非覆盖慢)"]
            D2["8. ALL (全表扫描 - 最差!)"]
        end
    
        %% 层级流向
        A1 --> A2 --> A3
        A3 --> B1
        B1 --> B2
        B2 --> C1
        C1 --> D1 --> D2
    
        %% 样式定义
        classDef top fill:#d4edda,stroke:#28a745,stroke-width:2px;
        classDef good fill:#fff3cd,stroke:#ffc107,stroke-width:2px;
        classDef warn fill:#ffcc99,stroke:#fd7e14,stroke-width:2px;
        classDef danger fill:#f5c6cb,stroke:#dc3545,stroke-width:2px;
    
        class A1,A2,A3 top;
        class B1,B2 good;
        class C1 warn;
        class D1,D2 danger;

    ==Type列性能金字塔==(要背)

    ┌─────────────────────────────────────────────────────────────┐
    │       EXPLAIN `type` 列 性能金字塔 (越往上越牛)             │
    └─────────────────────────────────────────────────────────────┘
                                ▲
                               ╱ ╲
                              ╱   ╲
                             ╱ 1 ╲  <--- system (表里只有一行)
                            ╱─────╲
                           ╱       ╲
                          ╱    2    ╲ <--- const (主键/唯一索引 等值查常量)
                         ╱───────────╲
                        ╱             ╲
                       ╱       3       ╲ <--- eq_ref (多表 JOIN 时,使用主键/唯一非空索引)
                      ╱─────────────────╲
                     ╱                   ╲
                    ╱         4           ╲ <--- ref (非唯一索引等值查找)
                   ╱───────────────────────╲
                  ╱                         ╲
                 ╱            5              ╲ <--- range (索引范围扫描)
                ╱─────────────────────────────╲
               ╱                               ╲
              ╱              6                ╲ <--- index (全索引扫描)
             ╱───────────────────────────────────╲
            ╱                                     ╲
           ╱                 7                   ╲ <--- ALL (全表扫描,性能最差)
          ╱─────────────────────────────────────────╲
    graph TD
        A["👑 system<br>(表只有一行,特殊情况)"] --> B["🚀 const<br>(主键/唯一索引 等值查常量,只读一次)"]
        B --> C["⚡ eq_ref<br>(多表 JOIN,主键/唯一非空索引等值关联)"]
        C --> D["✅ ref<br>(非唯一索引等值查找)"]
        D --> E["🟡 range<br>(索引范围扫描,如 >, <, BETWEEN)"]
        E --> F["🟠 index<br>(全索引扫描,扫整个索引树)"]
        F --> G["💀 ALL<br>(全表扫描,性能最差,必须优化)"]
    
        style A fill:#28a745,color:#fff,stroke:#1e7e34,stroke-width:2px
        style B fill:#20c997,color:#fff,stroke:#17a2b8,stroke-width:2px
        style C fill:#17a2b8,color:#fff,stroke:#138496,stroke-width:2px
        style D fill:#ffc107,color:#333,stroke:#d39e00,stroke-width:2px
        style E fill:#fd7e14,color:#fff,stroke:#e36a10,stroke-width:2px
        style F fill:#dc3545,color:#fff,stroke:#bd2130,stroke-width:2px
        style G fill:#7b1fa2,color:#fff,stroke:#4a148c,stroke-width:2px

    多表join实战优化案例

    1. 核心概念:估算连接质量(成本计算)

    在多表嵌套循环连接(Nested Loop Join)中,单看某一行的 rows 无法准确评估总成本。优化器评估成本的逻辑大致为:

    • 总扫描行数估算:将 EXPLAIN 输出中各个表的 rows 值相乘。这个乘积能大致告诉你 MySQL 为了执行该查询,底层需要检查的总行数。
    • 系统变量限制:系统变量 max_join_size 利用这个行积来判断查询是否过大,从而中止那些预估需要检查数十亿行的荒谬查询。

    2. 优化前置条件与初始状态

    假设我们有如下 4 表关联查询语句:

    sql

    EXPLAIN SELECT tt.TicketNumber, tt.TimeIn, ... , et.COUNTRY, et_1.COUNTRY, do.CUSTNMBR
    FROM tt, et, et AS et_1, do
    WHERE tt.SubmitTime IS NULL
      AND tt.ActualPC = et.EMPLOYID
      AND tt.AssignedPC = et_1.EMPLOYID
      AND tt.ClientID = do.CUSTNMBR;

    表结构与索引现状:

    • 关联字段数据类型:tt 表关联字段为 CHAR(10),而 et 和 do 表主键为 CHAR(15)。
    • 初始 EXPLAIN 表现:由于 WHERE 条件导致驱动表全表扫描,且字符长度不匹配导致被驱动表索引失效,所有表的 type 均为 ALL。
    • 性能灾难:rows 乘积为 74 × 2135 × 74 × 3872 = 45,268,558,720 行(450亿次检查),产生严重的笛卡尔积。MySQL 需要极长的时间才能执行完毕。

    3. 逐步优化过程与 EXPLAIN 演变

    🚀 第一次优化:消除字段长度不匹配(单字段)

    • 问题诊断:tt.ActualPC (CHAR(10)) 与 et.EMPLOYID (CHAR(15)) 长度不一致,导致 MySQL 无法利用索引。当类型相同但尺寸不同,或存在隐式类型转换时,均会导致索引失效。

    • 优化操作:

      sql

      ALTER TABLE tt MODIFY ActualPC VARCHAR(15);
    • EXPLAIN 变化:

      • et 表的 type 从 ALL 变为 eq_ref(通过主键关联),rows 降为 1。
      • 扫描行积骤减至约 6 亿行,性能提升了 74 倍,查询时间从数小时/直接崩溃降至几秒钟。

    🚀 第二次优化:消除所有关联字段长度不匹配

    • 优化操作:将 tt 表另外两个关联字段 AssignedPC 和 ClientID 的长度也修改为 15。

      sql

      ALTER TABLE tt MODIFY AssignedPC VARCHAR(15), MODIFY ClientID VARCHAR(15);
    • EXPLAIN 变化:

      • et_1 和 do 表的 type 均变为 eq_ref,rows 均为 1。
      • tt 表的 type 变为 ref(普通索引查找),rows 降至 52。此时连接结构已非常优秀。

    🚀 第三次优化:更新统计信息(终极补丁)

    • 问题诊断:默认情况下,MySQL 假设索引列中的数据是均匀分布的。如果分布极度倾斜,优化器会做出错误的表连接顺序判断。
    • 优化操作:执行 ANALYZE TABLE tt;,告知 MySQL 重新分析键的分布情况。
    • 最终 EXPLAIN 表现:
      • 驱动表 tt 为 ALL(因为 SubmitTime IS NULL 过滤性差,全表扫可能更优)。
      • 所有被驱动表(et, et_1, do)均为 eq_ref,rows 全部为 1。此时连接达到完美状态。

    ==面试题==:

    第一关:B+ 树长什么样?(为什么磁盘 I/O 少?)

    核心思想:把树变“胖”,把树变“矮”。

    text

                      【 B+ 树结构示意图 (非叶子节点只存索引) 】
    
                                 [ 15 | 30 ]                     <--- 第1层:根节点 (常驻内存)
                                /     |     
                               /      |      
                   [ 5 | 10 ]       [ 20 | 25 ]       [ 35 | 40 ]  <--- 第2层:树干节点 (磁盘块)
                  /    |           /    |           /    |    
                ...   ...   ...   ...   ...   ...   ...   ...   ...
                /      |           /     |          /      |      
             [1,2]  [5,6]  [9,10] [15,18][20,22][25,28][30,33][35,38][40,45] <--- 第3层:叶子节点(存数据)
               |      |      |      |      |      |      |      |      |
               +------+------+------+------+------+------+------+------+
                                (双向链表,支持范围查询)
    graph TD
        %% 根节点 (第一层)
        Root["[ 15 | 30 ]<br>(根节点: 常驻内存)"]
    
        %% 树干节点 (第二层)
        L2_1["[ 5 | 10 ]<br>(树干: 存索引)"]
        L2_2["[ 20 | 25 ]<br>(树干: 存索引)"]
        L2_3["[ 35 | 40 ]<br>(树干: 存索引)"]
    
        Root --> L2_1
        Root --> L2_2
        Root --> L2_3
    
        %% 叶子节点 (第三层)
        L3_1["[1,2]"]
        L3_2["[5,6]"]
        L3_3["[9,10]"]
        L3_4["[15,18]"]
        L3_5["[20,22]"]
        L3_6["[25,28]"]
        L3_7["[30,33]"]
        L3_8["[35,38]"]
        L3_9["[40,45]"]
    
        L2_1 --> L3_1
        L2_1 --> L3_2
        L2_1 --> L3_3
    
        L2_2 --> L3_4
        L2_2 --> L3_5
        L2_2 --> L3_6
    
        L2_3 --> L3_7
        L2_3 --> L3_8
        L2_3 --> L3_9
    
        %% 叶子节点之间的链表关系 (用虚线表示)
        L3_1 -.-> L3_2 -.-> L3_3 -.-> L3_4 -.-> L3_5 -.-> L3_6 -.-> L3_7 -.-> L3_8 -.-> L3_9
    
        %% 节点样式
        classDef root fill:#f9f2f4,stroke:#d9534f,stroke-width:2px;
        classDef branch fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
        classDef leaf fill:#d4edda,stroke:#28a745,stroke-width:2px;
    
        class Root root;
        class L2_1,L2_2,L2_3 branch;
        class L3_1,L3_2,L3_3,L3_4,L3_5,L3_6,L3_7,L3_8,L3_9 leaf;

    💡 为什么快?

    • 极度矮胖:假设一个节点 16KB,一个主键+指针占 10 字节,一个节点能存 1600 个指针。3 层高的 B+ 树,能存 1600 * 1600 * 1600 约等于 40 亿条数据!
    • 磁盘 I/O 极少:找一条数据,只需要从根节点到叶子节点,只发生 3 次磁盘交互。相比二叉树动辄二三十次的 I/O,简直是降维打击。

    第二关:聚簇索引 vs 二级索引(回表与覆盖索引)

    核心思想:主键索引是“字典正文”(存全量数据),普通索引是“目录”(只存主键)。

    text

      【 聚簇索引 (主键 id) 】                        【 二级索引 (普通字段 name) 】
    
               [ 根节点: id=30 ]                               [ 根节点: name='Bob' ]
    
                     |                                               |
    
            -----------------                       -------------------------
            |               |                       |                       |
      [ id=10 ]        [ id=50 ]              [ Alice ]               [ Charlie ]
    
         |                |                       |                       |
    
     -----------      -----------             -----------             -----------
     |         |      |         |             |         |             |         |
    [id=1,     [id=10, [id=50,  [id=60,    [Alice,    [Bob,       [Charlie, [David,
     data..]   data..] data..]  data..]     id=2]      id=30]      id=50]     id=60]
     (叶子节点包含完整的一行数据)               (叶子节点只包含 name + 主键id)
    graph TD
        subgraph 聚簇索引_主键id
            A_Root["根节点: id=30"]
            A_B1["分支: id=10"]
            A_B2["分支: id=50"]
    
            A_Root --> A_B1
            A_Root --> A_B2
    
            A_L1["[id=1, 完整data...]"]
            A_L2["[id=10, 完整data...]"]
            A_L3["[id=50, 完整data...]"]
            A_L4["[id=60, 完整data...]"]
    
            A_B1 --> A_L1
            A_B1 --> A_L2
            A_B2 --> A_L3
            A_B2 --> A_L4
    
            A_L1 -.-> A_L2 -.-> A_L3 -.-> A_L4
        end
    
        subgraph 二级索引_普通字段name
            B_Root["根节点: name='Bob'"]
            B_B1["分支: Alice"]
            B_B2["分支: Charlie"]
    
            B_Root --> B_B1
            B_Root --> B_B2
    
            B_L1["[Alice, id=2]"]
            B_L2["[Bob, id=30]"]
            B_L3["[Charlie, id=50]"]
            B_L4["[David, id=60]"]
    
            B_B1 --> B_L1
            B_B1 --> B_L2
            B_B2 --> B_L3
            B_B2 --> B_L4
    
            B_L1 -.-> B_L2 -.-> B_L3 -.-> B_L4
        end
    
        %% 样式定义
        classDef rootStyle fill:#f9f2f4,stroke:#d9534f,stroke-width:2px;
        classDef branchStyle fill:#e1f5fe,stroke:#0288d1,stroke-width:2px;
        classDef leafStyleA fill:#fff3cd,stroke:#ffc107,stroke-width:2px;
        classDef leafStyleB fill:#d4edda,stroke:#28a745,stroke-width:2px;
    
        class A_Root,B_Root rootStyle;
        class A_B1,A_B2,B_B1,B_B2 branchStyle;
        class A_L1,A_L2,A_L3,A_L4 leafStyleA;
        class B_L1,B_L2,B_L3,B_L4 leafStyleB;

    💡 什么是回表?

    • 你在【二级索引】里查到了 name='Bob',拿到了它的主键 id=30。
    • 但是你需要 SELECT age 字段,二级索引里没有。你只能拿着 id=30 再去【聚簇索引】里查一遍,拿到完整数据。这就是“回表”。

    💡 什么是覆盖索引?

    • 如果你写 SELECT id, name FROM table WHERE name = 'Bob'。
    • 你要的 id 和 name 在【二级索引】的叶子节点里全都有。MySQL 直接返回,不需要回表。这就是覆盖索引(对应 EXPLAIN 里的 Extra: Using index)。

    第三关:最左前缀原则(联合索引的底层排序)

    核心思想:先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。

    假设你建了联合索引 INDEX(a, b, c),数据在底层的排列顺序是这样的:

    text

      【 联合索引 (a, b, c) 叶子节点底层排序图 】
    
      |  a  |  b  |  c  |  主键 id (回表用) |
      |-----|-----|-----|-----------------|
      |  1  |  10 |  50 |      100        |
      |  1  |  10 |  99 |      200        |  <-- a相同(都是1),按b排序(b=10)
      |  1  |  20 |  30 |      300        |  <-- b相同,按c排序
      |  2  |   5 |  80 |      400        |  <-- a变了,b重新开始排
      |  2  |  10 |  20 |      500        |
      |  3  |  50 |  70 |      600        |
    

    image-20261019003024604

    💡 为什么查 b 用不上索引?

    • 你问:WHERE b = 10。
    • 在这个“大字典”里,a 是有顺序的(1,1,1,2,2,3…),但 b 的全局顺序是乱的(10, 10, 20, 5, 10, 50)。因为 a=1 的 b=20 比 a=2 的 b=5 排在前面。MySQL 没法利用 B+ 树查找,只能全表扫。

    💡 为什么查 a, b 就能用上?

    • 你问:WHERE a = 2 AND b = 10。
    • MySQL 先在 B+ 树里定位到 a=2 的区域,在这个区域里,b 是有序的(5, 10)。可以直接利用二分查找快速定位。

    💡 为什么遇到范围查询 a > 1 AND b = 10 就断了?

    • a > 1 找出了 a=2 和 a=3 的所有数据。
    • 在 a=2 到 a=3 这个跨度里,b 的值有 5, 10, 50… 又变成全局无序的了。所以 b 的索引失效,发生部分索引失效。

    📝 面试终极拷问:

    面试官:“如果我对 (a, b, c) 建立索引,执行 SELECT * FROM t WHERE a=1 AND c=3,能用到索引吗?”

    • 你的回答:“能用上,但只能用 a 的部分。因为跳过了 b,c 就无法利用索引的有序性了。但因为 a=1 已经过滤了大部分数据,所以还是比全表扫快。不过在 EXPLAIN 里,key_len 的长度只会算上 a 的部分。”
  • Redis 数据类型之字符串,列表,集合,有序集合

    Redis 支持五种基本数据类型,每种数据类型都有其特定的应用场景和操作命令。理解这些数据类型的特点和使用方法是掌握Redis的关键。

    ==字符串== (String)

    字符串的特点和应用

    字符串是Redis 最基本的数据类型,也是最常用的类型。在 Redis 中,字符串是二进制安全的,可以存储任何数据,包括数字、文本、序列化对象、甚至是图片。

    主要特点:

    • 二进制安全:可以存储任何二进制数据
    • 最大长度:单个字符串最大 512MBR
    • 编码优化:Redis 会根据内容自动选择最优编码
    • 原子操作:所有字符串操作都是原子性的

    常见应用场景:

    • 缓存数据(JSON、XML、HTML 等)
    • 计数器(访问量、点赞数等)
    • 分布式锁
    • 会话存储
    • 配置信息存储

    基本操作命令

    设置和获取

    # 设置键值
    # 用法:SET key value
    SET name "张三"
    SET age 25
    
    # 获取值
    # 用法:GET key
    GET name        # 返回 "张三"
    GET age         # 返回 "25"
    GET nonexistent # 返回 (nil)
    
    # 批量设置
    # 用法:MSET key1 value1 key2 value2 key3 value3
    MSET user:1:name "张三" user:1:age 25 user:1:city "北京"
    
    # 批量获取
    # 用法:MGET key1 key2 key3
    MGET key1 key2 key3
    MGET user:1:name user:1:age user:1:city
    
    # 设置并返回旧值
    # 用法:GETSET key newvalue
    GETSET key newvalue
    GETSET counter 100  # 返回旧值,设置新值为100

    设置和获取 (SET / GET)

    这是 Redis 最基础的读写操作。

    • SET key value
      • 作用:设置一个键(key)的值(value)。如果 key 已存在,会覆盖旧值。
      • 图片示例:
        • SET name "张三":存入一个键 name,值为字符串 "张三"。
        • SET age 25:存入一个键 age,值为 25。
        • 💡 注意:在 Redis中,数字也是以字符串形式存储的。
    • GET key
      • 作用:获取指定键的值。如果键不存在,返回 nil(空)。
      • 图片示例:
        • GET name:返回 "张三"。
        • GET age:返回 "25"。
        • GET nonexistent:因为不存在这个键,所以返回 (nil)。

    批量设置 (MSET – Multi SET)

    当你需要同时设置多个键值对时,用这个命令可以减少网络请求次数,提高效率。

    • MSET key1 value1 key2 value2 ...
      • 作用:同时设置一个或多个 key-value 对。
      • 图片示例:
        • MSET user:1:name "张三" user:1:age 25 user:1:city "北京"
        • 这里使用了冒号 : 作为命名空间分隔符。这是一种很好的编程习惯,它实际上存了三个键:user:1:name、user:1:age、user:1:city,看起来就像是一个用户的属性集合。

    批量获取 (MGET – Multi GET)

    与 MSET 对应,用于一次性获取多个键的值。

    • MGET key1 key2 key3 ...
      • 作用:同时获取一个或多个 key 的值。返回一个列表,顺序与请求的 key 顺序一致。如果某个 key 不存在,对应位置返回 nil。
      • 图片示例:
        • MGET user:1:name user:1:age user:1:city
        • 这会一次性把刚才存进去的三个值全部取出来,返回结果类似:1) "张三" 2) "25" 3) "北京"。

    设置并返回旧值 (GETSET)

    这是一个结合了“修改”和“查询”的原子操作。

    • GETSET key newvalue
      • 作用:将给定 key 的值设为 newvalue,并返回 key 的旧值。
      • 图片示例:
        • GETSET counter 100:把 counter 的值设为 100,同时返回它之前的值(如果有的话,比如返回 "50")。
        • 💡 这个命令在需要重置计数器(比如每天清零)并获取昨日统计数据时非常有用。
        • ⚠️ 注意:在较新的 Redis版本中,官方更推荐使用 SET key value GET 来代替 GETSET,因为功能更强大且更通用。

    image-20261019160949566

    image-20261019161349073


    条件设置

    # 仅当键不存在时设置
    # 用法:SETNX key value
    SETNX lock:resource "server1"  # 分布式锁应用
    
    # 设置键值和过期时间
    # 用法:SETEX key seconds value
    SETEX session:abc123 3600 "user_data"  # 3600秒后过期
    
    # 仅当键不存在时设置,并指定过期时间
    # 用法:SET key value EX seconds NX
    SET lock:order:1001 "processing" EX 30 NX
    
    # 仅当键存在时设置
    # 用法:SET key value XX
    SET existing_key "new_value" XX

    image-20261019170652915


    RedisString 类型高级应用:锁、会话与条件更新

    1. 核心命令演进

    命令语法 核心作用 原子性 缺陷/适用场景 推荐指数
    SETNX key value 仅当 key 不存在时设置(互斥锁雏形) 是 极易死锁。若客户端宕机未释放,锁永远存在。 ⭐ (已淘汰,仅供了解)
    SETEX key sec value 设置值并指定过期时间(会话管理) 是 无法做到“不存在才设置”。适用于覆盖写入、会话存储。 ⭐⭐⭐
    SET key val EX sec NX 不存在才设置,且带过期时间 是 现代分布式锁的终极标准方案,完美解决死锁。 ⭐⭐⭐⭐⭐
    SET key val XX 仅当 key 存在时更新(条件更新) 是 防止无中生有,常用于更新已存在的键,防止脏数据。 ⭐⭐⭐⭐

    2. 核心命令深度剖析

    1. 分布式锁的终极方案:SET ... EX ... NX

    • 代码示例:SET lock:order:1001 "server_1" EX 30 NX
    • 专业解析:
      • NX (Not eXists):保证互斥性,只有一个客户端能抢到锁。
      • EX 30:保证防死锁。即使客户端 A 抢到锁后突然崩溃,30秒后锁自动释放,不会阻塞系统。
      • 原子性:早期使用 SETNX + EXPIRE 是两步操作,如果在这两步之间发生宕机,依然会死锁。而 SET 配合 EX 和 NX 参数,是 Redis 底层的一条单线程原子命令,绝对安全。

    2. 会话管理的神器:SETEX

    • 代码示例:SETEX session:abc123 3600 "user_data"
    • 专业解析:Redis 常被用作分布式 Session 缓存。SETEX 强制要求设置过期时间,避免了因忘记设置过期时间导致 Redis 内存被无用的 Session 撑爆。

    3. 条件更新的守卫:SET ... XX

    • 代码示例:SET existing_key "new_value" XX
    • 专业解析:XX (eXists) 保证了只有键存在时才更新。在并发场景下,如果多个线程试图修改同一数据,XX 可以防止“预期修改现有数据,结果却创建了新键”的意外情况。

    ==3==.Redis分布式锁(面试题)

    虽然 SET key value EX 30 NX 解决了死锁和互斥问题,但在极端的高并发生产环境下,依然存在隐患,这也是大厂面试最爱问的:

    ⚠️ 风险:误删别人的锁

    • 场景:客户端 A 抢到锁并设置 30 秒过期。但 A 的业务逻辑执行了 35 秒。第 31 秒时,锁自动过期,客户端 B 抢到了锁。第 35 秒,A 执行完毕,直接执行了 DEL lock,结果把 B 的锁给删了。
    • 解决方案(Lua 脚本):解锁时,必须先 GET 判断当前锁的 value 是不是自己的(比如判断是不是 “server_1″),如果是,再 DEL。必须用Lua 脚本执行,以保证判断和删除的原子性。

    lua

    -- 安全的解锁 Lua 脚本逻辑
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end
    • (注:如果要做到绝对完美,还需要解决“锁过期了但业务没执行完”的问题,即引入 Redisson框架的看门狗(Watchdog)机制,实现自动续期。)

    1. 核心概念与基石

    • 为什么需要? 单机环境用 synchronized 即可。但在分布式集群/微服务架构下,多进程需访问同一共享资源(如扣减库存),本地锁失效,必须引入跨进程的全局互斥锁。
    • 为什么选 Redis? 基于内存、性能极高;单线程模型天然具备操作的原子性。

    2、 流程与四大痛点(面试重点)

    1. 阶段一:基础互斥(解决并发问题)

    • 命令:SETNX lock_key value (Set if NoteXists)
    • 逻辑:返回 1 代表抢锁成功,0 代表失败;业务结束用 DEL 释放。
    • 致命缺陷:死锁。若客户端抢锁后崩溃,未执行 DEL,锁永远存在。

    2. 阶段二:防死锁(解决过期问题)

    • 错误做法:SETNX 成功后,再单独执行 EXPIRE 设置过期时间。这俩不是原子操作,中间宕机依然死锁。
    • 正确做法:使用一条原子命令 SET lock_key value EX 30 NX。一步到位实现“不存在才设置”且“自动过期”,彻底解决死锁。

    3. 阶段三:防误删(解决原子性与越界问题)

    • 新隐患:业务执行时间 > 锁过期时间。客户端 A 没执行完锁就过期,B 抢到了锁;A 执行完后顺手 DEL,把 B 的锁删了。
    • 解决方案:
      1. 唯一标识:value 存客户端唯一 ID(如 UUID)。
      2. Lua 脚本:释放锁时,用Lua 脚本(保证原子性)判断:若 value 等于自己的 ID,才执行 DEL。

    4. 阶段四:自动续期(终极救星:Redisson 看门狗)

    • 新隐患:如果预估不好过期时间,设短了业务没做完锁就没了;设长了客户端宕机则长时间死锁。
    • 终极方案:引入 Redisson 框架的 Watchdog(看门狗)机制。
      • 机制:不指定过期时间时(默认 30s),后台起定时任务,每 10s 检查当前线程是否存活。若存活,自动将锁重置为 30s(自动续期);若宕机,停止续期,30s 后释放。

    ==🐶==终极解法:Redisson 的看门狗机制 (Watchdog)

    为了解决“过期时间设长了容易死锁,设短了业务没执行完”这个矛盾,Java 生态中最强大的Redis客户端——Redisson,引入了看门狗机制。

    1. 它是怎么工作的?

    • 不写死过期时间:当你使用Redisson加锁时(例如 lock.lock()),如果不手动指定时间,它默认的过期时间是 30 秒。
    • 后台定时任务(看门狗):一旦加锁成功,Redisson会在后台启动一个定时任务。
    • 自动续期:这个“狗”每隔 10 秒(也就是过期时间的 1/3)就会去检查一下:客户端 A 还在执行吗?如果是,它就会给锁重新设置为 30 秒。相当于无限续杯。
    • 停止续期:一旦客户端 A 的业务执行完毕,主动调用 unlock(),或者客户端 A 真的直接宕机了(那么“狗”也跟着死了,无法续期),锁就会在 30 秒后真正过期释放。

    2. 核心逻辑图解

    text

    客户端 A 抢到锁 (过期时间 30s)
      |
      +---> 启动看门狗
      |
      +---> 执行业务 (耗时 40s)
              |
              +--- 第 10s:看门狗检查 -> A 还在运行 -> 续期到 30s
              +--- 第 20s:看门狗检查 -> A 还在运行 -> 续期到 30s
              +--- 第 30s:看门狗检查 -> A 还在运行 -> 续期到 30s
              +--- 第 40s:业务执行完毕,主动解锁。
      |
      +---> 看门狗停止。

    💡 面试总结(必背)

    “Redis 分布式锁的演进路径是:SETNX 实现基础互斥,SET NX EX 解决死锁,Lua 脚本 + 唯一 ID 解决误删,Redisson 看门狗 解决业务超时自动续期。 在工程落地中,Java 体系首选引入Redisson 框架。”


    字符串操作

    # 追加字符串
    # 用法:APPEND key value
    SET message "Hello"
    APPEND message " World"  # message 现在是 "Hello World"
    
    # 获取字符串长度
    # 用法:STRLEN key
    STRLEN message  # 返回 11
    
    # 获取子字符串
    # 用法:GETRANGE key start end
    GETRANGE message 0 4   # 返回 "Hello"
    GETRANGE message -5 -1 # 返回 "World"
    
    # 设置子字符串
    # 用法:SETRANGE key offset value
    SETRANGE message 6 "Redis"  # message 现在是 "Hello Redis"

    追加字符串 (append)

    SET message "Hello"
    APPEND message " World
    • SET message "Hello":将键 message 的值设置为字符串 "Hello"。
    • APPEND message " World":将值 " World" 追加到键 message 现有值的末尾。
    • 执行结果:message 的值变为 "Hello World"。该命令返回追加后字符串的总长度(即 11)。

    获取字符串长度 (strlen)

    STRLEN message
    • STRLEN message:获取键 message 所对应字符串的字节长度。
    • 执行结果:返回 11(对应 “Hello World” 11个英文字符)。
    • 注:由于Redis 采用 UTF-8 编码,如果值是中文(如“张三”),返回的长度将是 6(每个中文字符占 3 个字节)。

    获取子字符串 (getrange)

    GETRANGE message 0 4
    GETRANGE message -5 -1
    • GETRANGE message 0 4:截取键 message 的值,从索引 0 开始,到索引 4 结束(包含索引 4)。
    • 执行结果:返回 "Hello"。
    • GETRANGE message -5 -1:使用负数索引截取。-1 代表最后一个字符,-5 代表倒数第 5 个字符。
    • 执行结果:返回 "World"。

    设置子字符串 (setrange)

    SETRANGE message 6 "Redis"
    • SETRANGE message 6 "Redis":从索引 6(即第 7 个字符)开始,用字符串 "Redis" 覆盖键 message 原有的字符。
    • 执行结果:原值 "Hello World" 的索引 6 到 10 的 "World" 被替换,message 的值变为 "Hello Redis"。该命令返回修改后字符串的总长度。

    image-20261019184213209

    数值操作

    Redis 可以将字符串当作数字进行操作,支持整数和浮点数。

    # 整数操作
    SET counter 10
    
    # 增加指定值
    INCR counter        # counter = 11
    INCRBY counter 5    # counter = 16
    DECR counter        # counter = 15
    DECRBY counter 3    # counter = 12
    
    # 浮点数操作
    SET price 19.99
    INCRBYFLOAT price 5.01  # price = 25.00
    INCRBYFLOAT price -2.50 # price = 22.50
    
    # 应用示例:网站访问计数
    INCR page:home:views               #  通常用于统计首页的访问量(PV)
    INCR user:1001:login_count         #  
    INCRBY article:123:likes 1

    image-20261019185624330


    ==列表== (List)

    列表的特点和应用

    Redis列表是简单的字符串列表,按照插入顺序排序。可以添加元素到列表的头部或尾部。

    主要特点:

    • 有序性:元素按插入顺序排列
    • 可重复:允许重复元素
    • 双端操作:支持头部和尾部操作
    • 最大长度:理论上可包含 2^32 – 1 个元素

    常见应用场景:

    • 消息队列
    • 最新消息列表
    • 用户操作历史
    • 任务队列
    • 时间线功能

    基本操作命令

    Redis四大核心字母缩写

    1. NX = Not eXists(不存在)

    • 中文含义:仅当键不存在时才执行操作(无中生有)。
    • 常见命令:SETNX、SET key value NX
    • 核心场景:抢锁、防覆盖。只有当前没人占用这个坑(键不存在),我才能把名字写进去。
    • 反向兄弟:XX (eXists),仅当键存在时才执行(锦上添花,常用于更新数据防止脏插)。

    2. EX = EXpire/ EXpires(过期)

    • 中文含义:设置键的过期时间(单位:秒)。
    • 常见命令:SETEX、SET key value EX 30
    • 核心场景:防死锁、存会话。给数据加一个“定时炸弹”,时间一到,Redis自动把数据删掉。

    3. L / R = Left / Right (左 / 右)

    • 中文含义:代表操作的方向(L 代表从左边/头部,R 代表从右边/尾部)。
    • 常见命令:
      • LPUSH / RPUSH(从左/右塞入)
      • LPOP / RPOP(从左/右弹出)
    • 核心场景:队列/栈操作。
      • 用 LPUSH 进,RPOP 出,就是先进先出(FIFO,消息队列)。
      • 用 LPUSH 进,LPOP 出,就是后进先出(LIFO,栈)。
    • (注:如果命令里没有 R,只有单独的 L,比如 LRANGE,这里的 L 就是纯指 List 数据类型)。

    4. B = Block(阻塞)

    • 中文含义:==阻塞等待。命令加上 B,就意味着如果拿不到数据,程序不会立刻返回空,而是原地“死等”,直到有数据来或超时==。
    • 常见命令:BLPOP、BRPOP、BRPOPLPUSH
    • 核心场景:==高可用消息队列。生产者没发消息时,消费者就挂起(不占用 CPU 疯狂死循环),一旦有消息,瞬间唤醒并拿走==。

    添加元素

    # 从左侧(头部)添加
    # 用法:LPUSH key element [element ...]
    LPUSH messages "消息3" "消息2" "消息1"
    # 结果:["消息1", "消息2", "消息3"]
    
    # 从右侧(尾部)添加
    # 用法:RPUSH key element [element ...]
    RPUSH messages "消息4" "消息5"
    # 结果:["消息1", "消息2", "消息3", "消息4", "消息5"]
    
    # 仅当列表存在时添加
    # 用法:LPUSHX key element
    # 用法:RPUSHX key element
    LPUSHX messages "消息6"
    RPUSHX messages "消息7"

    从左侧(头部)添加:LPUSH

    # 用法: LPUSH key element [element ...]
    LPUSH messages "消息3" "消息2" "消息1"
    • 原理:L 代表 Left。Redis会按你书写的顺序,将元素依次压入列表的最左侧。
    • 执行过程:先压入“消息3”,再把“消息2”压到最前面,最后把“消息1”压到最前面。
    • 返回结果:["消息1", "消息2", "消息3"] (注意顺序是反过来的)。

    从右侧(尾部)添加:RPUSH

    # 用法: RPUSH key element [element ...]
    RPUSH messages "消息4" "消息5"
    • 原理:R 代表 Right。将元素依次追加到列表的最右侧(尾部)。
    • 执行结果:在原有列表基础上追加,最终变为 ["消息1", "消息2", "消息3", "消息4", "消息5"]。

    仅当列表存在时添加:LPUSHX / RPUSHX

    # 用法: LPUSHX key element
    # 用法: RPUSHX key element
    LPUSHX messages "消息6"
    RPUSHX messages "消息7"
    • 原理:带有 X(eXists)后缀。只有当 messages 这个键已经存在时,才会执行添加。
    • 特性:如果前面没有创建 messages 列表,这两条命令直接返回 0 且什么都不做。防止因为拼写错误生成无用的空列表。image-20261019200501733

    获取元素

    # 获取指定范围的元素
    # 用法:LRANGE key start stop
    LRANGE messages 0 -1    # 获取所有元素
    LRANGE messages 0 2     # 获取前3个元素
    LRANGE messages -3 -1   # 获取最后3个元素

    image-20261019200810638

    删除元素

    # 从左侧(头部)删除
    # 用法:LPOP key
    LPOP messages           # 弹出并返回第一个元素
    
    # 从右侧(尾部)删除
    # 用法:RPOP key
    RPOP messages           # 弹出并返回最后一个元素
    
    # 在指定元素前/后插入
    # 用法:LINSERT key BEFORE|AFTER pivot element
    LINSERT messages BEFORE "消息3" "消息2.5"

    从左侧(头部)删除:LPOP

    # 用法: LPOP key
    LPOP messages
    • 详细解释:L 代表 Left。这个命令会将列表最左边(头部)的第一个元素移除,并将这个被移除的元素返回给你。

    从右侧(尾部)删除:RPOP

    # 用法: RPOP key
    RPOP messages
    • 详细解释:R 代表 Right。这个命令会将列表最右边(尾部)的最后一个元素移除,并返回给你。

    在指定元素前/后插入

    # 在指定元素前/后插入
    # 用法: LINSERT key BEFORE|AFTER pivot element
    LINSERT messages BEFORE "消息3" "消息2.5"
    • 注意:LINSERT 其实是插入命令
    • 详细解释:它的作用是在列表中找到指定的基准元素(pivot),然后在它的前面(BEFORE)或后面(AFTER)插入新元素。
    • 举例:假设列表现在是 ["A", "B", "C"]。
      • 执行 LINSERT messages BEFORE "B" "X"。
      • 结果会变成 ["A", "X", "B", "C"]。
    • 特别注意:如果基准元素(比如图里的 “消息3″)在列表里找不到,这个命令会返回 -1,代表插入失败。

    image-20261019202146535


    获取元素

    # 获取指定范围的元素
    # 用法:LRANGE key start stop
    LRANGE messages 0 -1    # 获取所有元素
    LRANGE messages 0 2     # 获取前3个元素
    LRANGE messages -3 -1   # 获取最后3个元素
    
    # 获取指定索引的元素
    # 用法:LINDEX key index
    LINDEX messages 0       # 获取第一个元素
    LINDEX messages -1      # 获取最后一个元素
    
    # 获取列表长度
    # 用法:LLEN key
    LLEN messages           # 返回列表长度

    获取指定索引的元素:LINDEX

    如果你只需要列表里的某一个特定位置的元素,用这个命令性能更高。

    • LINDEX messages 0
      • 解释:获取列表的第一个元素(最左边)。
    • LINDEX messages -1
      • 解释:获取列表的最后一个元素(最右边)。
    • ⚠️ 避坑提示:如果索引超出了列表长度(比如列表只有 3 个元素,你查索引 5),它不会报错,而是返回 (nil)(空)。

    获取列表长度:LLEN

    • LLEN messages
      • 解释:返回当前列表中包含的元素总个数(Length)。如果键不存在,返回 0。

    image-20261019203006492

    删除元素

    # 从左侧(头部)弹出
    # 用法:LPOP key
    LPOP messages           # 弹出并返回第一个元素
    
    # 从右侧(尾部)弹出
    # 用法:RPOP key
    RPOP messages           # 弹出并返回最后一个元素
    
    # 删除指定值的元素
    # 用法:LREM key count element
    LREM messages 1 "消息2"   # 从头开始删除1个"消息2"
    LREM messages -1 "消息2"  # 从尾开始删除1个"消息2"
    LREM messages 0 "消息2"   # 删除所有"消息2"
    
    # 保留指定范围的元素
    # 用法:LTRIM key start stop
    LTRIM messages 0 99     # 只保留前100个元素

    精准删除指定值(lrem 重点!)

    LREM` 是 List 中最灵活的删除命令,它的核心在于中间的 `count` 参数:
    **语法**:`LREM key count element
    • LREM messages 1 "消息2"
      • 解释:count 为正数 1。表示从左往右(从头开始)找,删除1个值为 "消息2" 的元素。
    • LREM messages -1 "消息2"
      • 解释:count 为负数 -1。表示从右往左(从尾开始)找,删除1个值为 "消息2" 的元素。
    • LREM messages 0 "消息2"
      • 解释:count 为 0。表示无视方向,删除列表中所有值为 "消息2" 的元素。
      • 💡 重点考点:LREM 的返回值是实际被删除的元素个数。
    # 1. 先创建一个包含多个 "A" 的列表
    RPUSH test "A" "B" "A" "C" "A"
    # 此时列表结构为: ["A", "B", "A", "C", "A"]
    
    # 2. 使用 count = 2 删除
    LREM test 2 "A"
    • 原理:Redis从左边开始找,遇到了第 1 个 “A”(删掉),继续找,遇到了第 2 个 “A”(删掉),此时已经达到了 count = 2 的限制,命令立刻停止。
    • LREM key -2 "元素" 的核心意思就是:从右边(尾部)开始往左边找,删掉最近遇到的 2 个指定元素。

    截断列表(LTRIM )

    这个命令不会按“值”删,而是按“范围”删,只保留指定区间内的元素,区间外的全删掉。

    • LTRIM messages 0 99
      • 解释:只保留索引 0 到 99 的元素(即前 100 个元素),第 101 个及以后的元素全部被丢弃。
      • 💡 场景极其常见:用于实现“最新 N 条记录”。比如你有一个保存用户操作日志的列表,为了防内存溢出,每次 LPUSH 插入新日志后,顺便执行一下 LTRIM key 0 99,这样就永远只保留最新的 100 条日志。

    image-20261019205228356

    修改元素

    # 设置指定索引的元素值
    # 用法:LSET key index element
    LSET messages 0 "新消息1"

    重新设置索引值:LSET

    • 语法:LSET key index element
    • 代码示例:LSET messages 0 "新消息1"
    • 详细解释:
      • 这个命令的作用是:将列表 messages 中,索引为 0(即第一个元素)的值,强制替换为 "新消息1"。
      • 它不会改变列表的长度,==只是把那个位置上的旧值给覆盖掉了==(类似于数组的赋值操作)。
      • 如果修改成功,Redis 会返回一个简单的字符串 OK。

    image-20261019205710586


    ==阻塞操作==

    阻塞操作是 Redis 列表的重要特性,常用于实现消息队列。

    # 阻塞式左侧弹出
    # 用法:BLPOP key [key ...] timeout
    BLPOP task_queue 30     # 等待30秒,如果有元素则弹出
    
    # 阻塞式右侧弹出
    # 用法:BRPOP key [key ...] timeout
    BRPOP task_queue 0      # 无限等待
    
    # 阻塞式右侧弹出并左侧推入
    # 用法:BRPOPLPUSH source destination timeout
    BRPOPLPUSH pending_tasks processing_tasks 60

    阻塞式左侧弹出:BLPOP

    • 语法:BLPOP key [key ...] timeout
    • 示例:BLPOP task_queue 30
    • 解释:
      • 客户端向 task_queue 列表请求数据。如果列表里有数据,立刻弹出一个(跟 LPOP 一样)。
      • 重点:如果列表是空的,客户端会原地阻塞等待(也就是卡在这里)。等待最长 30 秒。如果在 30 秒内,有另一个客户端往 task_queue 里 RPUSH 了一个新任务,这个阻塞的客户端就会瞬间被唤醒,拿到数据并返回。
      • 如果超过 30 秒依然没数据,返回 (nil)。

    阻塞式右侧弹出:BRPOP

    • 语法:BRPOP key [key ...] timeout
    • 示例:BRPOP task_queue 0
    • 解释:
      • 逻辑和 BLPOP 完全一样,只是从右侧(尾部)弹出。
      • 核心重点:timeout 的值如果是 0,代表无限等待(永不超时)。除非有数据进来,或者网络断开/客户端主动断开,否则它会一直死死卡在这里等。(这在实战中常用来做常驻后台的消费者进程)。

    高级进阶:==BRPOPLPUSH==(安全队列)

    • 语法:BRPOPLPUSH source destination timeout

    • 示例:BRPOPLPUSH pending_tasks processing_tasks 60

    • 解释:这个命令极其重要,它是“可靠消息队列”的基石。

      • 它的动作是:阻塞等待 pending_tasks(源列表),一旦有数据,把它从右边弹出,同时立刻从左边推入 processing_tasks(目标列表)。
      • 为什么牛? 解决“消息丢失”问题。假如你只用 BRPOP 拿到了任务,结果程序突然崩溃,这个任务就彻底丢了。用了 BRPOPLPUSH,任务会被备份到 processing_tasks 里。等你处理完了,再把它删掉。如果崩了,重启后还可以从备份列表里捞出来重试。
      • (注:Redis 6.2 之后,官方推出了更强大的 BLMOVE 命令取代了它,但底层逻辑一致)。

    示例图理解阻塞

    🎨 图 1:BLPOP(阻塞式左侧弹出)

    场景:后台消费者死死盯着左边,来一个拿一个。

    [ 初始状态 ] 列表为空,消费者开启阻塞监听:
    Queue: [ 空 ]
    客户端: BLPOP task_queue 30  ---> (💤 原地挂起,最多等30秒)
    
    [ 3秒后 ] 生产者突然塞入新任务(RPUSH):
    Queue: [ 任务A ]
                    ^
                    |
    客户端: (🔔 瞬间唤醒!) ---> 拿到 "任务A",命令结束返回。
    
    [ 最终 ] 任务被取走:
    Queue: [ 空 ]
    • 核心机制:如果左边有数据立刻拿;没数据就原地干等,直到超时或来数据。

    🎨 图 2:BRPOP(阻塞式右侧弹出 + timeout 0)

    场景:常驻消费者无限等待,典型的 FIFO(先进先出)队列模式。

    text

    [ 初始状态 ] 消费者开启无限等待(0 代表死等):
    Queue: [ 空 ]
    客户端: BRPOP task_queue 0  ---> (🔒 彻底挂起,永不超时)
    
    [ N小时后 ] 生产者用 LPUSH 从左边塞入:
    Queue: [ 任务1, 任务2, 任务3 ]
                                 ^
                                 |
    客户端: (🔔 从右侧弹出!) ---> 拿到 "任务3"。
    
    [ 最终 ] 按照先进先出的顺序,任务3最后进,最先被拿走。
    Queue: [ 任务1, 任务2 ]
    • 核心机制:timeout = 0 相当于把消费者进程“焊死”在这里,除非服务器宕机或网络断开,否则绝对不会返回 nil。

    ==🎨 图 3==:BRPOPLPUSH(安全转移:从右弹,推入左)

    场景:==可靠队列。取任务的同时,把它备份到另一个列表里,防丢==。

    text

    [ 初始状态 ] 
    待处理列表: pending_tasks   = [ 任务A, 任务B ]
    处理中列表: processing_tasks = [ 空 ]
    
    [ 执行命令 ]
    客户端: BRPOPLPUSH pending_tasks processing_tasks 60
    
    [ 一瞬间的原子操作 ] (任务从右出,立刻推入左边)
    1. 从 pending_tasks 右侧弹出 "任务B"。
    2. 立刻将 "任务B" 推入 processing_tasks 左侧。
    
    [ 执行后结果 ]
    待处理列表: pending_tasks   = [ 任务A ]       <-- 任务B没了
    处理中列表: processing_tasks = [ 任务B ]       <-- 任务B安全备份在这里!
    • 核心机制:如果客户端拿到“任务B”后,还没处理完就突然断电了,没关系!重启后,系统可以去 processing_tasks 里查,发现里面还有“任务B”,重新拿出来执行即可。这就是消息不丢失的基石。

    列表的应用场景

    消息队列实现

    # 生产者:添加任务到队列
    LPUSH task_queue "发送邮件:user@example.com"
    LPUSH task_queue "生成报表:monthly_report"
    LPUSH task_queue "数据备份:database_backup"
    
    # 消费者:从队列获取任务
    BRPOP task_queue 30
    # 返回:1) "task_queue" 2) "发送邮件:user@example.com"

    最新消息列表

    # 添加新消息(保持最新100条)
    LPUSH latest_news "新闻标题1"
    LTRIM latest_news 0 99
    
    # 获取最新10条消息
    LRANGE latest_news 0 9      #   数据太多了保留部分消息

    用户操作历史

    # 记录用户操作
    LPUSH user:1001:actions "登录:2024-01-15 10:30:00"
    LPUSH user:1001:actions "查看商品:product_123"
    LPUSH user:1001:actions "加入购物车:product_123"
    
    # 获取最近操作历史
    LRANGE user:1001:actions 0 9     

    image-20261019214616395

    image-20261019214825482


    集合 (Set)

    集合的特点和应用

    Redis 集合是字符串的无序集合,集合中的元素是唯一的。

    主要特点:

    • 唯一性:不允许重复元素
    • 无序性:元素没有固定顺序
    • 快速查找:O(1) 时间复杂度判断元素是否存在
    • 集合运算:支持交集、并集、差集运算

    常见应用场景:

    • 标签系统
    • 好友关系
    • 权限管理
    • 去重统计
    • 抽奖系统

    基本操作命令

    添加和删除元素

    # 添加元素
    # 用法:SADD key member [member ...]
    SADD tags "Redis" "数据库" "缓存" "NoSQL"
    SADD user:1001:skills "Java" "Python" "Redis"
    
    # 删除元素
    # 用法:SREM key member [member ...]
    SREM tags "缓存"
    SREM user:1001:skills "Java"
    
    # 随机删除并返回元素
    # 用法:SPOP key [count]
    SPOP tags           # 随机删除一个元素
    SPOP tags 2         # 随机删除两个元素

    添加元素:SADD

    • 语法:SADD key member [member ...]
    • 代码示例 1:SADD tags "Redis" "数据库" "缓存" "NoSQL"
      • 解释:向名为 tags 的集合中添加 4 个元素。如果其中某个元素(比如 “Redis”)已经存在,Redis 会自动忽略它,不会报错。
    • 代码示例 2:SADD user:1001:skills "Java" "Python" "Redis"
      • 解释:向名为 user:1001:skills 的集合中添加用户的技能标签。
      • 返回值:命令返回的是实际成功添加的元素个数(比如返回 4,说明 4 个都是新的;如果返回 2,说明有 2 个之前就已经在集合里了)。

    删除元素:SREM

    • 语法:SREM key member [member ...]
    • 代码示例:
      • SREM tags "缓存":从 tags 集合中精准删除 "缓存" 这个元素。
      • SREM user:1001:skills "Java":从技能集合中移除 "Java"。
      • 返回值:返回实际被成功删除的元素个数(如果删一个不存在的元素,返回 0)。

    随机删除并返回:SPOP

    这是 Set 最有趣、实战中最常用的命令之一,天然适合做“抽奖”或“随机分配”。

    • 语法:SPOP key [count]
    • 代码示例:
      • SPOP tags:从 tags 集合中随机挑 1 个元素,把它从集合里删掉,并把这个元素返回给你。
      • SPOP tags 2:从 tags 集合中随机挑 2 个元素,删掉并返回(返回的是一个列表)。
      • ⚠️ 避坑提示:如果你填的 count 大于集合里现有的元素总数(比如集合里只有 3 个,你填 SPOP tags 5),Redis 不会报错,它会把剩下的 3 个全弹出来给你,然后集合就空了。

    查询操作

    # 获取所有元素
    # 用法:SMEMBERS key
    SMEMBERS tags
    
    # 判断元素是否存在
    # 用法:SISMEMBER key member
    SISMEMBER tags "Redis"      # 返回 1(存在)
    SISMEMBER tags "MySQL"      # 返回 0(不存在)
    
    # 获取集合大小
    # 用法:SCARD key
    SCARD tags                  # 返回集合元素数量
    
    # 随机获取元素(不删除)
    # 用法:SRANDMEMBER key [count]
    SRANDMEMBER tags            # 随机返回一个元素
    SRANDMEMBER tags 3          # 随机返回三个元素

    获取所有元素:SMEMBERS

    • 语法:SMEMBERS key
    • 代码示例:SMEMBERS tags
    • 详细解释:
      • 返回集合 tags 中包含的所有元素。
      • ⚠️ 极客避坑:Set 是无序的,所以返回结果的顺序和你插入时的顺序完全不一样,这是正常现象。
      • ⚠️ 性能警告:如果这个集合里有几十万个元素,执行这个命令会直接把 Redis 阻塞。生产环境严禁对大集合使用 SMEMBERS,如果非要遍历,请改用 SSCAN 命令分批次查。

    判断元素是否存在:SISMEMBER

    • 语法:SISMEMBER key member
    • 代码示例:
      • SISMEMBER tags "Redis" -> 返回 1 (代表存在/True)
      • SISMEMBER tags "MySQL" -> 返回 0 (代表不存在/False)
    • 详细解释:
      • 判断某个特定的元素是否在集合中。
      • 核心考点:这个命令的时间复杂度是 O(1)。底层是哈希表,查得极快。应用场景:判断“该用户是否点过赞”、“该IP是否在黑名单里”。

    获取集合大小:SCARD

    • 语法:SCARD key
    • 代码示例:SCARD tags
    • 详细解释:
      • 返回集合中元素的个数(Cardinality 基数)。
      • 时间复杂度也是 O(1)(底层自带计数器),哪怕集合里有 100 万个元素,也是瞬间返回。
      • 应用场景:统计网站当天的独立访客数(UV)。

    随机获取元素(不删除):SRANDMEMBER

    • 语法:SRANDMEMBER key [count]
    • 代码示例:
      • SRANDMEMBER tags -> 随机返回 1 个元素。
      • SRANDMEMBER tags 3 -> 随机返回 3 个元素。
    • 详细解释:
      • 注意和上一张图里的 SPOP 区分:SRANDMEMBER 只是随机“看一眼”,元素依然留在集合里,不会被删除。
      • 进阶注意:如果你填的 count 是正数(如 3),它保证返回的 3 个元素绝对不重复;如果你填的是负数(如 -3),它返回的 3 个元素可能会有重复。

    移动元素

    # 将元素从一个集合移动到另一个集合
    # 用法:SMOVE source destination member
    SMOVE user:1001:skills user:1002:skills "Python"

    移动元素:SMOVE

    • 语法:SMOVE source destination member
    • 代码示例:SMOVE user:1001:skills user:1002:skills "Python"
    • 详细解释:
      • source(源集合):user:1001:skills
      • destination(目标集合):user:1002:skills
      • member(要移动的元素):"Python"
      • 执行动作:把 "Python" 这个元素从用户 1001 的技能集合中删掉,同时添加到用户 1002 的技能集合中。

    集合运算

    Redis支持集合间的数学运算,这是集合类型的强大特性。

    # 准备测试数据
    SADD set1 "a" "b" "c" "d"
    SADD set2 "c" "d" "e" "f"
    SADD set3 "d" "e" "f" "g"
    
    # 交集运算
    # 用法:SINTER key [key ...]
    SINTER set1 set2            # 返回 {"c", "d"}
    SINTER set1 set2 set3       # 返回 {"d"}
    
    # 并集运算
    # 用法:SUNION key [key ...]
    SUNION set1 set2            # 返回 {"a", "b", "c", "d", "e", "f"}
    
    # 差集运算
    # 用法:SDIFF key [key ...]
    SDIFF set1 set2             # 返回 {"a", "b"}
    SDIFF set2 set1             # 返回 {"e", "f"}
    
    # 将运算结果存储到新集合
    # 用法:[SINTERSTORE | SUNIONSTORE | SDIFFSTORE] destination key [key ...]
    
    SINTERSTORE result set1 set2
    SMEMBERS result             # 查看交集结果

    交集运算:SINTER(共同拥有的)

    • 数学概念:两边都有的元素。
    • 代码示例:
      • SINTER set1 set2 -> 返回 {"c", "d"} (set1 和 set2 共同有的)
      • SINTER set1 set2 set3 -> 返回 {"d"} (三个集合都有的)
    • 应用场景:共同好友、共同关注。比如 SINTER 用户A的关注 用户B的关注,瞬间找出他俩都关注了谁。

    并集运算:SUNION(全部加起来的)

    • 数学概念:把两边所有元素合并在一起(自动去重)。
    • 代码示例:SUNION set1 set2 -> 返回 {"a", "b", "c", "d", "e", "f"}
    • 应用场景:标签汇总、获取所有权限。比如把几个角色的权限集合合并,算出该用户的最终总权限。

    差集运算:SDIFF(我独有的)

    • 数学概念:在第一个集合里,但不包含在后面集合里的元素(有方向性)。
    • 代码示例:
      • SDIFF set1 set2 -> 返回 {"a", "b"} (set1 有,但 set2 没有的)
      • SDIFF set2 set1 -> 返回 {"e", "f"} (set2 有,但 set1 没有的)
    • 应用场景:可能认识的人(比如:我的好友 – 我的好友的好友)、找茬(新上架的商品 – 已经看过的商品)。

    存储结果:...STORE 系列(新集合存储)

    带 STORE 后缀的命令。前面的 SINTER 只是在屏幕上打印结果,而加了 STORE 会在Redis 中生成一个新的集合来保存结果。

    • 代码示例:SINTERSTORE result set1 set2
    • 详细解释:计算 set1 和 set2 的交集,并把结果(c 和 d)存入一个名为 result 的新集合中。然后你用 SMEMBERS result 就能看到这个新集合的内容。
    • (同样,SUNIONSTORE、SDIFFSTORE 也是这个逻辑)。
    • ⚠️ 覆盖警告:如果 result 这个键之前已经存在,它原有的数据会被直接清空覆盖。

    有序集合 (zset)

    有序集合的特点和应用

    有序集合是 Redis 中最复杂的数据类型,它结合了集合和列表的特点。

    主要特点:

    • 唯一性:成员唯一,不允许重复
    • 有序性:按分数(score)排序
    • 双重索引:可以按成员或分数查找
    • 范围查询:支持按分数或排名范围查询

    常见应用场景:

    • 排行榜系统
    • 优先级队列
    • 时间线排序
    • 范围查询
    • 权重计算

    基本操作命令

    添加和删除元素

    # 添加元素(分数 成员)
    # 用法:ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
    ZADD leaderboard 1500 "玩家A"
    ZADD leaderboard 2300 "玩家B" 1800 "玩家C" 2100 "玩家D"
    # 批量添加
    ZADD leaderboard 1200 "玩家E" 1900 "玩家F" 2500 "玩家G"
    
    # 删除元素
    # 用法:ZREM key member [member ...]
    ZREM leaderboard "玩家E"
    
    # 按分数范围删除
    # 用法:ZREMRANGEBYSCORE key min max
    ZREMRANGEBYSCORE leaderboard 0 1000
    
    # 按排名范围删除
    # 用法:ZREMRANGEBYRANK key start stop
    ZREMRANGEBYRANK leaderboard 0 2    # 删除前3名

    添加元素:ZADD

    • 语法:ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
    • 代码示例:
      • ZADD leaderboard 1500 "玩家A"
        • 解释:往 leaderboard 里添加“玩家A”,分数设为 1500。
      • ZADD leaderboard 2300 "玩家B" 1800 "玩家C" 2100 "玩家D"
        • 解释:支持一次添加多个,格式为 分数1 成员1 分数2 成员2。
      • ZADD leaderboard 1200 "玩家E" 1900 "玩家F" 2500 "玩家G"
        • 解释:批量添加。如果成员已存在,ZADD 会更新该成员的分数。
    • 💡 高级可选参数(括号里的):
      • NX:只添加不存在的成员(不更新已有分数)。
      • XX:只更新已存在的成员(不添加新成员)。
      • CH:返回值变为“发生变化的成员总数”(默认只返回新增的个数)。
      • INCR:让分数像 INCR 命令一样递增(比如 ZADD leaderboard INCR 100 "玩家A")。

    删除元素:ZREM

    • 语法:ZREM key member [member ...]
    • 代码示例:ZREM leaderboard "玩家E"
    • 解释:精准移除指定的成员(不管它的分数是多少)。返回实际删除的成员个数。

    按分数范围删除:ZREMRANGEBYSCORE

    • 语法:ZREMRANGEBYSCORE key min max
    • 代码示例:ZREMRANGEBYSCORE leaderboard 0 1000
    • 解释:删除分数在 0 到 1000 之间(包含边界)的所有成员。
    • 应用场景:清理积分过低的“僵尸用户”。

    按排名范围删除:ZREMRANGEBYRANK

    • 语法:ZREMRANGEBYRANK key start stop
    • 代码示例:ZREMRANGEBYRANK leaderboard 0 2
    • 解释:按名次删除。ZSet 的排名从 0 开始,0 2 代表删除排名第 1 到第 3 名的成员(前 3 名)。
    • 应用场景:末位淘汰(删除排名最后 10 名可用 ZREMRANGEBYRANK key -10 -1)。

    查询操作

    # 获取元素数量
    # 用法:ZCARD key
    ZCARD leaderboard
    
    # 获取元素分数
    # 用法:ZSCORE key member
    ZSCORE leaderboard "玩家B"
    
    # 获取元素排名
    # 用法:ZRANK key member        # 正序排名(从0开始)
    # 用法:ZREVRANK key member     # 逆序排名(从0开始)
    ZRANK leaderboard "玩家B"
    ZREVRANK leaderboard "玩家B"
    
    # 统计分数范围内的元素数量
    # 用法:ZCOUNT key min max  
    ZCOUNT leaderboard 1500 2000
    
    # 获取分数范围内的元素数量(字典序)
    # 用法:ZLEXCOUNT key min max
    ZLEXCOUNT leaderboard a c

    获取元素数量:ZCARD

    • 语法:ZCARD key
    • 代码示例:ZCARD leaderboard
    • 解释:返回集合中成员的总个数(Cardinality)。

    获取元素分数:ZSCORE

    • 语法:ZSCORE key member
    • 代码示例:ZSCORE leaderboard "玩家B"
    • 解释:查询指定成员的分数(score)。如果成员不存在,返回 (nil)。

    获取元素排名:ZRANK / ZREVRANK(面试重点)

    • 语法:ZRANK key member / ZREVRANK key member
    • 代码示例:
      • ZRANK leaderboard "玩家B" -> 正序排名(分数从小到大,从 0 开始算)。
      • ZREVRANK leaderboard "玩家B" -> 逆序排名(分数从大到小,从 0 开始算)。
    • 解释:查询某个成员在当前集合中的名次。
    • ⚠️ 避坑:返回值是从 0 开始的。如果返回 0,代表是第 1 名;如果返回 (nil),说明该成员不在集合中。

    统计分数范围内的元素数量:ZCOUNT

    • 语法:ZCOUNT key min max
    • 代码示例:ZCOUNT leaderboard 1500 2000
    • 解释:统计分数在 1500 到 2000(包含边界)之间的成员个数。
    • 应用场景:例如统计“考试成绩在 80-100 分之间的学生人数”、“VIP 积分在 1000 分以上的用户数”。

    范围操作

    有序集合的范围操作是其最强大的功能之一。

    按排名范围查询

    # 按排名获取元素(正序)
    # 用法:ZRANGE key start stop [WITHSCORES]
    ZRANGE leaderboard 0 2              # 获取前3名
    ZRANGE leaderboard 0 2 WITHSCORES   # 获取前3名及分数
    ZRANGE leaderboard 0 -1             # 获取所有元素
    
    # 按排名获取元素(逆序)
    # 用法:ZREVRANGE key start stop [WITHSCORES]
    ZREVRANGE leaderboard 0 2 WITHSCORES # 获取前3名(高分到低分)

    1. 按“排名”范围查询(看名次)

    正序查:ZRANGE(分数从低到高)

    • 语法:ZRANGE key start stop [WITHSCORES]
    • 代码示例:
      • ZRANGE leaderboard 0 2:取排名第 1 到第 3 的成员(索引 0, 1, 2)。
      • ZRANGE leaderboard 0 2 WITHSCORES:取前 3 名,并且带出他们的分数(加上 WITHSCORES 返回值会成对出现:成员1, 分数1, 成员2, 分数2…)。
      • ZRANGE leaderboard 0 -1:取所有成员(-1 代表最后一个)。

    逆序查:ZREVRANGE(分数从高到低)

    • 语法:ZREVRANGE key start stop [WITHSCORES]
    • 代码示例:ZREVRANGE leaderboard 0 2 WITHSCORES
    • 解释:取分数最高的前 3 名及其分数。做排行榜(如游戏榜单、热搜榜)通常用这个。

    按分数范围查询

    # 按分数范围获取元素(正序)
    # 用法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
    ZRANGEBYSCORE leaderboard 1500 2000
    ZRANGEBYSCORE leaderboard 1500 2000 WITHSCORES
    ZRANGEBYSCORE leaderboard 1500 2000 LIMIT 0 5
    
    # 按分数范围获取元素(逆序)
    # 用法:ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
    ZREVRANGEBYSCORE leaderboard 2000 1500 WITHSCORES
    
    # 使用开区间
    ZRANGEBYSCORE leaderboard 1500 2000    # 不包含1500和2000
    ZRANGEBYSCORE leaderboard -inf +inf     # 所有元素

    正序查询:ZRANGEBYSCORE

    • 语法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
    • 核心逻辑:查找分数在 min 到 max 之间的成员,按分数从低到高(正序)排列。
    • 代码拆解:
      1. ZRANGEBYSCORE leaderboard 1500 2000
        • 解释:获取分数在 1500 到 2000(包含边界)之间的所有成员。
      2. ZRANGEBYSCORE leaderboard 1500 2000 WITHSCORES
        • 解释:同上,但在返回成员名字的同时,附带返回他们的分数。
      3. ZRANGEBYSCORE leaderboard 1500 2000 LIMIT 0 5
        • 解释:分页查询。在 1500 到 2000 分之间,跳过 offset 0 个,取出 count 5 个(即取前 5 名)。

    逆序查询:ZREVRANGEBYSCORE

    • 语法:ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
    • 核心逻辑:按分数从高到低(逆序)排列。
    • ⚠️ 极其重要的避坑点:因为是从高到低排,所以参数顺序变了!max 必须写在前面,min 写在后面。
    • 代码拆解:
      • ZREVRANGEBYSCORE leaderboard 2000 1500 WITHSCORES
      • 解释:获取分数在 2000 到 1500 之间的成员,按分数从高到低返回。如果你写成 1500 2000,将返回空结果。

    按字典序范围查询

    # 按字典序范围获取元素
    # 用法:ZRANGEBYLEX key min max [LIMIT offset count]
    ZRANGEBYLEX leaderboard a c
    # 用法:ZREVRANGEBYLEX key max min [LIMIT offset count]
    ZREVRANGEBYLEX leaderboard c a
    
    # 示例:相同分数的元素按字典序排列
    ZADD words 0 "apple" 0 "banana" 0 "cherry" 0 "date"
    ZRANGEBYLEX words a c              # 获取a到c之间的单词
    ZRANGEBYLEX words banana date      # 获取banana到date之间的单词

    正序查询:ZRANGEBYLEX

    • 语法:ZRANGEBYLEX key min max [LIMIT offset count]
    • 代码示例:ZRANGEBYLEX leaderboard a c
    • 解释:在 leaderboard 集合中,找出成员名字在字母 a 到 c 之间(包含 a 和 c)的所有元素,按从 A 到 Z 的正序排列。

    逆序查询:ZREVRANGEBYLEX

    • 语法:ZREVRANGEBYLEX key max min [LIMIT offset count]
    • 代码示例:ZREVRANGEBYLEX leaderboard c a
    • ⚠️ 避坑警告:和按分数逆序查一样,参数顺序必须反过来! 最大值 c 写在前面,最小值 a 写在后面,代表从 Z 到 A 的逆序排列。

    • 解释:你向 words 集合中添加了 4 个单词,注意看,它们的分数全部是 0。在 Redis 底层,它们会自动按字母顺序排好:apple -> banana -> cherry -> date。

    接着执行:

    • ZRANGEBYLEX words a c -> 返回 apple(以a开头)和 banana(以b开头)。因为 cherry 是以 c 开头的,其实也包含在内。
    • ZRANGEBYLEX words banana date -> 返回从 banana 到 date 之间的单词(包含 banana 和 date)。
  • Redis数据类型之哈希 (Hash)

    哈希的特点和应用

    ==Redis哈希是一个键值对集合,特别适合存储对象==

    Redis 的 Hash 就是一个大字典(大 Key),字典里面装着小字典(若干个 field-value 键值对)。

    主要特点:

    • 结构化存储:键值对形式存储
    • 内存优化:小哈希使用压缩编码
    • 部分更新:可以只更新某个字段
    • 原子操作:字段级别的原子操作

    常见应用场景:

    • 用户信息存储
    • 配置信息管理
    • 购物车实现
    • 对象缓存
    • 计数器组合

    基本操作命令

    设置和获取字段

    # 用法:HSET key field value
    HSET user:1001 name "张三"
    HSET user:1001 age 28
    HSET user:1001 city "北京"
    HSET user:1001 email "zhangsan@example.com"
    
    # 用法:HMSET key field value [field value ...]
    HMSET user:1002 name "李四" age 25 city "上海" email "lisi@example.com"
    
    # 仅当字段不存在时设置
    # 用法:HSETNX key field value
    HSETNX user:1001 phone "13800138000"
    
    # 获取单个字段
    # 用法:HGET key field
    HGET user:1001 name
    HGET user:1001 age
    
    # 获取多个字段
    # 用法:HMGET key field [field ...]
    HMGET user:1001 name age city
    
    # 获取所有字段和值
    # 用法:HGETALL key
    HGETALL user:1001
    
    # 获取所有字段名
    # 用法:HKEYS key
    HKEYS user:1001
    
    # 获取所有值
    # 用法:HVALS key
    HVALS user:1001

    设置字段(存数据)

    • HSET key field value(单条设置)
      • 解释:向名为 user:1001 的 Hash 中,设置一个字段(field)和对应的值。相当于给对象赋值。
      • 例如:HSET user:1001 name "张三" 就是把 1001 用户的姓名设为张三。
    • HMSET key field value [field value ...](批量设置)
      • 解释:一次性设置多个字段和值。
    • HSETNX key field value(仅当字段不存在时设置)
      • 解释:NX 还是那个熟悉的意思(Not eXists)。如果 user:1001 里面已经有 phone 这个字段,命令直接返回 0 失败,不会覆盖旧值。常用于防重复写入。

    获取字段(读数据)

    • HGET key field(获取单条)
      • 解释:获取指定字段的值。比如 HGET user:1001 name 会返回 "张三"。
    • HMGET key field [field ...](批量获取)
      • 解释:一次性获取多个指定字段的值(返回一个列表)。适合渲染页面时,一次性把需要的字段全捞出来,减少网络请求。
    • HGETALL key(获取所有)
      • 解释:获取该 Hash 下的所有字段和值(返回值是一个键值对列表)。
      • ⚠️ 性能避坑:如果这个 Hash 里有几万个字段,用 HGETALL 会瞬间阻塞 Redis。生产环境大 Hash 严禁全量获取。

    获取字段名或值(只看一半)

    • HKEYS key
      • 解释:只获取所有的字段名(如 name, age, city),不获取具体的值。
    • HVALS key
      • 解释:只获取所有的字段值(如 张三, 28, 北京),不获取字段名。

    删除和检查字段

    # 删除字段
    # 用法:HDEL key field [field ...]
    HDEL user:1001 email
    HDEL user:1001 phone city
    
    # 检查字段是否存在
    # 用法:HEXISTS key field
    HEXISTS user:1001 name      # 返回 1(存在)
    HEXISTS user:1001 phone     # 返回 0(不存在)
    
    # 获取字段数量
    # 用法:HLEN key
    HLEN user:1001

    删除字典里的字段:HDEL

    • 语法:HDEL key field [field ...]
    • 代码示例:
      • HDEL user:1001 email -> 把 user:1001 这个大字典里的 email 字段删掉。
      • HDEL user:1001 phone city -> 一次删掉 phone 和 city 两个字段。
    • 详细解释:它只删除字典里的指定字段,整个 Key(字典)依然存在。

    检查字段是否存在:HEXISTS

    • 语法:HEXISTS key field
    • 代码示例:
      • HEXISTS user:1001 name -> 返回 1(真/存在)。
      • HEXISTS user:1001 phone -> 返回 0(假/不存在)。
    • 详细解释:检查大字典里有没有某个特定的字段。时间复杂度是 O(1),查得极快。

    查看字典有多少字段:HLEN

    • 语法:HLEN key
    • 代码示例:HLEN user:1001
    • 详细解释:返回这个 Hash 里字段(field)的总个数。

    批量操作

    哈希支持高效的批量操作,减少网络往返次数。

    # 批量设置用户信息
    HMSET user:1003 
      name "王五" 
      age 30 
      city "广州" 
      email "wangwu@example.com" 
      phone "13900139000" 
      department "技术部" 
      position "高级工程师"
    
    # 批量获取用户信息
    HMGET user:1003 name age city department position
    
    # 获取完整用户信息
    HGETALL user:1003

    批量设置:HMSET(图中的换行符 )

    • 图中代码:

      HMSET user:1003 
        name "王五" 
        age 30 
        city "广州" 
        ...

      详细解释:

      • 这里使用了 Linux 终端里的反斜杠 ,它的作用是“命令换行”。这其实是一整行命令,只是为了在终端里方便人类阅读,才折行写的。
      • 如果不用 ,你必须把所有字段写在同一行:HMSET user:1003 name "王五" age 30...
      • 运维意义:如果用单条的 HSET 设置 7 个字段,客户端和 Redis服务器要来回通信 7 次。用 HMSET 一次性打包发送,只通信 1 次。在高并发场景下,极大地节约了网络带宽,降低了延迟。

    批量获取:HMGET

    • 图中代码:HMGET user:1003 name age city department position
    • 详细解释:一次性把这 5 个指定字段的值全取出来,返回一个列表。
    • 运维意义:同样是减少网络 IO。比如研发需要展示用户卡片(只要名字、年龄、城市),不要让他们在代码里循环调 5 次 HGET,而是让他们直接发一条 HMGET 指令。

    全量获取:HGETALL

    • 图中代码:HGETALL user:1003
    • 详细解释:把这个字典里所有的字段和值全部取出来。

    数值操作

    哈希字段支持数值操作,常用于计数器场景。

    # 增加字段的数值
    # 用法:HINCRBY key field increment
    HSET user:1001 login_count 0
    HINCRBY user:1001 login_count 1     # login_count = 1
    HINCRBY user:1001 login_count 5     # login_count = 6
    
    # 增加字段的浮点数值
    HINCRBYFLOAT key field increment
    HSET user:1001 balance 100.50
    HINCRBYFLOAT user:1001 balance 25.30   # balance = 125.80
    HINCRBYFLOAT user:1001 balance -10.00  # balance = 115.80

    整数增减:HINCRBY

    • 语法:HINCRBY key field increment
    • 代码示例:
      • HSET user:1001 login_count 0 (初始化:用户 1001 的登录次数为 0)
      • HINCRBY user:1001 login_count 1 (加 1,结果返回 1)
      • HINCRBY user:1001 login_count 5 (加 5,结果返回 6)
    • 详细解释:将 user:1001 字典中的 login_count 字段的值加上指定的整数 increment。
    • ⚠️ 极客提示:如果 increment 填负数(如 -1),就变成了减法。

    浮点数增减:HINCRBYFLOAT

    • 语法:HINCRBYFLOAT key field increment
    • 代码示例:
      • HSET user:1001 balance 100.50 (初始化:余额 100.50)
      • HINCRBYFLOAT user:1001 balance 25.30 (加 25.30,返回 125.80)
      • HINCRBYFLOAT user:1001 balance -10.00 (减 10.00,返回 115.80)
    • 详细解释:支持对浮点数(小数)进行加减。完美避开由于精度问题导致的金额计算错误。

    对象存储应用

    哈希非常适合存储对象数据,提供了比字符串更好的结构化存储。

    用户信息管理

    # 创建用户信息
    HMSET user:1001 
      name "张三" 
      age 28 
      email "zhangsan@example.com" 
      city "北京" 
      department "研发部" 
      position "软件工程师" 
      salary 15000 
      join_date "2023-01-15" 
      status "active"
    
    # 更新用户信息
    HSET user:1001 age 29
    HSET user:1001 salary 16000
    HSET user:1001 position "高级软件工程师"
    
    # 获取用户基本信息
    HMGET user:1001 name age email city
    
    # 获取用户工作信息
    HMGET user:1001 department position salary
    
    # 检查用户状态
    HGET user:1001 status

    创建对象(全量写入):HMSET

    • 代码:使用 HMSET user:1001 ... 建立了一个包含姓名、年龄、邮箱、部门、薪水等 9 个属性的用户字典。
    • 业务优势:对比把整个用户数据转成 JSON 存在 String 里,Hash 结构在 Redis底层非常节省内存(Key 少,元数据开销小)。

    更新对象(局部更新):HSET(核心亮点)

    • 代码:
      • HSET user:1001 age 29 (改年龄)
      • HSET user:1001 salary 16000 (改薪水)
      • HSET user:1001 position "高级软件工程师" (升职了)
    • 为什么这是核心亮点?
      • 如果这 9 个字段是存成一个大 JSON 字符串,你要改薪水,必须:把整个 JSON 查出来 -> 反序列化成对象 -> 改薪水 -> 序列化回 JSON -> 覆盖写回 Redis。极度浪费 CPU 和网络带宽。
      • 用 Hash,你只需要 HSET 修改那一个字段,指哪打哪,极其高效,且天然防并发覆盖。

    ==Redis key 和二维表关系==

       [ Redis 客户端发送指令 ]
                  |
                  v
    +-------------------------------------------------------+
    |  Key (大字典名 / 表名) : user:1001                    |
    +-------------------------------------------------------+
    |  Field (字段/行)       |  Value (值/列)                |
    +------------------------+------------------------------+
    |  name                  |  "张三"                      |
    |  age                   |  29  (被HSET更新过)          |
    |  email                 |  "zhangsan@example.com"      |
    |  city                  |  "北京"                      |
    |  department            |  "研发部"                    |
    |  position              |  "高级软件工程师"(被更新过)  |
    |  salary                |  16000 (被更新过)            |
    |  join_date             |  "2023-01-15"                |
    |  status                |  "active"                    |
    +-------------------------------------------------------+

    购物车实现

    # 用户购物车:cart:user_id,字段为商品ID,值为数量
    HSET cart:1001 product:123 2
    HSET cart:1001 product:456 1
    HSET cart:1001 product:789 3
    
    # 增加商品数量
    HINCRBY cart:1001 product:123 1     # 数量变为3
    
    # 减少商品数量
    HINCRBY cart:1001 product:456 -1    # 数量变为0
    
    # 删除商品(数量为0时)
    HDEL cart:1001 product:456
    
    # 获取购物车所有商品
    HGETALL cart:1001
    
    # 获取购物车商品数量
    HLEN cart:1001
    
    # 清空购物车
    DEL cart:1001

    ==配置信息管理==

    # 应用配置
    HMSET app:config 
      database_host "localhost" 
      database_port 3306 
      database_name "myapp" 
      redis_host "localhost" 
      redis_port 6379 
      cache_ttl 3600 
      max_connections 100 
      debug_mode "false"
    
    # 获取数据库配置
    HMGET app:config database_host database_port database_name
    
    # 获取缓存配置
    HMGET app:config redis_host redis_port cache_ttl
    
    # 更新配置
    HSET app:config debug_mode "true"
    HSET app:config max_connections 200
    
    # 获取所有配置
    HGETALL app:config

    初始化应用配置:HMSET(全量写入)

    • 代码:使用 HMSET app:config ... 创建了一个名为 app:config 的大字典。
    • 解释:把数据库的 host、port、name,Redis 的 host、port,以及缓存时间、最大连接数等 8 个配置项一次性写入 Redis。

    按模块获取配置:HMGET(按需拉取)

    • 代码 1:HMGET app:config database_host database_port database_name
      • 解释:应用启动时,只需要连接数据库,所以精准拉取这 3 个配置项,不拉取无关的 Redis 配置。
    • 代码 2:HMGET app:config redis_host redis_port cache_ttl
      • 解释:初始化缓存模块时,精准拉取 Redis 相关的 3 个配置项。

    动态更新配置:HSET(局部热更新)

    • 代码:
      • HSET app:config debug_mode "true" (开启调试模式)
      • HSET app:config max_connections 200 (调大最大连接数)
    • 解释:在系统运行过程中,运维或研发只需要修改特定的字段。不需要重写整个配置字典。
    • 优势:应用只要监听这个 Key 的变化(或者定时去 HGET),就能实现配置热加载,不用重启服务。

    获取全量配置:HGETALL(全量拉取)

    • 代码:HGETALL app:config
    • 解释:一次性获取所有的配置项。通常用于后台管理系统的“配置详情”页面展示,或者用于配置的备份与审计。

    📚 ==Redis Hash 核心应用:配置信息管理(轻量级配置中心)==

    1. 核心目的

    将应用配置(如数据库地址、缓存策略、开关参数)从代码/本地文件中剥离,集中存储在 Redis Hash 中,实现集中管理、按需获取与动态热更新。

    2. 核心业务闭环

    操作阶段 使用命令 业务作用 架构优势
    1. 初始化配置 HMSET 一次性写入所有配置项(DB、Redis、连接数等)。 减少网络 RTT,原子性写入。
    2. 按需读取 HMGET 各模块启动时,仅拉取自己需要的字段(如 DB 模块只拉取 database_*)。 精准获取,避免拉取全量数据造成带宽浪费。
    3. 动态更新 HSET 运行期间修改单个配置项(如将 debug_mode 改为 true)。 局部覆盖,实现配置热更新,无需重启服务。
    4. 全量查看 HGETALL 后台管理页面展示或配置审计。 方便可视化面板展示所有配置项。

    3. 架构设计优势(为什么用 Hash 而不用 String?)

    1. 内存开销极低:如果使用 String 存配置,每个配置项都会产生独立的 Key(如 config:db_host、config:db_port),产生大量的元数据开销。Hash 将所有配置聚合成一个 Key(app:config),极大节省内存。
    2. 操作原子性:通过 HSET 更新单个字段,天然防止并发覆盖整个配置文件(对比 JSON 字符串的“读-改-写”流程更安全)。
    3. 结构高度契合:Hash 的 Field-Value 结构完美对应配置文件的 Key-Value 键值对。

    4. ⚠️ 生产环境避坑与运维监控指南

    作为 SRE/运维,在落地此方案时必须关注以下红线:

    1. 高可用与持久化要求极高:配置中心是核心命脉。一旦Redis 宕机或数据丢失,可能导致所有微服务无法启动。必须配置 AOF 持久化 + 主从高可用。
    2. 严禁高频全量拉取:坚决禁止在业务主流程(如每次 HTTP 请求)中执行 HGETALL,否则极易打满Redis的 QPS 与网络带宽。正确姿势:应用启动时拉取一次放入本地内存,后续通过 Pub/Sub 或长轮询监听变更。
    3. ==权限与误删风险==:必须对 app:config 这种核心 Key 做严格的 ACL权限控制,严禁研发误操作执行 DEL app:config(如图中清空购物车的命令,在这里执行将是毁灭性的灾难)。
    4. 热更新监控:运维需监控配置的变更频率。如果某个配置项被频繁改动(如日志级别反复横跳),可能会引发频繁的 GC或 IO 抖动。

    5. 总结金句(面试)

    “Redis Hash 是构建轻量级配置中心的绝佳方案。它利用聚合存储省内存、局部更新防并发的特性,实现了配置的动态热更新。但在架构层面,必须配合持久化保证与严格的ACL 权限,并杜绝高频的 HGETALL 全量拉取,才能保障系统的稳定。”

    [ 👨‍💻 后台管理系统 / 运维 ] 
          │
          ├─(1. 初始化 HMSET)───────────────┐
          ├─(3. 动态更新 HSET)──────────────┤ 
          ├─(4. 全量审计 HGETALL)───────────┤
          │                                 v
          │                     +-----------------------+
          │                     |   Redis Hash (核心)   |
          │                     |  Key: app:config      |
          │                     |-----------------------|
          │                     | db_host: 127.0.0.1    |
          │                     | db_port: 3306         |
          │                     | debug_mode: false     |
          │                     +-----------------------+
          │                                 │
          │                                 ├─(2. 按需精准拉取 HMGET)──┐
          │                                 │                          v
          │                                 │                 [ 🖥️ 业务服务 A (DB模块) ]
          │                                 │                 [ 🖥️ 业务服务 B (缓存模块) ]
          │                                 │                          │
          └─(5. Pub/Sub 广播配置变更)───────┴──────────────────────────┘
                                                                       │
                                                                       v
                                                            (服务本地内存更新,无需重启)
    
    ========================================================================================
    🚨 [ SRE 运维红线 / 监控告警 ]
    1. 坚决禁止业务高频调 HGETALL (防止打满网络带宽和 Redis QPS)
    2. 强制开启 AOF 持久化 + 主从高可用 (配置丢失 = 全体微服务瘫痪)
    3. 严格 ACL 权限控制,严禁研发执行 DEL app:config (防毁灭性误删)

    image-20261020010309795

  • Mysql的SQL语句

    • SQL是结构化的查询语句
    • SQL的种类
      • DDL:数据定义语句
      • DCL:数据控制语言
      • DML:数据操作语言
      • DQL:数据查询语言

    SQL 语句概览

    这里介绍了 SQL 的概念和四大分类。这里理清一下:

    • SQL (Structured Query Language):结构化查询语言,是我们和关系型数据库(如 MySQL)沟通的标准语言。

    • SQL 的四大种类(面试常考):

      • DDL (Data Definition Language) 数据定义语言:用来定义和管理数据库对象的(如库、表、列)。核心指令:CREATE(建)、DROP(删)、ALTER(改)。(主要讲的就是 DDL 对库的操作)。
      • DCL (Data Control Language) 数据控制语言:用来管理权限的。核心指令:GRANT(授权)、REVOKE(收回权限)。
      • DML (Data Manipulation Language) 数据操作语言:用来操作表中的数据的。核心指令:INSERT(增)、DELETE(删)、UPDATE(改)。
      • DQL (Data Query Language) 数据查询语言:专门用来查询数据的。核心指令:SELECT。(虽然很多地方把它归为 DML,但单独拎出来更严谨)。

    DDL数据定义语句

    • 对库或者表进行操作的语句
    • 创建数据库
    create database db01;
    # 创建数据库
    create database DB01;
    # 数据库名区分大小写(注意windows里面不区分)
    # show variables like 'lower%'; 可以看到lower_case_table_names默认0是区分大小写通过/etc/my.cnf [mysqld]下可以永久修改
    show databases;
    # 查看数据库(DQL)
    show create database db01;
    # 查看创建数据库语句
    help create database;
    # 查看创建数据库语句帮助
    create database db02 charset utf8;
    # 创建数据库的时候添加属性
    • 删除数据库
    drop database db02;
    # 删除数据库db02
    • 修改定义库
    alter database db01 charset utf8;
    show create database db01;

    DDL 数据定义语句 —— 针对“数据库”的操作

    教你怎么对“库”进行增删改查。

    1. 创建数据库 (CREATE)

    #  创建数据库
    create database db01;
    • 最基础的建库语法,创建一个名为 db01 的数据库。
    create  database  DB01;
    
    # 数据库名区分大小写(注意windows里面不区分)
    # show variables like 'lower%'; 可以看到lower_case_table_names默认0是区分大小写通过/etc/my.cnf [mysqld]
    • 重点原理(避坑):
      • 在 Linux 系统下,MySQL 默认是区分大小写的。db01 和 DB01 是两个不同的数据库。这由系统变量 lower_case_table_names 控制,默认值为 0(0代表区分大小写,1代表不区分,全变小写)。
      • 在 Windows 系统下,由于文件系统(NTFS)本身不区分大小写,MySQL 默认 lower_case_table_names=1,所以不区分。
      • 这就解释了为什么很多人在 Windows 上写好的代码,放到 Linux 服务器上运行会报错“表不存在”——因为大小写不一致。
    create database db02 charset utf8;
    # 创建数据库的时候添加属性,添加一个utf-8编码,
    • 建库时指定字符集。非常实用!如果不指定,==MySQL 默认可能是 latin1(不支持中文)。指定为 utf8 可以防止以后存入中文时出现乱码==。

    2. 查看数据库 (SHOW)

    show databases;
    # 查看数据库(DQL)
    • 列出当前 MySQL 服务器中所有的数据库名称。
    show create database db01;
    # 查看创建数据库语句,显示当初用什么字符集,什么排序规则创建
    • 极其常用! 当你接手一个别人的数据库,不知道它当初是用什么字符集、什么排序规则创建的时候,用这个命令,MySQL 会把当初创建它的 CREATE 语句原原本本地打印出来。
    help create database;
    # 查看创建数据库语句帮助
    • 查官方文档。比如你想知道建库时还能加什么参数(如 COLLATE 排序规则),直接 help 一下非常方便。

    3. 删除数据库 (DROP)

    drop database db02;
    # 删除数据库db02
    • 极度危险命令! 它会瞬间且不可逆地删除数据库里所有的表、数据、权限设置。在==生产环境中执行此命令前,务必确认已做备份==。

    4. 修改数据库定义 (ALTER)

    alter database db01 charset utf8;
    show create database db01;
    • 修改数据库属性。比如刚才建的 db01 字符集不对,可以用 ALTER 命令修改它的默认字符集为 utf8。
    • 修改完后再用 show create database db01; 验证一下,确保修改成功。

    5、 创建表 (CREATE TABLE)

    建表是 DDL 的基础,==核心在于定义列名、数据类型以及数据属性==。

    1. 基础语法:

    create table 表名(
        列名1 数据类型,
        列名2 数据类型,
        ...
    );
    • 笔记中示例定义了:int(整数)、varchar(可变长字符)、tinyint(微小整数)、enum(枚举)、datetime(时间)。

    6 进阶语法(带属性与引擎设置):

    create table student(
        sid int not null primary key auto_increment comment '学号',
        sname varchar(20) not null comment '学生姓名',
        ...
    ) charset utf8 engine innodb;
    • 数据属性(写在数据类型后面):
      • not null:非空约束,该列必须有值。
      • primary key:主键约束(唯一且非空)。
      • auto_increment:自增属性,通常配合主键使用(必须是整数类型)。
      • default:设置默认值(如 default 'm')。
      • comment:添加列注释,方便后期维护。
    • 表级属性(写在括号外面):
      • charset utf8:指定表的字符集(防止中文乱码)。
      • engine innodb:指定存储引擎(InnoDB 支持事务,是目前的默认推荐引擎)。

    7、 查看表信息 (SHOW / DESC)

    • show tables; :查看当前数据库下所有表的名称。
    • show create table 表名;:查看该表当初被创建时的完整 SQL 语句(包含字符集、引擎、注释等,非常实用)。
    • desc 表名;:查看==表的结构定义==(列出所有列名、数据类型、是否为空、主键、默认值等详细信息)。

    8、 删除表 (DROP)

    • drop table 表名;:极度危险命令。删除表及其所有数据,还原需要事先备份。

    9、 修改表结构 (ALTER TABLE)

    ALTER TABLE 是后期维护表结构最常用的命令,用于在不删除表的情况下修改其定义。

    1. 修改表名:

    • alter table 旧表名 rename 新表名;

    2. 添加列 (ADD):

    • alter table 表名 add 列名 数据类型; (默认添加到末尾)
    • alter table 表名 add 列名 数据类型 first; (添加到表的第一列)
    • alter table 表名 add 列名 数据类型 after 指定列名; (添加到指定列的后面)
    • alter table 表名 add 列1 类型, add 列2 类型; (一次添加多列)

    3. 修改列 (MODIFY / CHANGE):

    • alter table 表名 modify 列名 新数据类型;:只能修改列的数据类型和属性,不能修改列名。
    • alter table 表名 change 旧列名 新列名 新数据类型;:既能修改列名,也能修改数据类型(功能更强大,通常使用这个来改列名)。

    4. 删除列 (DROP):

    • alter table 表名 drop 列名;

    学习与实操建议

    1. 命名规范与语法避坑

    • 统一命名格式:在 Linux 环境下,数据库名和表名默认严格区分大小写(由系统变量 lower_case_table_names 控制,默认为 0)。为了避免跨平台(Windows 迁移至 Linux)时出现“表不存在”的错误,建议统一采用小写字母加下划线的命名规范(如 wordpress_db)。
    • 避开保留字:==严禁使用 MySQL 的保留字(如 order、user、database、select 等)作为库名或表名==。若必须使用,需在 SQL 语句中强制加上反引号(“`),这会增加后续维护的复杂度,应尽量避免。

    2. 字符集(Charset)配置标准

    • 显式指定字符集:==在执行 CREATE DATABASE 时,必须显式加上 charset 参数==。如果不指定,MySQL 可能会使用默认的 latin1,导致存储中文时出现乱码。
    • 推荐使用 utf8mb4:相较于旧版的 utf8,utf8mb4 是更完整的字符集标准,支持 4 字节字符(如 Emoji 表情),能有效避免特殊字符写入时的报错。

    3. 操作环境与权限隔离

    • 使用专用测试库:严禁在系统自带的核心库(如 mysql、information_schema、performance_schema)中执行 DDL 操作。必须建立专用的测试数据库(如 test_db)进行命令练习。
    • 高频命令演练:在测试库中反复执行 create -> show create -> alter -> drop 的完整生命周期操作,观察每一步的系统反馈和报错信息。

    4. 结合现有备份知识的底层逻辑

    • 之前掌握的 mysqldump 工具,理解逻辑备份的本质:备份文件(如 .db 或 .sql 文件)实质上是将数据库的结构和数据转换成了一系列的 DDL(定义结构) 和 DML(插入数据)语句。
    • 在执行==数据恢复==(source 或 < 导入)时,若备份文件中包含建库语句(如 CREATE DATABASE),则无需手动建库;若备份文件中仅包含建表语句,则==必须先手动创建一个同名数据库==,否则导入会报错。

    image-20261014112420509

    image-20261014113630936

    image-20261014113836918

    image-20261014114004299

    备份流程图

    graph TD
        %% 定义样式
        classDef terminal fill:#2d2d2d,stroke:#666,stroke-width:2px,color:#fff;
        classDef mysql fill:#00758f,stroke:#005a6e,stroke-width:2px,color:#fff;
        classDef action fill:#f9f9f9,stroke:#333,stroke-width:1px;
        classDef success fill:#d4edda,stroke:#28a745,stroke-width:2px,color:#155724;
        classDef danger fill:#f8d7da,stroke:#dc3545,stroke-width:2px,color:#721c24;
    
        %% 阶段一
        subgraph 第一阶段: 准备数据
            A1[终端登录 MySQL]:::terminal
            A2[创建库 restore_test 并切换]:::mysql
            A3[创建表 student]:::mysql
            A4[插入测试数据]:::mysql
            A5[验证数据存在]:::success
        end
    
        %% 阶段二
        subgraph 第二阶段: 执行备份
            B1[退出 MySQL 命令行]:::mysql
            B2[终端执行 mysqldump 备份]:::terminal
            B3[生成备份文件 restore_test.db]:::action
        end
    
        %% 阶段三
        subgraph 第三阶段: 模拟灾难
            C1[重新登录 MySQL]:::terminal
            C2[删除数据库 restore_test]:::danger
            C3[验证已删除]:::action
        end
    
        %% 阶段四
        subgraph 第四阶段: 恢复数据
            D1[手动创建空库]:::mysql
            D2[切换到该库]:::mysql
            D3[执行 source 导入]:::mysql
        end
    
        %% 阶段五
        subgraph 第五阶段: 最终验证
            E1[查看表结构]:::mysql
            E2[查询数据成功恢复]:::success
        end
    
        %% 连接线
        A1 --> A2 --> A3 --> A4 --> A5
        A5 --> B1
        B1 --> B2 --> B3
        B3 --> C1 --> C2 --> C3
        C3 --> D1 --> D2 --> D3
        D3 --> E1 --> E2
    
        %% 强调核心逻辑
        C2 -.->|关键前提: 必须先建库| D1

    学习与实操建议

    1. 建表原则:规范与前瞻性

    • 严格定义属性:除了主键自增列,其他列尽量加上 not null 和 default 约束,这能有效减少业务代码中的空指针报错。
    • 必带注释:所有列都必须写 comment 注释,尤其是 enum 或 tinyint 表示状态的列(如 1代表正常,0代表禁用),否则后期维护极其困难。
    • 显式指定引擎和字符集:建表语句末尾强制带上 engine innodb 和 charset utf8mb4,避免依赖数据库的全局默认设置。

    2. 操作 ALTER TABLE 的注意事项

    • 性能影响:在==数据量极大的表==上执行 ALTER TABLE(特别是 MODIFY 修改数据类型、CHANGE 修改列名)==可能会导致锁表==。在生产环境中,建议在业务低峰期执行,或使用在线 DDL 工具(如 pt-online-schema-change)。
    • 修改数据类型可能导致数据丢失:例如将一个包含长字符串的 varchar(100) 列修改为 char(10),会导致超出 10 个字符的数据被截断。

    3. 强化命令记忆

    • 区分 MODIFY 与 CHANGE:记住 MODIFY 只改属性不改名字,CHANGE 名和属性都能改。
    • 明确 DESC 的用途:在遇到线上故障排查时,第一时间使用 desc 表名; 确认表结构,比去看建表语句更快捷。

    MySQL 列属性(Column Attributes)标准规范总结

    在 MySQL 的 CREATE TABLE 或 ALTER TABLE 语句中,==列属性用于对列的数据进行约束和规范==。根据适用范围,列属性可分为通用属性和特殊属性两类。

    一.通用属性(适用于绝大多数列)

    这些属性可以根据业务需求,添加在任意数据类型的列上。

    属性 作用 适用范围 说明
    NULL / NOT NULL 定义该列是否允许为空值 所有数据类型 默认为 NULL(允许为空)。建议核心字段显式声明 NOT NULL。
    DEFAULT 值 定义该列的默认值 所有数据类型 插入数据时若未指定该列,则自动填入默认值。
    COMMENT '注释' 为该列添加说明注释 所有数据类型 强烈建议所有列都添加,便于后期维护。
    UNSIGNED 无符号(非负数) 仅限数值类型 如 INT UNSIGNED,不能加在字符或时间类型上。

    二. 特殊属性(具有严格使用限制)

    这类属性不能随意添加,必须满足特定的前提条件。

    1. AUTO_INCREMENT(自增)

    • 作用:插入数据时,若未指定该列的值,数据库自动生成递增的整数。
    • 三大硬性限制:
      1. 数量限制:一张表最多只能有一个列使用 AUTO_INCREMENT。
      2. 索引限制:使用 AUTO_INCREMENT 的列,必须被定义为 PRIMARY KEY(主键)或 UNIQUE KEY(唯一键)。
      3. 类型限制:仅支持整数类型(如 INT、BIGINT、TINYINT),不可用于 VARCHAR、DATETIME 等类型。

    2. PRIMARY KEY(主键)

    • 作用:唯一标识表中的每一行记录。
    • 限制:一张表只能有一个主键,主键列的值必须唯一且非空(隐含 NOT NULL 和 UNIQUE 约束)。

    3. UNIQUE KEY(唯一键)

    • 作用:保证该列的值唯一。
    • 限制:与主键不同,唯一键允许为空值(NULL),且一张表可以有多个唯一键。

    三. 标准建表示例

    以下示例展示了通用属性与特殊属性的正确组合方式:

    五、 核心原则
    明确适用范围:添加属性前,先确认该属性是否适用于当前列的数据类型(如 UNSIGNED 仅限数值)。
    
    遵守自增规则:AUTO_INCREMENT 仅能用于整数主键,且每表仅限一列。
    
    规范定义约束:核心字段必须显式声明 NOT NULL,业务常用字段应设置 DEFAULT 默认值。CREATE TABLE student (
        -- 特殊属性:整数 + 主键 + 自增(正确)
        sid INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '学号',
    
        -- 通用属性:非空 + 注释
        sname VARCHAR(20) NOT NULL COMMENT '学生姓名',
    
        -- 通用属性:默认值 + 注释
        sage TINYINT DEFAULT 18 COMMENT '学生年龄',
    
        -- 通用属性:非空 + 默认值 + 注释
        sgender ENUM('m','f') NOT NULL DEFAULT 'm' COMMENT '学生性别',
    
        -- 通用属性:非空 + 注释
        comtime DATETIME NOT NULL COMMENT '入学时间'
    ) CHARSET=utf8 ENGINE=InnoDB;

    四.核心原则

    1. 明确适用范围:添加属性前,先确认该属性是否适用于当前列的数据类型(如 UNSIGNED 仅限数值)。
    2. 遵守自增规则:AUTO_INCREMENT 仅能用于整数主键,且每表仅限一列。
    3. 规范定义约束:核心字段必须显式声明 NOT NULL,业务常用字段应设置 DEFAULT 默认值。

    MySQL DCL 数据控制语言

    整体分为两大块: DCL(数据控制语言):管权限(用户授权、回收权限)

    ​ DML(数据操作语言):管表里的数据(增、改、删,还有伪删除方案)

    补充:SQL 四大分类简单区分

    DDL:库、表结构(create alter drop truncate)

    DML:表内数据(insert update delete)

    DCL:权限管理(grant revoke)

    DQL:查询 select

    一、DCL 数据控制语言

    DCL 作用:控制数据库用户的访问权限,创建用户、分配权限、收回权限

    1. grant 授权语句

    grant all on *.* to root@'192.168.226.%' identified by '123456'
    • grant all:授予所有权限(增删改查、建表等全部权限)

    • on *.*:*.* 代表所有库。所有表;如果写成 testdb.* 就是 testdb 库下面所有表

    • to root@'192.168.226.%'
      • root:用户名
      • 192.168.226.%:允许登录的客户端 IP,%是通配符,代表 192.168.226 网段任意 IP都可以登录
    • identified by '123456':设置这个用户的密码为 123456

    注释:授予 root@’192.168.226.%’ 用户所有权限,不是超级管理员

    grant all on *.* to root@'192.168.226.%' identified by '123456' with grant option;
    • 多了 with grant option:超级链路权限,==这个用户拿到权限后,还可以把自己的权限再授权给别的用户==

    用户资源限制参数(限制用户使用资源)

    • max_queries_per_hour:用户每小时最多能执行多少次查询
    • max_updates_per_hour:用户每小时最多执行多少次更新(update/insert)
    • max_connections_per_hour:用户每小时最多发起多少次连接
    • max_user_connections:该用户同一时间最多能保持多少个并发连接

    用途:防止某个用户疯狂占用数据库资源,做限流。

    ​

    2. revoke 回收权限

    revoke select on *.* from root@'192.168.226.%';
    • 含义:从用户root@192.168.226.% 回收 select(查询)权限
    • 语法:revoke 权限 on 库.表 from 用户;
    show grants for root@'192.168.226.%';
    • 查看这个用户当前拥有哪些权限。

    二、DML 数据操作语言

    DML:操作表里面的行数据,不修改表结构;包含 insert新增、update修改、delete删除

    1. insert 插入数据

    写法 1:简写(不推荐,必须和表字段顺序完全一致)

    insert into stu values('linux01',1,NOW(),'zhangsan',20,'m',NOW(),110,123456);
    • 特点:values 后面的值,顺序、数量、类型必须和建表时字段完全对应,少写 / 顺序错直接报错。

    写法 2:规范写法(推荐,指定字段)

    insert into stu(classid,birth,sname,sage,sgender,comtime,telnum,qq) 
    values('linux01',1,NOW(),'zhangsan',20,'m',NOW(),110,123456);
    • 在表名后面 (字段1,字段2...) 指定要插入哪些列
    • values 里面的值和前面括号里字段一一对应;可以只插入部分字段,没写的字段会自动填充默认值(允许 null 的填 null)

    写法 3:一次性插入多条记录

    insert into stu(classid,birth,sname,sage,sgender,comtime,telnum,qq) 
    values
    ('linux01',1,NOW(),'zhangsan',20,'m',NOW(),110,123456),
    ('linux02',2,NOW(),'zhangs i',21,'f',NOW(),111,1234567);

    多条记录用逗号隔开,一次性写入,性能优于多次单独 insert。

    NOW():mysql 内置函数,获取当前时间(年月日时分秒)

    2. update 更新修改数据

    ⚠️重点:update不加 where 条件会修改全表,生产严禁!!

    update student set sgender='f';
    # 不规范!没有where,会把student表所有行sgender全部改成f
    update student set sgender='f' where sid=1;
    # ✅规范写法:只修改sid=1这一行,把性别sgender改成f
    update student set sgender='f' where 1=1;
    # where 1=1 永远成立,等价于没有where,修改整张表

    修改 mysql 用户密码

    update mysql.user set password=PASSWORD('123456') where user='root' and host='localhost';
    • mysql.user:mysql 内置系统表,存放所有数据库账号信息
    • PASSWORD()函数:把明文密码加密
    • 修改系统用户后,必须执行 flush privileges; 刷新权限才能生效

    3. delete 删除数据

    ⚠️delete 不加 where 会清空整张表,高危操作!

    delete from student;
    # 不规范:删除student表里全部数据,表结构保留,自增id继续累加
    delete from student where sid=3;
    # ✅规范删除:只删除sid=3的这一行

    truncate 清空表

    truncate table student;

    重点区分 delete vs truncate

    1. truncate 属于DDL 语言(不是 DML),不是删除行,是重建整张表
    2. 清空所有数据,自增主键重置,重新从 1 开始
    3. 不记录事务日志,速度远快于 delete;不能回滚
    4. 表结构保留
    特性 delete truncate
    分类 DML DDL
    自增 id 继续增长 重置从 1 开始
    事务 可以回滚 不可回滚
    速度 慢(逐行删除记日志) 快,直接重建表

    三.alter ..status 伪删除(逻辑删除)

    业务场景:很多重要业务数据不能物理删除(删掉就找不回来了),比如订单、用户数据,需要保留记录,只是前端 “看不见”,用伪删除(逻辑删除)

    思路:不使用 delete,新增一个状态字段标记是否删除

    1. 新增状态字段
    alter table student add status enum('1','0') default 1;
    • alter table 修改表结构,给 student 表增加 status 字段
    • enum (‘1′,’0’):只能存 ‘1’ 或者 ‘0’
    • default 1:默认值是 1
      • status='1':正常有效数据
      • status='0':标记为已删除
    1. “删除” 操作,实际是 update 修改状态
    update student set status='0' where sid=1;
    # 把sid=1的数据标记为已删除,数据还在数据库里
    1. 查询数据的时候,只查正常数据
    select * from student where status=1;
    # 查询所有没有被标记删除的数据,被标记0的不会查出来

    优点:数据永久保留,可以恢复; 缺点:表数据会越来越多,需要定期归档。


    where 就是用来指定筛选条件

    where 作用:过滤行,只对满足条件的行执行 update /delete/select。

    update student set sgender='f' where sid=1;

    👉 where sid=1:只找到 sid 等于 1 那一行,只修改这一行。

    delete from student where sid=3;

    👉 where sid=3:只删除 sid=3 这一行。

    如果没有 where,或者 where 条件永远成立

    update student set sgender='f';
    -- 没有where → 表里面【所有行】全部修改
    
    update student set sgender='f' where 1=1;
    -- 1=1永远是true,条件对全部行成立,等价于不带where,全表修改

    ⚠️生产环境大坑:update /delete 忘记写 where,直接全表改 / 全表删!

    where 不只在 update、delete 用,查询 select 也用

    select * from student where status=1;

    where status=1:只查询状态为正常的数据,过滤掉标记删除的数据。

    where 就是行过滤器:满足条件的行才会被操作,不满足的跳过。

    重点总结

    1. DCL:grant 授权,revoke 回收权限,控制账号访问
    2. DML:insert 增,update 改,delete 删;update、delete 必须带 where
    3. delete 只删行、truncate 重建表,自增主键行为不一样
    4. 生产业务优先逻辑伪删除,慎用物理 delete/truncate

    插入数据实践

    image-20261015144532570

    image-20261015145006225

    image-20261015151323924

  • Mysql权限管理

    用户权限介绍

    • 权限
    INSERT,SELECT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN,  PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE
    • 每次设定只能有一个属主,没有属组或其他用户的概念
    grant     all privileges    on     *.*    to   user01@''192.168.226.%''  identified by    ''123'';
                    权限               作用对象          归属               密码

    作用对象分解

    • . [当前MySQL实例中所有库下的所有表]
    • wordpress.* [当前MySQL实例中wordpress库中所有表(单库级别)]
    • wordpress.user [当前MySQL实例中wordpress库中的user表(单表级别)]

    企业中权限的设定

    • 开发人员说:给我开一个用户
    • 沟通
      • 你需要对哪些库、表进行操作
      • 你从哪里连接过来
      • 用户名有没有要求
      • 密码要求
      • 发邮件
    • 一般给开发创建用户权限
    grant select,update,delete,insert on *.* to 'user01'@'192.168.226.%' identified by '123456';

    实验思考问题

    #创建wordpress数据库
    create database wordpress;
    #使用wordpress库
    use wordpress;
    #创建t1、t2表
    create table t1 (id int);
    create table t2 (id int);
    #创建blog库
    create database blog;
    #使用blog库
    use blog;
    #创建t1表
    create table tb1 (id int);

    授权

    1、grant select on *.* to wordpress@'192.168.226.%' identified by '123';
    2、grant insert,delete,update on wordpress.* to wordpress@'192.168.226.%' identified by '123';
    3、grant all on wordpress.t1 to wordpress@'192.168.226.%' identified by '123';

    模拟企业真实场景下,DBA(数据库管理员)为开发人员创建专属数据库、建表,并精细化分配权限。

    第一阶段:初始化环境与建库建表(准备“空房子”)

    在给开发人员授权之前,DBA 需要先把基础设施搭好。

    1. 创建数据库
      • create database wordpress;
      • create database blog;
      • 作用: 在 MySQL 实例中创建两个独立的业务库(模拟企业里不同的项目)。这就好比建了两栋独立的办公楼。
    2. 切换并建表
      • use wordpress;
      • create table t1 (id int);
      • create table t2 (id int);
      • use blog;
      • create table tbl (id int);
      • 作用: 进入具体的库(办公楼),并在里面建好具体的表(房间)。

    第二阶段:核心授权操作(给开发人员发“门禁卡”)

    这是你实验的重点。企业里开发人员来了,DBA 不能直接给 root 账号,必须遵循最小权限原则。你通过 grant 语句逐步开放权限。

    代码 1: grant select on *.* to wordpress@'192.168.226.%' identified by '123';

    • 作用拆解:

      • grant select:赋予查询权限。
      • on *.*:作用于所有数据库的所有表(全局级别)。
      • to wordpress@'192.168.226.%':创建/指定一个名为 wordpress 的用户,且限制只能从 192.168.226.x这个网段连接(通过IP限制来源,比允许任何IP安全)。
      • identified by '123':设置该用户的密码为 123。
    • 业务含义: 这一步给了开发人员查看全局数据的权限,通常用于排查问题时查阅基础数据,但还没有修改数据的权利。

    代码 2: grant insert,delete,update on wordpress.* to wordpress@'192.168.226.%' identified by '123';

    • 作用拆解:
      • insert,delete,update:赋予增、删、改权限(CRUD 中的 CUD)。
      • on wordpress.*:缩小范围,只针对 wordpress 这个库下的所有表。
    • 业务含义: 开发人员开始在自己的业务库(wordpress)里写代码了,他需要能修改自己业务库的数据。注意,他依然没有权限修改 blog 库的数据。

    代码 3: grant all on wordpress.t1 to wordpress@'192.168.226.%' identified by '123';

    • 作用拆解:
      • grant all:赋予所有权限(包括建表、删表、增删改查等)。
      • on wordpress.t1:精确到表级别,只对 wordpress 库里的 t1 这张表生效。
    • 业务含义: 这就是最小权限原则的体现。可能 t1 表是一个非常核心的配置表,开发人员需要最高权限去维护它,但同一个库里的 t2 表(可能涉及敏感数据)他依然只能用代码 2 里给的基础权限。

    代码 4:刷新权限 flush privileges;

    • 作用: 虽然用 grant 语句授权通常会自动刷新内存,但在企业操作规范中,执行完一整套权限变更后,执行 flush privileges; 是一个好习惯,确保权限立即生效。

    第三阶段:验证测试(以开发人员身份登录)

    授权做完了,必须验证是否生效。终端截图展示了这个验证过程:

    1. 尝试登录: mysql -uwordpress -p123(不带IP或带 -h localhost)

      • 结果: ERROR 1045 (28000): Access denied
      • 原因: 因为我们之前授权时,写的是 @'192.168.226.%'。当你直接本机登录(默认走localhost)时,MySQL 认为你不在授权的网段内,所以拒绝。
    2. 按授权规则登录: mysql -uwordpress -p123 -h 192.168.226.10

      • 结果: 成功进入 MySQL 命令行。
      • 原因: -h 指定了 192.168.226.10 这个 IP,完全匹配了之前的网段授权。
    3. 权限验证:

      • 进去后执行 use wordpress; 成功切换。
      • 执行 show tables; 成功看到了 t1 和 t2(因为有 select 权限)。
      • 执行 select * from t1; 成功读取数据(因为没有写入测试,所以显示 Empty set,但不报权限错误,说明查询权限正常)。

      image-20261011225042032

    image-20261011230950981

    权限级别(全局、库级、表级)和具体操作权限

    在企业级 MySQL权限管理中,这三条命令演示了基于角色的访问控制(RBAC)和最小权限原则的落地过程。

    第一条:全局只读权限(Global Read-Only)

    grant select on *.* to wordpress@'192.168.226.%' identified by '123';
    • 权限范围:*.* 代表全局级别(Global Level),即当前 MySQL 实例中的所有数据库和所有表。
    • 赋予的操作:select。仅允许执行 SELECT 查询语句。
    • 安全策略(Host 限制):@'192.168.226.%' 限制了该账号的登录来源 IP网段,非该网段的客户端连接会被直接拒绝。
    • DBA 视角的业务意义:为开发人员提供全局只读权限。这通常用于排查跨库的数据依赖问题,或进行全局数据抽取,但==严格禁止其对生产环境做任何数据变更(DML)或结构变更(DDL)==。

    第二条:库级读写权限(Database-Level Read/Write)

    grant insert,delete,update on wordpress.* to wordpress@'192.168.226.%' identified by '123';
    • 权限范围:wordpress.* 代表库级别(Database Level),权限被精确收敛到 wordpress 这一个数据库下的所有表。
    • 赋予的操作:insert(插入)、delete(删除)、update(更新)。结合第一条命令,该用户在 wordpress 库中具备了完整的 DML(数据操纵语言) 权限(CRUD)。
    • DBA视角的业务意义:满足开发人员对自身业务库进行日常数据维护的需求。同时,由于权限范围限制在 wordpress.*,该用户依然无法修改其他业务库(如 blog 库)的数据,实现了==业务隔离==。

    第三条:表级全权控制(Table-Level Full Control)

    grant all on wordpress.t1 to wordpress@'192.168.226.%' identified by '123';
    • 权限范围:wordpress.t1 代表表级别(Table Level),权限被进一步收敛到 wordpress 库中的 t1 这一张表。
    • 赋予的操作:all。代表该表上的所有可用权限,包括但不限于 DROP(删表)、ALTER(修改表结构)、CREATE(建索引)等 DDL(数据定义语言) 权限,以及所有 DML权限。
    • DBA视角的业务意义:应对特殊业务场景。例如,t1 可能是开发人员需要全权负责维护的临时表、日志表或核心配置表。==赋予 all 权限允许他们自主重建表结构或清空表数据==,而同一库中的其他表(如 t2)依然受限于第二条命令的普通读写权限。

    综合权限模型分析

    这三条命令构成了一个漏斗式的权限收敛模型:

    1. 全局层(*.*):授予基础的查询权限,保证跨库查阅能力。
    2. 数据库层(wordpress.*):授予业务所需的读写权限,满足日常开发需求。
    3. 数据表层(wordpress.t1):授予特定表的完全控制权限,应对特殊维护需求。

    重点: MySQL的权限是叠加(Union)生效的。该用户最终的权限是这三条命令权限的并集。这种分层授权既保证了开发人员有足够的权限完成工作,又严格限制了误操作或恶意操作波及的范围。


    客户端连接测试以及权限验证的过程

    image-20261011231551543

    这一整套操作,就是标准的DBA 用户授权后的功能验证(Functional Testing),确保交给开发人员的账号是符合预期且安全的。

    DBA 权限分配与验证 流程图

    text

    [阶段一:DBA 上帝视角] 
           │
           ▼
      mysql -uroot -p123456
           │
           ├─ 角色:数据库超级管理员(DBA)
           ├─ 权限:最高权限(建库、删库、建表、创建用户、分配权限等)
           └─ 现实意义:绝对保密,仅限核心 DBA 团队掌握,绝不外借给开发人员。
           │
           ▼
    [阶段二:DBA 派发权限] 
           │
           ▼
      grant ... to 'wordpress'@'192.168.226.%' identified by '123';
           │
           ├─ 角色:DBA(执行者) <--> 开发人员(申请者)
           ├─ 操作:DBA 用 root 登录,执行 grant 命令
           │    ├─ 网络层授权:放行 @'192.168.226.%'(限制来源 IP 网段)
           │    └─ 数据层授权:分配特定库表的读写权限
           └─ 原则:最小权限原则(只给工作必需的权限,绝不轻易 grant all)
           │
           ▼
    [阶段三:开发人员日常连接] 
           │
           ▼
      Navicat (输入主机 IP、账号 wordpress、密码 123)
           │
           ├─ 角色:开发人员
           ├─ 操作:使用图形化工具 Navicat 连接数据库
           ├─ 权限验证(连接成功即证明):
           │    ├─ DBA 分配的网络权限(IP限制)生效了
           │    └─ DBA 分配的库表权限生效了
           └─ 后续限制(实验结论验证):
                ├─ 尝试删掉 wordpress 库里的 t1 表 → 会被拒绝(无 DROP 权限)
                └─ 尝试修改 blog 库里的数据 → 会被拒绝(无跨库写权限)

    逻辑架构视角(权限收敛漏斗)

    如果你从系统架构的角度来看,这三步其实是一个权限逐级收敛、安全性逐级提高的过程:

    text

    ===================== 权限边界 =====================
          【最高层】 root 账号 (localhost)
            └─> 拥有全部 DDL / DML / 授权 / 管理权限
                     │
                     │ (DBA 执行 grant 命令,向下派发)
                     ▼
          【中间层】 wordpress 账号 (192.168.226.% 网段)
            ├─> 全局层 (*.*) : 只有 SELECT (全局只读)
            ├─> 数据库层 (wordpress.*) : 拥有 INSERT, DELETE, UPDATE (库级读写)
            └─> 数据表层 (wordpress.t1) : 拥有 ALL (表级全权)
                     │
                     │ (开发人员用 Navicat 连接)
                     ▼
          【最底层】 开发人员实际可操作范围
            └─> 只能严格按照 DBA 分配的库表权限执行操作,多一步都做不了

    核心总结(一句话牢记)

    DBA用 root 手握全局,通过 grant 像做手术一样精细划分权限,最后开发人员只能用 wordpress 账号在划定的圈子里干活。 这就是企业级数据库安全管理的标准范式。

    image-20261012000836323

    image-20261012003911387

    案件复盘:1142 报错背后的真相

    线索一:操作者是谁?—— 身份决定权限

    • 表面现象:Navicat左侧连接名为 wordpress连接和权限测试,报错 INSERT command denied。
    • 底层本质:你这次没有用 root 越权操作,而是老老实实按规范使用了 wordpress 普通账号。MySQL 服务器精准识别了你的身份,并执行了权限判定。
    • 最终结论:系统“认号不认人”,你是普通用户,就只享有普通用户的待遇。

    线索二:数据从哪来?—— 能看 ≠ 能改

    • 表面现象:表格里还能看到 10 和 111111,但当你想插入 888 时却被拒绝。
    • 底层本质:
      • 那两条数据是你(DBA)之前用 root 账号在 Linux 终端强行插入的。
      • 现在你能看见它们,是因为你拥有 grant select on *.* 的全局只读权限。
      • 但你现在改不动,是因为你没有 blog 库的写入权限。
    • 最终结论:允许跨库查数据辅助开发,严禁跨库篡改数据保证安全。

    线索三:锅该谁背?—— 工具只是传话筒

    • 表面现象:你曾怀疑“是不是 Navicat 默认给了我权限?”
    • 底层本质:Navicat 只是将你的鼠标操作(点击保存)翻译成 INSERT SQL语句发送给服务器。服务器核对权限后拒绝,Navicat只是把错误原封不动地弹出来给你看。
    • 最终结论:Navicat只是传话筒,MySQL 才是铁面无私的判官。绝对不存在“工具绕过权限”一说。

    MySQL连接管理

    不建议使用的方法:

    vim /etc/my.cnf
    [client]
    user = root
    password = 123456
    • MySQL

      • -u:指定用户
      • -p:指定密码
      • -h:指定主机
      • -P:指定端口
      • -S:指定sock
      • -e:指定SQL
    • 第三方连接工具

      • sqlyog
      • navicat

    1. 常见的几种登录方式

    • 方式一:明文密码(极度不推荐)
      • 命令:mysql -uroot -p123456
      • 特点:把密码直接写在命令里。虽然敲起来快,但极其危险。同一台服务器上的其他用户,只要执行 ps aux | grep mysql,就能直接看到你的密码明文。在生产环境绝对禁止。
    • 方式二:交互式输入密码(最推荐 ✅)
      • 命令:mysql -uroot -p
      • 特点:敲完回车后,系统会提示 Enter password:,此时你输入密码,屏幕不会显示任何字符(不显示星号,也不显示数字)。这种方式既安全(密码不会留在历史命令或进程里),又通用(不需要依赖任何配置文件)。
    • 方式三:依赖配置文件(图1里的写法,不推荐 ❌)
      • 命令:直接敲 mysql
      • 特点:这就是你刚才在 /etc/my.cnf 的 [client] 下面配置了 user 和 password 之后的效果。
      • 缺点:极度不安全。任何能登上这台服务器的人,只要敲 mysql 就能以 root 身份进去。生产环境严禁这么干。
    • 方式四:使用 mysql_config_editor(企业级安全用法)
      • 命令:先执行 mysql_config_editor set --login-path=local --host=localhost --user=root --password,然后以后登录只需敲 mysql --login-path=local。
      • 特点:这是 MySQL官方提供的加密登录方式。密码会被加密存储在一个 .mylogin.cnf 文件里,比明文写在配置文件里安全得多。企业里用得多。

    image-20261012013226239

    image-20261012013258341


    企业级 MySQL 用户权限管理方案与实战命令

    最小权限原则(Least Privilege)和职责分离(Separation of Duties)。

    第一部分:创建受限超级管理员与权限回收

    这部分演示了 DBA如何创建一个“受约束的超级管理员”,以及如何回收不再需要的权限。

    1. 创建带资源限制的超级管理员

    sql

    CREATE USER 'super_admin'@'localhost' IDENTIFIED BY 'StrongAdminP@ssw0rd';
    GRANT USAGE ON *.* TO 'super_admin'@'localhost'
      WITH
        MAX_QUERIES_PER_HOUR 0
        MAX_UPDATES_PER_HOUR 0
        MAX_CONNECTIONS_PER_HOUR 0
        MAX_USER_CONNECTIONS 10;
    • CREATE USER ...:先创建一个账号,密码设置得比较强(符合安全规范)。
    • GRANT USAGE ...:这里非常巧妙。USAGE 本身代表“没有任何实际权限”,仅仅只允许该用户登录数据库。
    • WITH ...:这是 MySQL的资源限制子句。
      • MAX_QUERIES_PER_HOUR 0:每小时查询次数无限制(0代表无限制)。
      • MAX_UPDATES_PER_HOUR 0:每小时更新操作无限制。
      • MAX_CONNECTIONS_PER_HOUR 0:每小时连接次数无限制。
      • MAX_USER_CONNECTIONS 10:这是核心。限制该账号最多只能同时保持 10 个活跃连接。这可以防止因为程序 Bug 或恶意攻击导致数据库连接池被瞬间耗尽。

    2. 授予管理员权限

    sql

    GRANT ALL PRIVILEGES ON *.* TO 'super_admin'@'localhost' WITH GRANT OPTION;
    • 赋予了所有库和表的全部权限(ALL PRIVILEGES)。
    • WITH GRANT OPTION:这个子句非常关键,它允许该用户把自己拥有的权限再授权给其他用户。这才是真正意义上的“管理员”权限。
    • 注意:由于前面设置了资源限制,这个超级管理员的连接数依然受限于 10,所以它不是“无限膨胀”的。

    3. 回收权限

    sql

    REVOKE SELECT ON *.* FROM root@'192.168.226.%';
    • 这里演示了权限回收。假设之前因为某个特殊原因,给 192.168.226.% 网段的 root 用户开了全局查询权限,现在任务完成了,出于安全考虑,必须用 REVOKE 收回。
    • 如果是验证权限,可以使用 SHOW GRANTS FOR root@'192.168.226.%'; 来确认回收是否成功。

    第二部分:不同角色的精细化权限分配方案

    这部分是整个文档的精华,它针对企业里常见的 6 种角色,给出了详细的权限设计方案。

    1. 开发人员 (Developer)

    • 核心需求:日常写代码、调试,需要操作开发/测试库,通常不接触生产库。

    • 基础权限:SELECT, INSERT, UPDATE, DELETE(针对开发库业务表)。

    • 结构权限:CREATE TABLE, ALTER TABLE, CREATE INDEX(允许他们建表和修改表结构,方便开发)。

    • 限制(红线):禁止 DROP(防止误删表)、禁止 TRUNCATE(防止清空表)、禁止 GRANT(防止开发人员私自授权,造成数据泄露)。

    • 授权命令示例:

      sql

      GRANT SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, ALTER TABLE 
      ON dev_db.* TO 'developer'@'192.168.1.%';

    2. 测试人员 (Tester)

    • 核心需求:执行测试用例,写入测试数据,检查功能。
    • 权限:只有数据操作权限(SELECT, INSERT, UPDATE, DELETE)。
    • 限制:禁止结构操作(不能改表结构、建表、删表)、禁止权限管理(不能 GRANT)。这是为了防止测试人员破坏测试环境的数据结构,导致测试数据失效。

    3. 数据分析/报表人员 (Data Analyst)

    • 核心需求:提取数据,制作报表。

    • 权限:只读(SELECT)。通常只针对特定的表或视图。

    • 安全亮点:通过视图隐藏敏感列。

      sql

      CREATE VIEW safe_user AS SELECT id, name, register_date FROM user_info;
      GRANT SELECT ON safe_user TO 'analyst'@'%';

      把身份证、手机号等敏感信息隐藏起来,只授予视图的查询权限。这样分析人员看不到敏感数据,但依然能完成分析工作。

    4. 运维、DevOps(Operations)

    • 核心需求:监控数据库状态、备份、维护。
    • 权限:BACKUP DATABASE(备份)、SHUTDOWN(关库)、PROCESS(查看进程)。
    • 限制:禁止 INSERT/UPDATE/DELETE 业务数据(防止运维误改数据)、禁止 DROP(防止破坏结构)。

    5. 业务系统账号 (App Account)

    • 核心需求:应用程序连接数据库,执行代码逻辑。
    • 权限:根据业务需要,仅授予 SELECT/INSERT/UPDATE/DELETE(针对具体的业务表)。
    • 限制:禁止 CREATE/ALTER/DROP。非常重要!因为程序 Bug 或 SQL 注入一旦发生,如果账号有DDL 权限,整个表结构可能瞬间被毁。如果只有 DML 权限,最多被删几行数据,可以通过备份快速恢复。也禁止 GRANT,防止权限扩散。
    • 特殊:如果程序需要用临时表,可以单独授予 CREATE TEMPORARY TABLES 权限。

    6. DBA 自身 (DBA)

    • 最高权限:ALL PRIVILEGES(只用于紧急情况和核心管理)。
    • 审计权限:SELECT 审计日志表,ALTER 审计规则。
    • 最佳实践:日常操作不建议使用 root 或高权限账号。强烈建议DBA 也通过堡垒机(Bastion Host,一种跳板机)来操作,这样所有操作都会被日志记录,防止内部人员作恶。

    总结:权限分配的通用原则

    文档最后总结的这三点,是所有运维人员和安全人员必须遵守的铁律:

    1. 环境隔离:生产、测试、开发环境必须用不同的账号体系,严格隔离,生产库的权限要给到最小。
    2. 动态调整:员工离职、转岗,必须立即回收权限(定期审计非常重要,比如每季度审一次)。
    3. 日志追溯:所有的权限变更(GRANT/REVOKE),以及敏感的 DELETE 操作,都必须要有审计日志,做到“有迹可循”。

    Mysql用户管理

    MySQL用户与主机域核心知识

    username@’主机域’ 主机域:可以理解为是Mysql登录的白名单 主机域格式:

    192.168.226.10
    192.168.226.1%
    192.168.226.1 192.168.226.10-19 192.168.226.100-199
    192.168.226.%
    192.168.%.%
    %
    `localhost`
    192.168.226.0/255.255.255.0

    一、 用户定义格式与作用

    MySQL 用户的核心定义格式是:username@'主机域'(Host)。

    • 主机域的作用:可以理解为 MySQL登录的白名单。它决定了特定的账号只能从哪些IP 地址或网段连接到数据库,这是 MySQL安全机制的重要一环。

    二、 主机域(Host)的常见书写格式

    在实际配置中,主机域可以使用具体的 IP、通配符(%)或网段形式,以应对不同的访问需求。结合当前环境(本机 IP 为 192.168.226.10),常见的格式如下:

    • 192.168.226.10:精确匹配单个 IP。只允许这一台机器(即本机)连接到数据库。
    • 192.168.226.%:C 类网段匹配。允许 192.168.226.1 到 192.168.226.254 这个网段内的所有机器连接。
    • 192.168.%.%:B 类网段匹配。允许 192.168 开头的所有网段(范围极大)。
    • 192.168.226.1%:前缀模糊匹配。匹配以 192.168.226.1 开头的所有 IP 地址(例如 192.168.226.10、192.168.226.100 等,不够精确,需慎用)。
    • %:全匹配。允许从任何IP地址连接(生产环境极度危险,慎用)。
    • localhost:本地 Socket 连接。仅允许从本机服务器发起连接,不经过 TCP/IP 网络协议(这是默认 root 用户的常用配置)。
    • 192.168.226.0/255.255.255.0:子网掩码形式。精确指定 192.168.226.0 整个网段(与 192.168.226.% 效果类似)。

    三、 实战用户表的操作与解析

    1. 切换系统库

    • 执行命令:use mysql;
    • 作用:切换到 MySQL 自带的系统数据库。MySQL所有的==用户账号、密码、权限信息==都存放在这个库中的 user 表里。

    2. 查询所有列(重点疑问)

    • 执行命令:select * from user;
    • 为什么会有那么多列输出? ==因为 select * 表示查询该表的所有字段== 。MySQL将用户的每一个具体权限(如增、删、改、查、建表、授权等)都设计成了独立的列字段。
      • 基本认证列:Host(主机域)、User(用户名)、Password(加密后的密码哈希值)。
      • 权限控制列:Select_priv、Insert_priv、Update_priv、Grant_priv 等等。这些列的值通常是 Y 或 N,代表该用户是否具备全局级别的某项权限。
      • 其他属性列:plugin(密码加密插件,截图显示为 mysql_native_password)、password_expired(密码是否过期)等。
      • 注意:当把这张表的所有列全部在一行中打印出来时(如您截图中的长列表),终端会显示得非常长,同时很多列(如 N 或 0)是因为该用户为普通用户或受限用户,没有开启这些全局权限。
      • image-20261012202746450

    3. 优化查询(查看核心信息)

    • 执行命令:select host,user from user;

    • 执行结果解析(结合您的环境): 这条命令==只提取最关键的两列==,输出会变得非常简洁清晰。在当前环境下,执行后会列出如下几行核心数据:

      • 127.0.0.1 | root
      • 192.168.226.% | wordpress
      • ::1 | root
      • localhost | root
      • localhost.localdomain | root
      • localhost | (空)
      • localhost.localdomain | (空)
      • 分析:这代表当前服务器默认配置了 root 账号用于本地访问(localhost 及几个本机回环IP 别名),并为名为 wordpress 的网站程序配置了专用的连接账号(允许从 192.168.226 网段连接)。最后两行空用户名通常代表初始自带的无密码匿名用户,出于安全考虑通常建议将其删除。

      image-20261012203822899

    image-20261012203622207


    用mysqladmin管理工具设置了root用户的密码

    mysqladmin -uroot password '123456' 通过用户名和密码来登录数据库 mysql -uroot -p123456

    1. 为什么它必须要在服务端(Linux终端)运行?

    mysqladmin 是一个独立的可执行程序(通常存放在 MySQL的 bin 目录下,比如 /usr/bin/mysqladmin)。 它的运行机制是:

    • 接收你输入的参数(比如 -uroot -p123456)。
    • 通过网络或本地 Socket 连接去连上 MySQL 服务端。
    • 向服务端发送一条“修改密码”的指令。
    • 服务端执行成功后,mysqladmin 程序就退出了。

    因为它是一个独立的程序,所以必须要在操作系统的命令行(也就是您截图里的 [root@localhost ~]#)下运行。一旦您输入 exit 退出了 mysql> 交互环境,就回到了操作系统层面,这时候再敲 mysqladmin,系统才认识它。

    2. 为什么在 mysql> 里面输入 mysqladmin 会报错?

    因为 mysql> 是一个SQL 解析器。您在里面输入的所有东西,都会被当作SQL 语句来解析。

    • 您输入 SELECT * FROM user;,它认识,去执行。
    • 您输入 mysqladmin ...,它心里想:“这什么玩意儿?SQL 语句哪有以 mysqladmin 开头的?”于是直接甩出 ERROR 1064(语法错误)。

    3. mysqladmin 和 SQL 命令改密码的区别

    截图里展示了两种改密码的方式,它们其实是殊途同归,只是入口不同:

    • 方法一(服务端外部工具,截图里做的): 在 Linux 终端:mysqladmin -uroot -p123456 password '654321' 特点: 不需要进入MySQL,直接在操作系统层面调用工具去连接并修改密码。
    • 方法二(客户端内部SQL语句): 先登录进 mysql>,然后执行: ALTER USER 'root'@'localhost' IDENTIFIED BY '654321'; 特点: 利用 SQL语句本身来修改权限表里的密码字段。

    image-20261012205721569


    忘记 root 密码后的强制重置流程

    它的核心思路是:让 MySQL 启动时暂时“忘记”检查密码(跳过权限表验证),从而让我们能免密登录进去,修改完密码后,再恢复正常启动。

    # 如果mysql的密码忘记了,如何解决?
    [root@server1 ~]# /etc/init.d/mysqld stop
    [root@server1 ~]# mysqld_safe --skip-grant-tables --skip-networking 
    [root@server1 ~]# mysql
    mysql> use mysql
    mysql> update user set password=PASSWORD('123456') where user='root' and host='localhost';
    Query OK, 0 rows affected (0.00 sec)
    Rows matched: 1  Changed: 0  Warnings: 0
    
    mysql> flush privileges;
    Query OK, 0 rows affected (0.00 sec)
    mysql> quit
    Bye
    [root@server1 ~]# pkill mysql
    [root@server1 ~]# systemctl start mysqld.service
    [root@server1 ~]# mysql -uroot -p123456
    

    第一阶段:停止当前服务,进入“无密码”模式

    1. [root@server1 ~]# /etc/init.d/mysqld stop

    • 作用:停止正在运行的 MySQL服务。
    • 原因:你要修改底层的认证方式,必须先让数据库停下来。

    2. [root@server1 ~]# mysqld_safe --skip-grant-tables --skip-networking &

    • 作用:这是整个流程的灵魂步骤。使用 mysqld_safe 工具(一个守护进程启动脚本)来启动MySQL。
    • 关键参数:
      • --skip-grant-tables:跳过权限表验证。==启动时,MySQL不会去加载 mysql.user 表里的密码和权限信息==。这意味着,任何人都可以免密登录,且拥有最高权限。
      • --skip-networking:跳过网络连接。防止其他机器通过网络连上这个“不设防”的数据库,只允许本机(localhost)通过 Socket 连接,保证重置过程的安全性。
    • 注:截图里的命令末尾虽然没有加 &(让程序在后台运行),但在实际操作中,敲下这行命令后,终端通常会卡住(因为它在运行),此时需要再开一个终端窗口来执行后面的步骤。

    第二阶段:免密登录并修改密码

    3. [root@server1 ~]# mysql

    • 作用:由于上一步已经跳过了权限验证,此时输入 mysql 命令,不需要 -u 和 -p 参数,直接就能进入 MySQL 交互界面。

    4. mysql> use mysql

    • 作用:切换到 mysql 系统库,准备修改 user 表。

    5. mysql> update user set password=PASSWORD('123456') where user='root' and host='localhost';

    • 作用:这是核心的密码修改语句。
      • 定位目标:找到 user 表中,用户名为 root 且主机为 localhost 的那一行记录。
      • 赋值操作:将 password 字段的值,修改为 PASSWORD('123456')。PASSWORD() 是一个内置函数,用于生成加密后的哈希值。
    • 结果提示解读:
      • Query OK, 0 rows affected:看起来好像没改(0行受影响),但在老版本 MySQL(如5.6/5.7之前)里,如果设置的新密码和旧密码一样,MySQL 可能认为不需要更新,所以显示 0。这并不代表密码没生效。
      • Rows matched: 1 Changed: 0:说明找到了一条符合条件的数据(匹配了1行),但可能因为密码没有变化,所以没有实质性的修改(Changed 0)。
      • 如果你的目标是设置一个全新的密码,这里就会显示 Changed: 1。

    6. mysql> flush privileges;

    • 作用:刷新权限表。
    • 原因:这是一个极其重要的习惯。在我们直接通过 update 修改完系统表后,必须通知MySQL服务器重新加载权限表,这样新的密码才会立即生效。

    7. mysql> quit

    • 作用:退出当前的 MySQL客户端环境。

    第三阶段:恢复正常状态并验证

    8. [root@server1 ~]# pkill mysql

    • 作用:强制杀掉所有与MySQL相关的进程。
    • 原因:刚刚启动的那个 MySQL是在“无权限验证”的畸形状态下运行的,不能一直这样开着。必须把它彻底停掉,才能正常启动。

    9. [root@server1 ~]# systemctl start mysqld.service

    • 作用:使用系统服务管理工具(systemctl),正常启动 MySQL。
    • 结果:此时MySQL会带着完整的权限表加载,正常启动,不再跳过验证。

    10. [root@server1 ~]# mysql -uroot -p123456

    • 作用:使用刚刚重置的新密码 123456 尝试登录数据库。
    • 如果成功,说明整个重置流程圆满完成。

    /etc/init.d/mysqld为啥因为PID停止失败

    1. 真正的数据库进程(/application/mysql/bin/mysqld)

    • 定位:这是 MySQL的核心二进制可执行文件。
    • 启动方式:执行该命令时,它启动数据库引擎,分配内存,监听 3306 端口,准备处理 SQL请求。
    • 与配置文件的关系:它启动时依赖 --defaults-file=/etc/my.cnf 参数。该参数指定了配置文件路径,mysqld 会读取并严格按照该文件内的参数(如数据目录、端口、缓冲区大小等)运行。
    • 特点:它不认识 start、stop 等操作指令。如果在终端直接执行,它会在前台运行并占用终端。

    2. 服务管理脚本(/etc/init.d/mysqld)

    • 定位:这是一个 Shell 脚本,用于封装对 MySQL进程的启停管理。
    • 工作方式:它接受 start、stop、restart、status 等参数,并执行相应的逻辑。
    • 执行 stop 的逻辑:脚本会去指定路径(如 /var/run 或数据目录)查找 mysqld.pid 文件。读取里面的进程号(PID),然后调用 kill 命令终止该进程。如果找不到PID文件,脚本就会报错 PID file could not be found!。
    • 与配置文件的关系:脚本内部通常也会引用 /etc/my.cnf 配置文件,但它的目的是为了获取PID 文件路径等管理相关的信息,而不是像 mysqld 那样加载所有数据库运行参数。

    3. 两者在本次操作中的关系(为什么会报错,以及如何处理)

    • 正常情况下,脚本(/etc/init.d/mysqld)负责管理核心进程(mysqld)。脚本启动 mysqld 时,会把进程号写入 PID文件,以便后续通过 stop 命令读取并停止它。
    • 当前报错 PID file could not be found! 的原因:脚本无法找到 PID 文件,因此它不知道要终止哪个进程。
    • 报错并不意味着 mysqld 进程没有运行。通过 ps -ef | grep mysql 可以看到,mysqld 进程(PID 817)实际上正在运行。
    • 因此,当前的操作逻辑是:由于管理脚本无法通过PID文件正常停止该进程,我们手动获取其 PID(817),直接使用 kill -9 817 命令终止它。停止后,再通过 mysqld_safe --skip-grant-tables 等参数手动启动,以进行后续的密码重置等操作。

    一句话总结: mysqld 是实际运行的服务进程,依赖 /etc/my.cnf 启动;/etc/init.d/mysqld 是管理该进程的脚本,依赖 PID 文件执行停止操作。PID 文件丢失导致脚本停止失败,所以需手动通过 PID强制终止进程。

    image-20261012212710720

    image-20261012221408351

    image-20261012221817075


    MySQL 中 修改密码的三种核心途径

    第一部分:修改密码的三种方式详解

    这三种方式其实殊途同归,最终目的都是修改底层 mysql.user 表里的密码字段,但适用场景和生效机制不同。

    # 1.直接修改,修改完直接生效
    mysql> set password=PASSWORD('123456');
    # 2.修改数据表user中的password字段,修改完需要flush privileges
    mysql> update user set password=PASSWORD('321') where user='root' and host='localhost';
    # 3.在授权的时候可以在最后通过identified by来指定密码,修改完需要flush lrivileges
    mysql> grant all privileges on *.* to user01@'192.168.226.%' identified by '123456';
    

    1. 直接修改(SET PASSWORD)

    • 语法:SET PASSWORD = PASSWORD('123456');
    • 机制:这是 MySQL提供的专用 SQL 语句。它专门用来修改当前登录用户的密码。
    • 生效:修改完立刻生效,不需要 flush privileges。因为 MySQL内部会直接更新内存中的权限缓存。
    • 适用场景:用户已经登录,且拥有修改自己密码的权限。
    • 避坑:在 MySQL8.0 中,不再使用 PASSWORD() 函数,直接写 SET PASSWORD = '123456'; 或者 ALTER USER 'root'@'localhost' IDENTIFIED BY '123456';。

    image-20261012224853003

    image-20261012224945986

    2. 修改数据表(UPDATE)

    • 语法:UPDATE user SET password=PASSWORD('321') WHERE user='root' AND host='localhost';

    • 机制:这是一种“硬核”修改方式。绕过 SQL语法层,直接去修改 mysql 系统库中的 user 表。

    • 生效:修改完必须执行 flush privileges;。因为直接修改表,MySQL的内存缓存(比如 information_schema)不会自动更新,必须手动刷新。

    • 适用场景:

      • 忘记密码,处于 --skip-grant-tables 免密模式时(此时 SET PASSWORD 等命令会报错,只能通过 UPDATE 表来修改)。
      • 批量修改多个用户或特定主机域的用户密码。

      image-20261012225908358

    image-20261012225934926

    3. 授权时指定(GRANT ... IDENTIFIED BY)

    • 语法:GRANT ALL PRIVILEGES ON *.* TO username@'192.168.226.%' IDENTIFIED BY '123456';
    • 机制:这是 MySQL5.7 及以前特有的语法。它把“创建用户”、“赋权”和“设置密码”三个动作合为一体。
    • 生效:修改完必须执行 flush privileges;。
    • 适用场景:新建用户并分配权限时,顺手设置好初始密码。
    • 避坑(非常重要!):在 MySQL 8.0 中,GRANT 语句不能再用来创建用户或设置密码了。必须先 CREATE USER,再 GRANT。所以这个语法在 8.0 里已经废弃。

    image-20261012234430220

    image-20261012234303804


    MySQL 三层启动调用链

    核心启动链:

    /etc/init.d/mysqld` ——(调用)——> `mysqld_safe` ——(拉起并监控)——> `mysqld

    第一层:/etc/init.d/mysqld —— 标准化接口(面向操作系统)

    1. 它是什么? 这是一个 Shell 脚本(在 CentOS 6及更早版本中是标准写法,CentOS 7+ 逐渐被 systemctl 取代)。它是 Linux 系统管理服务的“标准入口”。

    2. 它的职责(为什么需要它?):

    • 适配系统规范:Linux 管理服务有一套统一的标准(比如用 service mysqld start 或 systemctl start mysqld)。这个脚本的作用就是把系统的标准命令翻译成 MySQL能听懂的话。
    • 环境准备:在真正拉起数据库之前,它会做很多繁琐的准备工作:
      • 读取配置文件(/etc/my.cnf)。
      • 检查数据目录的权限(确保 mysql 用户有权限读写 /var/lib/mysql)。
      • 检查 PID文件是否残留、端口是否被占用。
    • 结果反馈:当底层启动完毕后,它会向系统返回 OK 或 FAILED,让你能在屏幕上看到成功或失败的状态。

    总结:它是面向操作系统的接口层,负责处理所有的系统对接和前置检查工作。


    第二层:mysqld_safe —— 中间层守护(安全与监控)

    1. 它是什么? 它是一个 守护进程(Daemon),本质上也是一个 Shell 脚本。它存在于第一层和核心数据库之间,是一个中间层。

    2. 它的职责(为什么需要它?):

    • 安全保障:它负责在启动核心数据库前,把一些不安全的因素排除掉。
    • 故障恢复(最核心作用):mysqld 这个核心进程有时候会因为内存溢出、Bug 或异常 SQL而崩溃。如果 mysqld 直接挂在系统上,服务就彻底死了。 而 mysqld_safe 会一直监控 mysqld 进程,如果发现它意外退出了,mysqld_safe 会立刻把它重新启动,保证数据库的高可用性。
    • 日志记录:负责记录启动过程中的错误日志(如 error.log),方便排查问题。
    • 参数传递:像我们之前模拟忘记密码时用的 --skip-grant-tables 和 --skip-networking,通常就是通过 mysqld_safe 把这个参数传递给 mysqld 的。

    总结:它是中间层的守护进程,负责监控核心进程并提供故障恢复机制。


    第三层:mysqld —— 核心功能实现(数据库本体)

    1. 它是什么? 这是 MySQL 真正的核心服务进程(二进制可执行文件,通常在 /usr/sbin/mysqld 或 /application/mysql/bin/mysqld)。它是实际执行数据库功能的核心程序。

    2. 它的职责(为什么需要它?):

    • 处理SQL 语句:解析客户端发来的 SELECT、UPDATE 等语句。
    • 管理存储引擎:操作 InnoDB 或 MyISAM引擎去读写磁盘上的数据文件。
    • 管理连接:监听 3306 端口,处理用户连接和权限验证。
    • 维护内存结构:管理 Buffer Pool、日志缓冲区等。

    3. 它的特点: 它极度专注,只做数据库该做的事。它不管防火墙,不管系统服务规范,不管崩溃后怎么重启(那是前两层的事)。它一旦启动,就会一直运行,直到被手动停止或崩溃。

    总结:它是核心功能实现层,专注于数据库服务和数据处理本身。

    为什么这种“分层设计”如此重要?(架构分析)

    如果把这三层合并成一个大程序,会带来什么问题?

    1. 不兼容:不同 Linux 发行版(Ubuntu、CentOS)管理服务的方式不同,合并会导致兼容性极差。
    2. 不稳定:核心数据库一旦崩溃,整个服务就全挂了,没有中间层去恢复它。
    3. 难维护:日志、权限检查、启动参数传递都和核心业务代码搅在一起,出错了很难排查。

    三层分层的好处:

    • 各司其职(单一职责):系统层管环境,守护层管生存,核心层管业务。

    • 高可用:mysqld_safe 的存在,大大提高了数据库的健壮性。

    • 可移植:只要针对不同的 Linux 系统写不同的 /etc/init.d/mysqld 脚本,底层的 mysqld_safe 和 mysqld 几乎不需要改动。

    补充:现代 MySQL 的变化(CentOS7+ 和 MySQL 8.0)

    现在(CentOS 7 及以上),/etc/init.d/mysqld 已经被 systemd 取代(即 systemctl start mysqld)。 但是,分层的核心思想没有变。

    • 现在的 systemd 会直接接管 mysqld,不再经过 mysqld_safe(因为 systemd本身就带有 Restart=on-failure 自动重启功能,替代了 mysqld_safe 的部分职责)。
    • 但在一些手动编译安装的 MySQL5.6/5.7 环境中,这三层调用链依然经典且常见。

    一句话总结:系统接口 -> 守护进程 -> 核心服务,层层递进,构建了稳定可靠的 MySQL 服务体系。


    1. 从文件属性看:它是一个具有执行权限的文件

    Linux 系统允许这个文件作为程序被运行

    2. 从文件内容看:它是一个 Shell 脚本

    看截图里的 cat /etc/init.d/mysqld 输出:

    • 第一行:#!/bin/sh 这是最关键的标志。这行叫做 Shebang。它告诉 Linux 内核:“这个文件是个脚本,请用 /bin/sh(也就是 Shell)来解释并执行它”。所以,它本质上就是一个 Shell 脚本。
    • 后续的注释: 大段的 # 开头的内容,都是在描述这个脚本的历史、用途(启动/停止 MySQL守护进程)、以及在老版本 Linux(如 RedHat)中的系统运行级别配置。这些是给管理员看的说明文档。

    3. 脚本内部通常包含什么?

    ​ 这个 mysqld 脚本的完整逻辑通常是这样的:

    • 它接收系统传来的参数(如 start、stop、restart、status)。
    • 它检查环境(检查配置、检查权限)。
    • 最核心的:它调用真正的程序。当接收到 start 参数时,它会去执行 /application/mysql/bin/mysqld_safe 或直接执行 mysqld。

    image-20261013000448562

    用户指令

    1. 查看系统用户表

    命令: mysql> select user,host from mysql.user;

    • 作用:查询 mysql 系统库中的 user 表,但只提取 user(用户名)和 host(允许登录的主机域)这两列。

    2. 查看当前登录的用户

    命令: mysql> select user();

    • 作用:查看当前这个连接是以哪个用户的身份登录的。
    • 结果解读(1 row in set): 输出 root@localhost。意思就是:您现在正以 root 用户的身份,从 localhost(本机)登录着MySQL。

    3. 查看当前数据库(成功但为空)

    命令: mysql> select database();

    • 作用:查看当前处于哪个数据库中。
    • 结果解读(1 row in set): 输出 NULL。意思是:您现在虽然登录了MySQL,但并没有选中任何数据库(刚登录进来默认就是没有选中任何库)。

    5. 切换数据库

    命令: mysql> use mysql;

    • 作用:切换到名为 mysql 的系统数据库。
    • 提示信息:
      • Reading table information for completion of table and column names:这是 MySQL 在读取表信息,为了给您提供自动补全提示(方便您敲表名、列名),不影响操作。
      • Database changed:明确告诉您,已经成功切换到 mysql 库了。

    6. 再次查看当前数据库(验证切换成功)

    命令: mysql> select database();

    • 作用:再次确认当前所在的数据库。
    • 结果解读(1 row in set): 输出 mysql。这就证明您刚才的 use mysql; 生效了,您现在位于 mysql 系统库中。

    image-20261013005134094


    MySQL多实例配置

    多实例

    • 多套后台进程+线程+内存结构
    • 多个配置文件
      • 多端口
      • 多socket文件
      • 多个日志文件
      • 多个server_id
    • 多套数据

    实战配置

    #创建数据目录
    mkdir -p /data/330{7..9}
    #创建配置文件
    touch /data/330{7..9}/my.cnf
    touch /data/330{7..9}/mysql.log
    #编辑3307配置文件
    vim /data/3307/my.cnf
    [mysqld]
    basedir=/application/mysql
    datadir=/data/3307/data
    socket=/data/3307/mysql.sock
    log_error=/data/3307/mysql.log
    log-bin=/data/3307/mysql-bin
    server_id=7
    port=3307
    [client]
    socket=/data/3307/mysql.sock
    #编辑3308配置文件
    vim /data/3308/my.cnf
    [mysqld]
    basedir=/application/mysql
    datadir=/data/3308/data
    socket=/data/3308/mysql.sock
    log_error=/data/3308/mysql.log
    log-bin=/data/3308/mysql-bin
    server_id=8
    port=3308
    [client]
    socket=/data/3308/mysql.sock
    #编辑3309配置文件
    vim /data/3309/my.cnf
    [mysqld]
    basedir=/application/mysql
    datadir=/data/3309/data
    socket=/data/3309/mysql.sock
    log_error=/data/3309/mysql.log
    log-bin=/data/3309/mysql-bin
    server_id=9
    port=3309
    [client]
    socket=/data/3309/mysql.sock
    #修改目录权限
    chown -R mysql.mysql /data/330*
    
    #初始化3307数据,每条命令需要间隔若干秒,否则失败
    /application/mysql/scripts/mysql_install_db 
    --user=mysql 
    --defaults-file=/data/3307/my.cnf 
    --basedir=/application/mysql --datadir=/data/3307/data
    #初始化3308数据,每条命令需要间隔若干秒,否则失败
    /application/mysql/scripts/mysql_install_db 
    --user=mysql 
    --defaults-file=/data/3308/my.cnf 
    --basedir=/application/mysql --datadir=/data/3308/data
    #初始化3309数据,每条命令需要间隔若干秒,否则失败
    /application/mysql/scripts/mysql_install_db 
    --user=mysql 
    --defaults-file=/data/3309/my.cnf 
    --basedir=/application/mysql --datadir=/data/3309/data
    
    #启动多实例
    mysqld_safe --defaults-file=/data/3307/my.cnf &
    mysqld_safe --defaults-file=/data/3308/my.cnf &
    mysqld_safe --defaults-file=/data/3309/my.cnf &
    # 进入单独的mysql实例
    mysql -S /data/3307/mysql.sock -uroot
    #设置每个实例的密码
    mysqladmin -S /data/3307/mysql.sock -uroot password '123456'
    #查看server_id
    mysql -S /data/3307/mysql.sock -e "show variables like 'server_id'"  -p123456
    mysql -S /data/3308/mysql.sock -e "show variables like 'server_id'"  -p123456
    mysql -S /data/3309/mysql.sock -e "show variables like 'server_id'"  -p123456
    # 关闭实例,如果设置了密码登录就需要密码了 用参数-p
    mysqladmin -S /data/3307/mysql.sock -uroot  -p123456 shutdown
    mysqladmin -S /data/3308/mysql.sock -uroot  -p123456 shutdown
    mysqladmin -S /data/3309/mysql.sock -uroot  -p123456 shutdown

    准备工作

    1. 编辑配置文件(核心部分) 使用 vim /data/3307/my.cnf 分别修改三个配置文件。这里面的参数是确保多实例互不干扰的关键:

    • basedir=/application/mysql:MySQL 的安装目录(三个实例共用同一个程序文件,节省空间)。
    • datadir=/data/3307/data:数据目录。这是每个实例独立的,数据绝不能混在一起。
    • socket=/data/3307/mysql.sock:套接字文件。本地连接(比如 mysql -uroot)默认走 socket,如果不区分开,连接会串门。
    • log_error=/data/3307/mysql.log:错误日志路径,独立存放方便排错。
    • log-bin=/data/3307/mysql-bin:开启二进制日志(用于主从复制或数据恢复),前缀独立。
    • server_id=7:服务器唯一 ID。在做主从复制时,每个节点的 ID 必须不同。
    • port=3307:监听端口。必须不同(3307/3308/3309)。
    • [client] socket=...:这个很重要。它告诉本机的MySQL 客户端,连接这个实例时应该去哪个 socket 文件找它。

    image-20261013215222286

    2. 修改目录权限

    bash

    chown -R mysql.mysql /data/330*
    • 解释:将刚才创建的目录和文件的所有者改为 mysql 用户。这一步极其重要! 因为后续启动 MySQL 时是用 mysql 用户的身份运行的。如果权限不对,MySQL 无法写入数据或日志,直接启动失败。

    3. 初始化数据

    bash

    /application/mysql/scripts/mysql_install_db 
    --user=mysql 
    --defaults-file=/data/3307/my.cnf 
    --basedir=/application/mysql --datadir=/data/3307/data
    • 解释:这是数据库奠基仪式。
      • 作用:它会去读取你写的 my.cnf,然后在 datadir 目录下生成 MySQL的基础系统库(比如 mysql 库、performance_schema 库等)。没有这一步,启动数据库会报错说找不到系统表。
      • 教程特意标注:#初始化3307数据,每条命令需要间隔若干秒,否则失败。
      • 为什么必须间隔? 当你连续敲这三条命令时,系统资源(CPU、磁盘 IO)可能还没释放完,或者是前一个临时进程的锁还没释放。强行连续执行会导致后一个卡死或报错。必须等前一个 mysql_install_db 命令完全执行完(回到命令提示符)之后,再执行下一个。
      • 你的错误复盘:上一张图,是在 3307 的 mysqld 已经启动(占用端口和资源)的情况下,又去执行 mysql_install_db,这违背了“先初始化,再启动”的铁律,所以卡死了。

    4.启动多实例

    bash

    mysqld_safe --defaults-file=/data/3307/my.cnf &
    mysqld_safe --defaults-file=/data/3308/my.cnf &
    mysqld_safe --defaults-file=/data/3309/my.cnf &
    • 解释:初始化完成后,终于可以启动了。
    • 使用 mysqld_safe 启动(这是官方推荐的启动方式,它会监控 mysqld 进程,如果异常挂掉会自动重启)。
    • 加上 & 让它后台运行,否则你的终端会被第一个启动的实例卡住。

    5. 登录并设置密码

    bash

    mysql -S /data/3307/mysql.sock -uroot
    • 解释:初始化后,root 用户默认没有密码。
    • 注意这里的 -S /data/3307/mysql.sock。因为你有三个实例,你直接敲 mysql -uroot,系统不知道该连哪个。必须通过指定 -S (socket) 参数,明确告诉客户端你要连 3307 这个实例。

    bash

    mysqladmin -S /data/3307/mysql.sock -uroot password '123456'
    • 解释:给 3307 实例的 root 用户设置密码为 123456。

    image-20261013220557546

    6. 验证实例(查看 server_id)

    bash

    mysql -S /data/3307/mysql.sock -e "show variables like 'server_id'" -p123456
    mysql -S /data/3308/mysql.sock -e "show variables like 'server_id'" -p123456
    mysql -S /data/3309/mysql.sock -e "show variables like 'server_id'" -p123456
    • 解释:分别连接三个实例,查看它们各自的 server_id。
    • 如果 3307 返回 7,3308 返回 8,3309 返回 9,说明你的多实例部署完全成功,它们各自独立工作了。

    image-20261013221151781

    正确的梳理流程应是:

    1. 创建目录和配置文件。
    2. 赋予 mysql 用户权限。
    3. 依次、逐个执行 `mysql_install_db` 初始化(务必等前一个结束)。
    4. 分别使用 `mysqld_safe` 启动服务。
    5. 通过 `-S` 指定 socket 进行单独管理。

    mysqladmin

    • “强制回应 (Ping)”服务器
    • 关闭服务器
    • 创建和删除数据库
    • 显示服务器和版本信息
    • 显示或重置服务器状态变量
    • 设置口令
    • 重新刷新授权表
    • 刷新日志文件和高速缓存
    • 启动和停止复制
    [root@localhost ~]# mysqladmin -uroot -p123456 create hellodb
    [root@localhost ~]# mysqladmin -uroot -p123456 drop hellodb
    [root@localhost ~]# mysqladmin -uroot -p123456 ping 检查服务端状态的
    [root@localhost ~]# mysqladmin -uroot -p123456 status 服务器运行状态
    [root@localhost ~]# mysqladmin -uroot -p123456 status 服务器状态 --sleep 2 --count 10 每两秒钟显示
    一次服务器实时状态一共显示10次
    [root@localhost ~]# mysqladmin -uroot -p123456 extended-status 显示状态变量
    [root@localhost ~]# mysqladmin -uroot -p123456 variables 显示服务器变量
    [root@localhost ~]# mysqladmin -uroot -p123456 flush-privileges 数据库重读授权表,等同于reload
    [root@localhost ~]# mysqladmin -uroot -p123456 flush-tables 关闭所有已经打开的表
    [root@localhost ~]# mysqladmin -uroot -p123456 flush-threds 重置线程池缓存
    [root@localhost ~]# mysqladmin -uroot -p123456 flush-status 重置大多数服务器状态变量
    [root@localhost ~]# mysqladmin -uroot -p123456 flush-logs 日志滚动。主要实现二进制和中继日志滚动
    [root@localhost ~]# mysqladmin -uroot -p123456 flush-hosts 清楚主机内部信息
    [root@localhost ~]# mysqladmin -uroot -p123456 kill 杀死线程
    [root@localhost ~]# mysqladmin -uroot -p123456 refresh 相当于同时执行flush-hosts flush-logs
    [root@localhost ~]# mysqladmin -uroot -p123456 shutdown 关闭服务器进程
    [root@localhost ~]# mysqladmin -uroot -p123456 version 服务器版本以及当前状态信息
    [root@localhost ~]# mysqladmin -uroot -p123456 start-slave 启动复制,启动从服务器复制线程
    [root@localhost ~]# mysqladmin -uroot -p123456 stop-slave 关闭复制线程

    mysqldump

    • 备份数据库和表的内容
    # 备份所有数据库
    mysqldump -uroot -p123456  --all-databases > /backup/mysqldump/all.db
                               -A > /backup/mysqldump/all.db 
    # 备份指定数据库
    mysqldump -uroot -p123456 wordpress > /backup/mysqldump/wordpress.db
    # 备份指定数据库指定表(多个表以空格间隔)
    mysqldump -uroot -p123456 wordpress t1 > /backup/mysqldump/t1.db
    # 备份指定数据库排除某些表
    mysqldump -uroot -p123456  test --ignore-table=test.t1 --ignore-table=test.t2 > /backup/mysqldump/test2.db
    
    • 还原的方法
    # 注:在导入备份数据库前,db_name如果没有,是需要创建的; 而且与db_name.db中数据库名是一样的才可以导入
    mysqladmin -uroot -p123456 create db_name 
    mysql -uroot -p123456 db_name < /backup/mysqldump/db_name.db
    # source也可以还原数据库
    mysql > use db_name
    mysql > source /backup/mysqldump/db_name.db

    第一部分:备份数据库和表的内容

    核心工具是 mysqldump(MySQL 自带的逻辑备份工具)。它的基本公式是: mysqldump [连接参数] [要备份的对象] > [导出的文件路径] (注:> 是 Linux 的重定向符号,意思是不在屏幕上打印,而是把结果写入右边的文件里)。

    1. 备份所有数据库

    mysqldump -uroot -p123456 --all-databases > /backup/mysqldump/all.db
                               -A > /backup/mysqldump/all.db
    • --all-databases:关键参数,意思是把当前 MySQL 服务器里所有的数据库(包括系统库 mysql、information_schema 等)全部导出。
    • > /backup/mysqldump/all.db:把导出的 SQL 语句保存到 all.db 这个文件中。
    • 适用场景:服务器整体迁移、全量冷备份。

    2. 备份指定数据库

    mysqldump -uroot -p123456 wordpress > /backup/mysqldump/wordpress.db
    • wordpress:写在连接参数后面,表示只备份名为 wordpress 的这个数据库.
    • 适用场景:只关心某一个业务库,不想备份其他无关的库。

    3. 备份指定数据库指定表(多个表以空格间隔)

    mysqldump -uroot -p123456 wordpress t1 > /backup/mysqldump/t1.db
    • 这里的语法是:mysqldump [连接参数] [数据库名] [表1] [表2] ...
    • wordpress:这是数据库名。(注意:这里的 wordpress 是数据库名,不是用户名,指的是MySQL自带的系统数据库)。
    • t1 和 t2:这是 wordpress 库里的两张表的名字。它们之间用空格隔开。
    • 作用:只备份 wordpress 库里的 t1 表和 t2 表,其他表不管。
    • 适用场景:误删了某几张核心表,只需要针对这几张表做单独备份。

    4. 备份指定数据库排除某些表

    bash

    mysqldump -uroot -p123456 wordpress --ignore-table=wordpress.t1 --ignore-table=wordpress.t2 > /backup/mysqldump/wordpress_t.db
    • wordpress:备份 wordpress 数据库。
    • --ignore-table=wordpress.t1:关键参数,意思是“忽略(不备份)wordpress 库里的 t1 表”。
    • --ignore-table=wordpress.t2:同上,忽略 t2 表。
    • 注意语法:写 --ignore-table 时,必须带上数据库名.表名(如 wordpress.t1)。
    • 适用场景:数据库里有一张表特别大(比如几千万条日志数据),每次备份都很慢,就可以用这个命令把它排除掉,只备份其他重要的表。

    第二部分:还原的方法

    还原的本质就是把你之前备份出来的 .db(其实里面就是一堆SQL语句)重新执行一遍。图里给出了两种主流方式。

    方式一:在 Linux 终端直接导入(不进入 MySQL 交互界面)

    步骤 1:先创建目标数据库(如果不存在)

    bash

    mysqladmin -uroot -p123456 create db_name
    • mysqladmin:这是MySQL 的管理工具。
    • create db_name:创建一个名为 db_name 的空数据库。
    • 重点:备份文件里通常只包含“建表”和“插入数据”的语句,不包含“建库”语句(除非你全库备份时带了 --databases 参数)。所以恢复前,你得先准备好一个空的“房子”。

    步骤 2:导入数据

    bash

    mysql -uroot -p123456 db_name < /backup/mysqldump/db_name.db
    • 这里的 < 是输入重定向符号。意思是把右边的 .db 文件内容,作为输入喂给左边的 mysql 命令。
    • db_name:指定要导入到哪个数据库里。
    • 注意:这是一种“盲导”,如果过程中有报错,终端可能一闪而过,你很难察觉。

    方式二:进入MySQL 交互界面导入

    步骤 1:登录 MySQL 并选中数据库

    sql

    mysql -uroot -p123456   # 先登录进 MySQL
    mysql> use db_name;     # 选中你的目标数据库

    步骤 2:使用 source 命令导入

    sql

    mysql> source /backup/mysqldump/db_name.db
    • source 是MySQL 客户端自带的一个命令。
    • 优点:当文件很大时,用 source 导入,如果有 SQL语法错误或者主键冲突,终端会明显地把报错信息打印出来,方便你排错。

    image-20261014005150210

    image-20261014005236115


    1. 备份指定库(test.db):导出文件里通常没有 CREATE DATABASE test; 和 USE test;。它仅仅是导出了一堆表和数据的 SQL语句。

    image-20261014005545238

    1. 备份全库(all.db):导出文件里包含 CREATE DATABASE mysql; 和 USE mysql; 等语句。

    image-20261014005822926

    ==总结==:

    最稳妥的方式就是用同名数据库进行恢复。

  • MySQL介绍

    image-20261016133620807

    一、 DBA是什么?

    DBA 是 Database Administrator 的缩写,中文叫 数据库管理员。

    简单来说,DBA 就是企业里专门负责“照顾”数据库的人。如果把数据库比作一个巨大的仓库,DBA 既是仓库的建造者,也是保安,还是维修工和搬运工。他们的核心目标是保证数据库稳定、安全、高效地运行,确保数据不丢失、不泄露,且能被快速存取。

    二. 二进制安装

    当前运维和开发最常见的做法是二进制安装MySQL

    • 下载二进制包并且解压
    wget https://downloads.mysql.com/archives/get/p/23/file/mysql-5.6.40-linux-glibc2.12-x86_64.tar.gz
    tar xzvf mysql-5.6.40-linux-glibc2.12-x86_64.tar.gz
    • 然后的步骤就和编译安装一样
    mkdir /application
    mv mysql-5.6.40-linux-glibc2.12-x86_64 /application/mysql-5.6.40
    ln -s /application/mysql-5.6.40 /application/mysql #为了后续写的脚本,方便监控mysql
    cd /application/mysql/support-files    #该文件夹有mysql初始化(预设)配置文件,覆盖文件是因为注释更全。
    cp my-default.cnf /etc/my.cnf
    
    mkdir /etc/init.d
    cp mysql.server /etc/init.d/mysqld    # mysql.server包含如何启动mysql的脚本命令,让系统知道通过该命令启动mysql时的动作,该目录存放系统中各种服务的启动/停止脚本
    cd /application/mysql/scripts
    useradd mysql -s /sbin/nologin -M
     yum -y install autoconf    #不然会报错
    ./mysql_install_db --user=mysql --basedir=/application/mysql --data=/application/mysql/data
    #  这个执行完后会出现   /application/mysql/bin/mysqld: error while loading shared libraries: libcrypt.so.1: cannot open shared object file: No such file or directory   错误
    
    MySQL 启动时找不到名为 libcrypt.so.1 的系统依赖库。
    安装缺失的库(推荐):
    尝试安装 libxcrypt-compat 包,它提供了对旧版 libcrypt.so.1 的兼容支持。
    
    yum install libxcrypt-compat
    
    # 安装 Perl 核心模块包(包含 Sys::Hostname)
    yum install -y perl-core
    
    
    vim /etc/profile.d/mysql.sh    #写到环境变量的子配置文件内,未修改前只能使用/bin/mysql的命令才能启用,而不能全局使用
    export PATH="/application/mysql/bin:$PATH"
    
    source /etc/profile    #否则要重启系统才生效
    • ==需要注意,官方编译的二进制包默认是在/usr/local目录下的,我们需要修改配置文件==
    sed -i 's#/usr/local#/application#g' /etc/init.d/mysqld /application/mysql/bin/mysqld_safe
    
    
    修正脚本路径:使用 sed 把官方默认写死的 /usr/local 批量替换为你实际安装的 /application。

    此时不可以通过systemctl命令启动,只能通过/etc/init.d/mysql start启动(Nginx也是,如果此时通过这样的命令启动Nginx,会导致systemctl start Nginx失败,因为冲突。)

    • 创建systemd管理文件,并且测试是否正常使用
    • 配置系统服务:编写 /usr/lib/systemd/system/mysqld.service 文件,以便用 systemctl 管理 MySQL。
    vim /usr/lib/systemd/system/mysqld.service
    [Unit]
    Description=MySQL Server
    Documentation=man:mysqld(8)
    Documentation=https://dev.mysql.com/doc/refman/en/using-systemd.html
    After=network.target
    After=syslog.target
    [Install]
    WantedBy=multi-user.target
    [Service]
    User=mysql
    Group=mysql
    ExecStart=/application/mysql/bin/mysqld --defaults-file=/etc/my.cnf    #强制从my.cnf读配置,不然会从多个路径读配置
    LimitNOFILE = 5000
    server_id = 1    #用作主从的时候生效
    
    ## 这个配置文件里面的# 注释得删掉,出现中文,冲突了
    
    vim /etc/my.cnf
     basedir = /application/mysql/
     datadir = /application/mysql/data
    
    ln -s /usr/lib64/libncurses.so.6.2 /usr/lib64/libncurses.so.5
    ln -s /usr/lib64/libtinfo.so.6.2 /usr/lib64/libtinfo.so.5
    
    ls -l /usr/lib64/libncurses.so.5 /usr/lib64/libtinfo.so.5  #检验软链接是否建立成功
    setenforce 0  # 这个很重要啊
    systemctl daemon-reload
    systemctl start mysqld
    systemctl enable mysqld
    chmod -R +x /application/mysql/bin/
    # 此时ss -ntl 可以看到3306端口
    ss -ntl
    
    mysqladmin -uroot password '123456'
    mysql -uroot -p123456
    # 安装 EPEL 源
    dnf install -y epel-release
    # 再次尝试安装兼容库
    dnf install -y ncurses-compat-libs
    

    1. 运行身份不同(最核心的原因)

    • 你手动执行时: 你是用 root 身份敲下 /application/mysql/bin/mysqld ... 的。root 是 Linux 里的超级管理员,拥有最高权限,几乎不受任何限制,所以它可以畅通无阻地读取和执行任何文件。
    • 使用 systemctl 启动时: systemd 会去读取你的服务文件 mysqld.service,看到里面写了 User=mysql 和 Group=mysql。于是,systemd 会降权,让 mysql 这个普通用户去执行 /application/mysql/bin/mysqld。
    • 问题来了: 如果 /application 或 /application/mysql/bin/ 目录的权限设置得太严格(比如只允许 root 进入),普通的 mysql 用户连目录都进不去,更别提执行里面的文件了,所以系统直接报 Permission denied。

    总结一下

    • 手动能启动:是因为你用 root 强行越过了权限和 SELinux 的限制。
    • systemctl 报错:是因为 systemd 严格按照 mysql 用户的权限和 SELinux 规则来办事。
    • 后来怎么成功的:因为你关闭了 SELinux,并且可能(如果没执行的话,建议执行一下)赋予了 /application 目录 755 权限,让 mysql 用户能够顺利访问并执行程序。

    这也是为什么在生产环境中,老手通常会把 MySQL 装在默认路径(如 /usr/local/mysql)或者专门为它配置 SELinux 策略,就是为了避免这种让人头疼的权限问题。


    MySQL体系结构管理

    客户端与服务器模型

    image-20261008160243758

    1. 顶部示意图:C/S 架构与通信协议

    • 架构模式:图片最上方展示了 MySQL 是典型的 C/S(Client/Server,客户端/服务器) 结构。
      • 左侧方块是客户机进程(如 mysql 命令行工具),通过网络(TCP/IP)向服务器发送请求。
      • 右侧方块也是客户机进程(如 PHP 程序),通过应用程序接口(套接字)连接。
    • 通信协议:
      • 示意图中特别标出了 TCP/IP 和 mysql 协议,以及 PHP 和 套接字。这说明 MySQL 支持多种连接方式,底层的传输层可以是网络协议,也可以是本地套接字。

    2. 核心知识点拆解

    A.==MySQL 自带客户端程序==

    文档指出 MySQL 安装后自带了一些位于 /application/mysql/bin 目录下的工具:

    • mysql:最常用的命令行客户端,用于交互式执行 SQL。
    • mysqladmin:用于执行管理操作(如关闭数据库、查看状态、修改密码等)。
    • mysqldump:用于数据库备份和导出数据的工具。

    B. mysqld 守护进程(服务器端)

    • 定义:mysqld 是 MySQL 的二进制程序,也是后台运行的守护进程(Daemon)。它是数据库的核心,负责处理所有数据请求。
    • 运行模式:
      • 单进程:在操作系统层面看,它是一个单一进程。
      • 多线程:但在进程内部,它是多线程运行的。这意味着它可以同时处理多个用户的并发请求(每个连接通常会对应一个线程)。

    C. 应用程序连接 MySQL 的方式

    这是图片中最详细的部分,讲解了两种主要的连接方式:

    方式一:TCP/IP 连接(远程连接)

    • 场景:当 PHP 应用和 MySQL 数据库不在同一台机器上时,必须使用 TCP/IP 方式。这也是最通用的远程连接方式。
    • 配置服务器(授权):
      • 为了让远程主机能连接,服务器端需要授权。
      • 命令:mysql> grant all privileges on *.* to root@'192.168.226.%' identified by '123456';
      • 解释:允许来自 192.168.226.x 网段的 root 用户,使用密码 123456 进行连接。
    • 配置客户端(安装与连接):
      • 安装:yum -y install mariadb(在 CentOS 等系统中,MariaDB 是 MySQL 的开源分支,通常作为客户端安装)。
      • 连接命令:mysql -uroot -p123456 -h192.168.226.X -P3306
        • -u:指定用户。
        • -p:指定密码。
        • -h:指定主机 IP(如果不加 -h,默认连本地)。
        • -P:指定端口(默认 3306)。

    方式二:Socket 连接(本地连接)

    • 定义:这是 MySQL 的默认连接方式(当 -h 参数为空或指定为 localhost 时)。

    • 原理:它不经过 TCP/IP 协议栈,而是通过本地的 Unix Socket 文件(图片中路径为 /application/mysql/tmp/mysql.sock)进行通信。

    • 特点:

      • 快速:无需建立 TCP 三次握手,直接通过文件系统通信,速度比 TCP 更快。
      • 本地:仅限本机连接,无法跨网络。
    • 验证区别:

      • 图片底部有一行小字提示:mysql -uroot -p123456 -h127.0.0.1。注意这里用的是 IP地址 127.0.0.1,而不是 localhost。
      • 关键点:在 MySQL 中,使用 -h localhost 通常走 Socket 连接;而使用 -h 127.0.0.1 则强制走 TCP/IP 连接。这常用于排查网络配置问题。

    总结

    这张图主要为了讲清楚 MySQL 的两个核心概念:

    1. 服务器端 (mysqld):是一个多线程的后台进程,负责干活。
    2. 连接方式:分为远程的 TCP/IP 和本地的 Socket。本地开发用 Socket 快,生产环境跨机器用 TCP/IP,且需要进行权限授权。

    image-20261008164445656

    image-20261008164243222

    3. 查找 Socket 文件

    • 命令:find / -name mysql.sock
    • 输出:
      • find: '/proc/30117': 没有那个文件或目录(这是一个无害的警告,通常是因为在 find 遍历过程中,某个进程刚好结束了,其 /proc 目录消失)。
      • /tmp/mysql.sock(这是关键结果!)
    • 原理:这对应了上面提到的“Socket,默认连接方式……通过 /application/mysql/tmp 下的 mysql.sock 文件”。在实际编译安装或配置中,这个 socket 文件的路径可能因配置文件(my.cnf)不同而有所变化。这里通过 find 命令确认了,当前服务器实际的 socket 文件位于 /tmp/mysql.sock。

    方式一:显式指定 Socket 文件进行连接

    • 命令:mysql -uroot -p123456 -S /tmp/mysql.sock
    • 参数解析:==-S 参数用于显式指定 Socket 文件的路径==。
    • 结果:成功登录 MySQL!Your MySQL connection id is 8。
    • 意义:这证明只要客户端找到了正确的 socket 文件,即使不走网络协议,也能快速连上数据库。这是本地管理数据库最常用的方式之一。

    方式二:通过 TCP/IP 强制本地回环连接

    • 命令:mysql -uroot -p123456 -h127.0.0.1
    • 参数解析:注意这里的 -h 指定了 127.0.0.1。
    • 结果:再次成功登录!Your MySQL connection id is 9(注意 ID 变成了 9,说明这是一次全新的独立连接)。
    • 原理与深度解析(极其重要):
      • 正如上面特别强调的:使用 -h localhost 通常走 Socket 连接;而使用 -h 127.0.0.1 则强制走 TCP/IP 连接。
      • 虽然是在本机,但 127.0.0.1 是一个 IP 地址。当客户端使用 -h 127.0.0.1 时,MySQL 客户端会将其视为网络请求,通过 TCP 协议封装数据包,经过操作系统的网络栈(回环接口 loopback),发往本机的 3306 端口。
      • 验证意义:这行命令验证了即使不通过外部网络,本机的 TCP/IP 网络栈、MySQL 对 3306 端口的监听以及权限配置都是完全正常的。

    image-20261008170946811

    Navicat 的作用和定义

    Navicat 是一款非常著名的图形化数据库管理工具(GUI 客户端)。

    之前的截图中,都是在 Linux 黑框框(命令行终端)里敲 SQL 语句,比如 grant all privileges...。这种方式虽然专业,但有几个缺点:

    1. 难记:需要记住复杂的 SQL 语法。
    2. 不直观:数据以文本形式展示,看几十条还行,看几万条就眼花。
    3. 易错:像您之前少打个分号 ; 就会报错。

    Navicat 的作用就是解决这些问题:

    • 可视化操作:通过==鼠标点击==就能建库、建表、改数据、删数据,不需要手敲 SQL。
    • 数据表格化:像看 Excel 一样查看数据库里的数据(就像截图中右侧那样)。
    • 多数据库支持:它不仅能连 MySQL,还能连 MariaDB、Oracle、SQL Server、PostgreSQL 等。
    • 远程连接利器:就像截图左上角显示的 server1 连接,您可以在 ==Windows 电脑上通过 Navicat 轻松连接到虚拟机里的 Linux MySQL 数据库==,不再需要一直在黑框框里操作。

    1. 表头字段(列)详解:

    • Host(主机):非常关键! 表示允许从哪台机器登录。
      • localhost:只允许本机(通过 Socket 或 127.0.0.1)登录。
      • 192.168.128.20:允许来自这个特定 IP 的远程机器登录。
      • 192.168.226.%:% 是通配符,表示允许 192.168.226.0 这个网段的所有机器登录。
    • User(用户):登录的用户名(如 root)。
    • Password(密码):这是加密后的密码。您看到的 *6BB4837EB7432Y... 这种乱码,是 MySQL 经过哈希算法加密后的密码(对应您命令行里输入的明文 123456)。
    • Select_priv, Insert_priv… 等(权限列):表示该用户有没有“查、增、改、删”的权限。Y 代表 Yes(有权限),N 代表 No(无权限)。
      • 看第一行 localhost / root,所有权限都是 Y,说明这是超级管理员。
      • 看第 6 行 localhost.localdomain / root,权限全是 N,这通常是系统自带的匿名或默认限制用户。
    • Grant_priv:表示该用户有没有“给其他人授权”的权限。
    • Shutdown_priv:表示该用户有没有“关闭数据库”的权限。

    2.远程连接实验对应操作

    • 第 4 行:Host: 192.168.128.20, User: root。权限全是 Y。

      • 这就是在命令行里执行 grant all privileges on *.* to root@'192.168.128.20'... 的结果。==MySQL 把这条授权记录直接写进了这张 user 表里==。
    • 第 5 行:Host: 192.168.226.20, User: root。权限全是 Y。

      • 这也是授权的结果。所以您的客户端(IP 192.168.226.20)现在有权限连服务器了。

    flush privileges刷新权限

    当您在命令行敲 grant... 语句时,MySQL 其实是在后台修改这张 user 表。 为了保证性能,MySQL 在启动时会把这张表的内容加载到内存中。之后,用户登录验证都是读内存。 当您修改了这张表(无论是通过 SQL 语句,还是直接在这个 Navicat 界面里双击修改),内存里的数据和表里的数据就不一致了。 此时执行 flush privileges;,就是告诉 MySQL:“重新读取一下这张表,把最新数据加载到内存里,立刻生效!”

    ==总结==:Navicat 让您直观地看到了 MySQL 内部长什么样。您之前敲的代码(授什么权、给谁授权),最终的结果都以数据的形式存放在这张 mysql.user 表里。

    在以后的实际工作中:

    • 写代码/搭建项目:用 Navicat 这类图形化工具。
    • 自动化脚本/服务器维护/应急排错:用命令行敲 SQL。两者结合使用效率最高

    image-20261008194421985

    image-20261008193813634

    status语句看连接方式

    image-20261008195128840

    image-20261008201345874

    第一视角:宏观映射(MySQL 逻辑 vs Linux 物理)

    文档开篇的表格,是理解数据库最直观的切入点。==MySQL 作为一个运行在操作系统上的软件,它的逻辑概念最终都要落地为 Linux 的文件==。

    MySQL 逻辑概念 Linux 物理概念 验证命令对比 核心含义
    库 (Database) 目录 (Directory) show databases; vs ls -l / 建一个库,底层就是建一个文件夹。
    表 (Table) 文件 (File) show tables; vs ls 建一张表,底层就是建一个或多个文件。
    二维表 文件 无 元数据(表结构) + 真实数据行 = 文件名(表名) + 文件内容(数据)。

    深度解析

    当您在 MySQL 中执行 CREATE DATABASE school; 时,MySQL 会在它的数据目录(通常是 /var/lib/mysql/)下创建一个名为 school 的文件夹。当您执行 USE school; CREATE TABLE student(...) 时,MySQL 会在 school 文件夹下创建对应的物理文件(比如 .ibd 文件或 .frm 文件)。

    第二视角:微观层级(InnoDB 存储引擎的物理结构)

    这是文档最核心、最具技术含量的部分。它解释了当您敲下 CREATE TABLE 后,数据到底是如何在磁盘上安放的。

    InnoDB 是 MySQL 默认的存储引擎,它遵循严格的“自上而下”的层级结构:

    数据表 (逻辑对象) → 表空间 (物理容器) → 段 (Segment) → 区 (Extent) → 页 (Page)

    我为您梳理这五个层级的关键知识点:

    1. 数据表 与 表空间 (Tablespace)

    • 逻辑与物理的转换点:表是逻辑的,表空间是物理的(对应磁盘上的文件,如 .ibd 文件)。
    • 作用:表空间(Tablespace)是存储表数据(段、区、页)的物理容器,用于==存储数据表的所有数据(包括段、区、页)==
    • 类型:InnoDB 默认使用独立表空间(每张表一个 .ibd 文件,便于管理)或共享表空间(所有表共用 ibdata1)。

    2. 表空间 与 段 (Segment)

    表空间由多个段组成,段是 “同类型数据的集合”。

    一个数据表至少包含 2 个核心段: 数据段(Data Segment):存储表中的实际数据行(对应 B + 树的叶子节点)。 索引段(Index Segment):存储表的索引数据(对应 B + 树的非叶子节点)。 若表有多个索引(如主键索引、二级索引),则每个索引会对应独立的索引段。 分区表中,每个分区会单独生成一套数据段和索引段,实现分区隔离。

    • 概念:表空间由多个“段”组成。段是“同类型数据”的集合。
    • 最重要的两个段:
      • 数据段:存您的真实数据行(比如用户的姓名、年龄)。
      • 索引段:存您的索引数据(比如主键索引的 B+ 树结构)。
    • 引申:如果您给表建了多个索引,就会有多个索引段。

    3. 段 与 区 (Extent)

    • 概念:区是空间分配的基本单位。段申请空间时,不会一页一页地要,而是一次要一个“区”。
    • 固定大小:一个区默认固定为 1MB(包含 64 个 16KB 的页)。
    • 为什么要有区?:段扩展时,==会以 “区” 为单位向表空间申请空间==,避免频繁分配小单位导致的碎片。

    4. 区 与 页 (Page)

    • 概念:页是 InnoDB 读写磁盘的最小 IO 单位。
    • 固定大小:默认 16KB。大小可配置(默认 16KB,可选 4KB、8KB、32KB、64KB),由innodb_page_size参数指定。
    • 核心运作机制(极其重要):
      • 即使您只想修改表里的一行数据(比如把名字“张三”改成“李四”),InnoDB 也不会只去读那一行,而是会把包含这一行的整个 16KB 的“页”从磁盘加载到内存中。
      • 修改完成后,再把整个页写回磁盘。这就解释了为什么数据库的内存(Buffer Pool)管理如此重要

    第三视角:生动示例与总结

    物理存储示例

    以 user(id, name, age) 为例:

    1. CREATE TABLE user(...) 后:生成 user.ibd 文件(表空间),并自动生成 2 个段,==创建了数据段和索引段==。

    2. 插入第一行数据时:

    3. 数据段先申请 1 个区(1MB,含 64 个 16KB 页),将数据写入其中一个页。 索引段同步申请区和页,存储索引信息。

      • 数据段去申请 1 个区(1MB)。
      • 这 1MB 里有 64 个页(16KB/个)。
      • 数据被写入其中一个页里。
      • 索引段同理,申请区并存储 B+ 树节点的索引信息。
    4. 随着数据变多,一个区不够用了,段就会继续申请下一个区。

    5. 关键特点总结(帮助记忆)

      层级 作用 大小 / 特性
      数据表 逻辑对象,定义数据结构和关系 无固定大小,由数据量决定
      表空间 物理文件容器,存储表的所有数据 可动态增长(如 .ibd 文件)
      段 按数据类型分组(数据 / 索引) 由多个区组成,大小动态扩展
      区 段的分配单位,由连续页组成 固定 1MB(含 64 个 16KB 页)
      页 最小 IO 单位,存储实际数据或索引 固定大小(默认 16KB),是读写基本单位

    InnoDB 引擎内部讲解

    要理解这两个段,必须先知道 InnoDB 存储数据的核心数据结构:B+ 树。 在 InnoDB 中,一张表其实就是一棵或多棵 B+ 树。B+ 树由两部分组成:非叶子节点(负责指路) 和 叶子节点(负责存真实数据)。

    “段(Segment)”是逻辑上的概念,它是同一种性质数据的集合。InnoDB 为了管理方便,把 B+ 树的“非叶子节点”和“叶子节点”分开存放,这就诞生了“索引段”和“数据段”。

    一、 数据段(Data Segment)

    1. 里面装的是什么?

    • 它专门用来存放 B+ 树的叶子节点。
    • 核心内容:表的真实数据行(比如 id=1, name='张三', age=18)。
    • 注意:如果您建表时设置了主键(InnoDB 强烈建议设置主键),那么数据段里的数据是按照主键顺序物理排序的。这就是所谓的“聚集索引”。

    2. 它的物理特性:

    • 数据段在物理上是由多个区(Extent)组成的。
    • 当您插入第一行数据时,数据段会先申请 1 个区(默认 1MB,包含 64 个 16KB 的页)。数据被写入其中的某个页里。
    • 自动扩展:当这 1MB 写满了,数据段会自动去申请下一个区。这个扩展过程是 InnoDB 底层自动完成的,操作数据库的人完全无感知。

    二、 索引段(Index Segment)

    1. 里面装的是什么?

    • 它专门用来存放 B+ 树的非叶子节点(包括根节点和中间节点)。
    • 核心内容:它不存真实的整行数据,它只存“路标”。
    • 路标长什么样? 比如:1~100 页的数据,主键范围是 1 到 500;101~200 页的数据,主键范围是 501 到 1000。

    2. 为什么需要独立的索引段?

    • 因为“目录(非叶子节点)”和“正文(叶子节点)”的更新频率和性质不同。目录通常很小,且相对稳定;正文很大,且频繁修改。把它们分成两个独立的“段”,物理隔离开,InnoDB 就能更高效地管理磁盘空间,避免频繁分配小空间导致的碎片。

    3. 一个表有几个索引段?

    • 如果您只建了主键,那么主键这棵 B+ 树,就拥有 1 个数据段(存主键对应的真实数据) + 1 个索引段(存主键 B+ 树的目录)。
    • 如果您还给 name 字段建了二级索引,那么 name 也会生成一棵独立的 B+ 树。这棵 name 树也会有它自己的 1 个索引段(存 name 树的目录)和 1 个数据段(注意:这里的“数据段”存的不再是完整行数据,而是 name 和主键 id 的对应关系)。

    三、 总结对照表

    为了让您一目了然,我做一个极简的对比:

    维度 数据段 (Data Segment) 索引段 (Index Segment)
    在 B+ 树中的位置 叶子节点 (最底层的具体数据) 非叶子节点 (上面负责指路的节点)
    存放的内容 真实的整行数据 (或二级索引+主键) 索引的目录、路标、范围指针
    物理组成 由多个区(Extent)组成 由多个区(Extent)组成
    扩展方式 写满一个区,自动申请下一个区 树变高变宽时,自动申请下一个区
    对用户的可见性 完全不可见,底层自动管理 完全不可见,底层自动管理

    ==总结==

    实际上,数据(叶子)和索引目录(非叶子)在物理磁盘上是分开存放在不同的段里的。它们是一棵树的两个部分,InnoDB 通过指针把它们连在一起。您查数据时,先走索引段(目录)找到指针,再顺藤摸瓜去数据段(正文)取数据。


    流程图一:逻辑查询与 B+ 树结构(我们在用数据库时发生了什么)

    这个流程展示了当我们执行一条 SQL 去查询数据时,InnoDB 是如何通过索引段指路,最终在数据段拿到数据的。

    graph TD
        A["客户端发起查询 (SELECT * FROM user WHERE id=18)"] --> B("进入 InnoDB 存储引擎")
        B --> C{"查找主键 B+ 树"}
    
        C -->|第一步 走索引段| D["索引段 (Index Segment)"]
        D --> D1["非叶子节点 (根节点)"]
        D1 --> D2["非叶子节点 (中间节点)"]
        D2 -.->|记录着路标 找到目标页的指针| E("找到目标页的指针")
    
        E -->|第二步 走数据段| F["数据段 (Data Segment)"]
        F --> F1["叶子节点 (Page 页)"]
        F1 --> F2["取出真实的整行数据 (id=18, name=张三)"]
    
        F2 --> G["返回给客户端"]
    
        style D fill:#f9f,stroke:#333,stroke-width:2px
        style F fill:#bbf,stroke:#333,stroke-width:2px

    图解核心:

    • 索引段(粉色):只负责“指路”,告诉你数据在第几个页。它本身不包含“张三”这个具体名字。
    • 数据段(蓝色):负责“存货”,最终的真实数据(id=18, name=张三)都在这里。
    • 它们是 B+ 树上同一棵树的两个不同部分,物理上是分开放的。

    流程图二:物理存储层级(InnoDB 在磁盘上是怎么建房子的)

    这个流程展示了从建表到插入数据,底层物理空间是如何一步步分配的(对应文档里的“表 → 表空间 → 段 → 区 → 页”)。

    graph TD
        %% 第一阶段:创建表空间
        Start["执行 CREATE TABLE user"] --> Space["创建独立表空间 user.ibd 文件"]
    
        %% 第二阶段:划分段(左右分流)
        Space --> Seg1["划分出 数据段"]
        Space --> Seg2["划分出 索引段"]
    
        style Seg1 fill:#bbf,stroke:#333,stroke-width:2px
        style Seg2 fill:#f9f,stroke:#333,stroke-width:2px
    
        %% 第三阶段:段申请区(左右并行)
        Seg1 -->|插入第一行数据时| Ext1["申请 1 个区 Extent"]
        Seg2 -->|同步创建索引时| Ext2["申请 1 个区 Extent"]
    
        style Ext1 fill:#fff3cd,stroke:#ffc107,stroke-width:1px
        style Ext2 fill:#fff3cd,stroke:#ffc107,stroke-width:1px
    
        %% 第四阶段:区划分为页(独立分组)
        subgraph 区与页的物理构成
            direction TB
            Ext1 -->|固定 1MB| P1["包含 64 个页 Page"]
            Ext2 -->|固定 1MB| P2["包含 64 个页 Page"]
            P1 -->|每个页固定 16KB| DataBlock["存入真实的行数据"]
            P2 -->|每个页固定 16KB| IndexBlock["存入路标 目录"]
        end
    
        %% 第五阶段:数据写满后的自动扩展循环
        DataBlock --> Check{"数据写满了吗"}
        Check -->|没满| UseEmpty["继续使用区里空闲的页"]
        Check -->|满了| AutoExt["段自动申请下一个区 1MB"]
        AutoExt -.->|循环扩展| Ext1
    
        %% 为分组框设置颜色
        style 区与页的物理构成 fill:#f5f5f5,stroke:#ccc,stroke-width:1px

    图解核心:

    1. 建表时,底层会自动分好“地盘”(段)。
    2. 段不是直接要“页”,而是要“区”。因为一次要 1MB(64个页)比一次要 16KB 效率高得多,不容易产生磁盘碎片。
    3. 自动扩展:当 1MB 的区被 64 个页填满后,段会自动去申请下一个区。这个动作完全由 InnoDB 在底层默默完成。
  • 自己的wordpress博客搭建方案

    方案一:PicGo + Typora + 阿里云 OSS(写作端图床方案)

    这套方案的核心是写文章时的极致便利:在 Typora 里粘贴截图,图片自动上传到阿里云 OSS,并自动替换为公网链接。

    详细过程

    第一步:阿里云 OSS 基础配置

    这是所有方案的基础,需要先在云端准备好“仓库”。

    1. 开通 OSS 与创建 Bucket:登录阿里云,开通“对象存储 OSS”服务。进入控制台,创建一个 Bucket(存储空间)。地域选离你近的(如华东1-杭州),存储类型选标准存储,读写权限必须设为公共读,否则图片无法对外访问。
    2. 关键:创建 RAM 子账号:强烈建议不要直接用主账号的 AccessKey。去“访问控制 RAM”里创建一个专门用于图床的子用户,只授予其“AliyunOSSFullAccess”权限。创建后保存好生成的 AccessKey ID 和 AccessKey Secret,这比你主账号的密码安全得多。
    3. 记录 Bucket 信息:记下你的 Bucket 名称、地域节点(Endpoint,如 oss-cn-hangzhou.aliyuncs.com)以及 Bucket 的外网访问域名。

    第二步:PicGo 与 Typora 联动配置

    1. 安装并配置 PicGo:下载安装 PicGo,在“图床设置”中选“阿里云 OSS”。填入刚才准备的 AccessKey、Secret、Bucket 名和 Endpoint。关键一点:上传路径建议设置为 picgo/ 或 {year}/{month}/,方便日后在 OSS 里管理。
    2. 设置 Typora:打开 Typora 的“偏好设置” -> “图像”。在“插入图片时…”选择上传图像。然后在“上传服务设定”里选 PicGo (app),并填好 PicGo 的安装路径。
    3. 验证:在 Typora 里粘贴一张截图,如果配置正确,截图会瞬间变成类似 https://你的bucket.oss-cn-hangzhou.aliyuncs.com/picgo/xxx.png 的链接,且 PicGo 的相册里会显示上传记录。

    总结

    这套方案的最大优势在于写作效率。你不需要离开 Typora 去手动处理任何图片,粘贴即上传。图片的物理存储与博客服务器完全解耦。但要注意,它只解决了用 Typora 写 Markdown 笔记时的图片托管问题。如果你用 WordPress 的后台编辑器直接写文章,这套方案帮不上忙,因为 WordPress 不会自动调用 PicGo。


    第一步开通oss和创建oss

    image-20261007231217472

    image-20261007231540133

    ​

    PicGo与Typora联动配置

    image-20261007232316429

    image-20261007232758223

    image-20261007233039617

    image-20261007233355828


    方案二:WordPress 媒体库自管理(纯本地存储)

    详细过程

    第一步:确认媒体库基础设置

    1. 检查上传目录权限:WordPress 默认会把上传的图片存到 /wp-content/uploads/ 目录,并按“年/月”自动分文件夹。确保这个目录有写入权限(一般 755 即可,部分服务器需要 775)。
    2. 设置图片尺寸:在后台「设置」→「媒体」中,可以设定缩略图、中等、大图的尺寸。WordPress 在上传一张图时会自动生成多个尺寸的副本,方便不同位置调用。建议保持默认,或根据主题需要调整。
    3. 关闭不必要的自动生成(可选):如果服务器空间紧张,可以用插件或代码禁止生成某些用不到的尺寸,减少冗余文件。

    第二步:在文章中插入图片

    1. 两种上传入口:
      • 文章编辑页内:点击「+」→「图片」区块,直接上传或从媒体库选择。
      • 媒体库独立页:后台「媒体」→「添加新」,先集中上传,再在文章里调用。
    2. 插入后的操作:上传后,在区块设置里可以填「替代文本」(Alt,利于 SEO 和无障碍)、选择对齐方式、设置链接目标(通常选「媒体文件」或「无」)。
    3. 图片链接形式:此时图片地址是 https://你的域名/wp-content/uploads/2026/10/xxx.jpg,直接由你的服务器提供。

    第三步:媒体库的日常管理

    1. 查看与编辑:在「媒体」→「媒体库」中,可以网格或列表查看所有图片,点击可编辑标题、Alt、描述,甚至做简单的裁剪旋转。
    2. 删除与替换:删除图片时要注意,如果文章里还在引用,会出现裂图。WordPress 不会自动检查引用关系,需要手动确认。
    3. 附件页面:默认情况下,每张图片都有一个独立的“附件页面”(URL 类似 ?attachment_id=123)。如果不想暴露这些页面,可以在主题里禁用,或装插件重定向。

    第四步:备份与迁移(重点)

    因为图片存在自己服务器上,备份就格外重要:

    1. 备份内容:数据库(存图片的元数据和文章里的链接)+ /wp-content/uploads/ 整个目录。
    2. 迁移时:换服务器或换域名,必须同时迁移数据库和 uploads 目录,否则图片会丢失或链接失效。换域名后还需用工具(如 Better Search Replace 插件)批量替换数据库里的旧域名。
    3. CDN 加速(可选):如果以后觉得图片加载慢,可以给 wp-content/uploads/ 配一个 CDN,但这是后话,不影响当前流程。

    总结

    这套方案的本质是把图片完全交给 WordPress 和你的服务器管理,不依赖任何第三方云存储。

    优点:

    • 零额外配置:装好 WordPress 就能用,不需要申请 AccessKey、配插件、调权限。
    • 数据完全自主:图片就在自己服务器上,不担心第三方服务停服、欠费或政策变动。
    • 备份逻辑简单:网站备份 = 数据库 + uploads 目录,打包带走即可。
    • 与 WordPress 深度集成:媒体库、区块编辑器、主题缩略图、SEO 插件都能直接读取图片信息。

    缺点:

    • 占用服务器磁盘和带宽:图片越多,服务器压力越大,尤其是流量上来后,图片会拖慢网站。
    • 迁移稍麻烦:换域名时必须批量替换数据库里的旧链接,否则图片全裂。
    • 没有自动压缩/WebP 转换:需要额外装插件(如 Smush、Imagify)来优化图片体积。
    • 无 CDN 时加载速度受服务器地域限制:服务器在国内、读者在国外,或反过来,图片加载会慢。

    适用场景:

    • 博客刚起步,图片量不大,访问量不高。
    • 想先把博客跑起来,不想在存储上花太多精力。
    • 对数据自主性要求高,或预算上不想为 OSS 额外付费。

    和方案一的配合建议:

    • 写作用 Typora + PicGo + OSS:保证 Markdown 写作时插图无感、链接稳定。
    • 博客用 WordPress 媒体库:发布到博客时,如果图片已经在 OSS,可以直接粘贴 OSS 链接;如果图片是博客专属的(如封面、截图),就上传到媒体库。
    • 两者并不冲突,按图片的“用途”分流即可。

    image-20261007234313469

    image-20261007234820435

    image-20261007235344148

    image-20261007235609612

    image-20261008000003598