docs(note): 重组笔记目录结构并新增 MySQL JOIN 实验笔记
- 随手记笔记按主题迁移至 note/资源/(DevOps、数据库、编程) - Windows使用、go errors 等笔记迁移时同步更名为规范标题 - 新增 MySQL 空表 JOIN 导致排序索引失效实验笔记 - 移除 go操作excel 笔记
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
# MySQL 空表 JOIN 导致 ORDER BY 索引失效——实验笔记
|
||||
|
||||
> 环境:MySQL 8.0.18+(hash join 自 8.0.18 引入,实测 8.0.28)
|
||||
> 现象:一条 `ORDER BY ... LIMIT` 查询原本走主表复合索引(免排序),LEFT JOIN 一张**空表**后,执行计划退化为全量读取 + `Using temporary; Using filesort`,主表索引"失效"。
|
||||
|
||||
---
|
||||
|
||||
## 一、一句话结论
|
||||
|
||||
**不是空表禁用了别的表的索引,而是空表让优化器为该 JOIN 选择了 hash join;hash join 不保序,导致"借主表索引序完成 ORDER BY"的整个计划作废**,主表随之退化为全量读取 + 临时表排序。
|
||||
|
||||
---
|
||||
|
||||
## 二、原理
|
||||
|
||||
### 1. 为什么嵌套循环(Nested Loop Join)能"免排序"
|
||||
|
||||
```
|
||||
按 idx(created_at, id) 倒序逐行读主表 → 每行对被 join 表做索引点查(eq_ref)→ 输出
|
||||
```
|
||||
|
||||
- 嵌套循环的输出顺序 = 驱动表(主表)的访问顺序
|
||||
- 主表走 `(created_at, id)` 复合索引倒序扫描,输出天然满足 `ORDER BY created_at DESC, id DESC`
|
||||
- `LIMIT 10` 可以提前终止:读到 10 行即停,`ORDER BY` 零成本(EXPLAIN 中体现为 `type=index` + `Backward index scan`,无 temporary/filesort)
|
||||
|
||||
### 2. hash join 为什么会破坏这一切
|
||||
|
||||
- hash join 把被 join 表整棵读进内存哈希表,再用驱动表逐行**探测**,输出顺序不再对应任何一方的索引序
|
||||
- 一旦计划中任何一个 JOIN 环节是 hash join,结果集都必须整体重排
|
||||
- 优化器权衡:"反正要排序,主表走哪个访问路径都无所谓" → 放弃排序专用索引,选最便宜的读取方式(全表扫或全索引扫,见第四节变体)
|
||||
|
||||
### 3. 为什么偏偏是空表触发 hash join(反直觉)
|
||||
|
||||
- 空表没有统计信息,优化器估算行数 ≈ 1
|
||||
- 估算"把空表整棵读进哈希表(≈1 次 IO)再探测" 比 "对主表每行做一次 B+ 树点查(N 次树遍历)" 更便宜
|
||||
- 于是选了 hash join —— 对空表本身这是**正确的成本决策**(0 行数据怎么访问都对),副作用是拖垮了主表的排序计划
|
||||
- 表有了真实数据后,统计信息更新,优化器改回索引点查(eq_ref),保序计划自动恢复
|
||||
|
||||
---
|
||||
|
||||
## 三、建表与造数(可直接执行)
|
||||
|
||||
### 1. 主表:10 万行,带 `(created_at, id)` 复合索引
|
||||
|
||||
```sql
|
||||
DROP TABLE IF EXISTS users, user_profiles, user_tags_empty, user_tags_noindex;
|
||||
|
||||
CREATE TABLE users (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(64) NOT NULL,
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_created_id (created_at, id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- 递归 CTE 造数需要先放开深度限制(仅当前会话)
|
||||
SET SESSION cte_max_recursion_depth = 100001;
|
||||
|
||||
INSERT INTO users (username, nickname, created_at)
|
||||
WITH RECURSIVE seq AS (
|
||||
SELECT 1 AS n
|
||||
UNION ALL
|
||||
SELECT n + 1 FROM seq WHERE n < 100000
|
||||
)
|
||||
SELECT
|
||||
CONCAT('user_', n),
|
||||
CONCAT('nick_', n),
|
||||
DATE_ADD('2025-01-01 00:00:00', INTERVAL FLOOR(RAND() * 31536000) SECOND)
|
||||
FROM seq;
|
||||
```
|
||||
|
||||
### 2. 对照用被 join 表:有数据,join 列唯一索引
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_profiles (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
city VARCHAR(32) NOT NULL DEFAULT '',
|
||||
UNIQUE KEY uk_user_id (user_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO user_profiles (user_id, city)
|
||||
WITH RECURSIVE seq AS (
|
||||
SELECT 1 AS n
|
||||
UNION ALL
|
||||
SELECT n + 1 FROM seq WHERE n < 100000
|
||||
)
|
||||
SELECT n, CONCAT('city_', FLOOR(RAND() * 100)) FROM seq;
|
||||
```
|
||||
|
||||
### 3. "罪魁"空表:结构与 user_profiles 完全同构,只是**不插入任何数据**
|
||||
|
||||
```sql
|
||||
CREATE TABLE user_tags_empty (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
tag VARCHAR(32) NOT NULL DEFAULT '',
|
||||
UNIQUE KEY uk_user_id (user_id)
|
||||
) ENGINE=InnoDB;
|
||||
-- 故意不插数据
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、对照实验
|
||||
|
||||
> 每组实验观察四处:主表 users 的 `type` / `key` / `rows` / `Extra`,以及被 join 表的访问方式。
|
||||
> **注意**:实验 SQL 故意 SELECT 了 `username`/`nickname` 等**索引外列**,原因见实验二的"变体"说明。
|
||||
|
||||
### 实验一(对照组):只 JOIN 有数据的表 → 索引完美生效
|
||||
|
||||
```sql
|
||||
EXPLAIN
|
||||
SELECT u.id, u.username, u.nickname, u.created_at, p.city
|
||||
FROM users u
|
||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
||||
ORDER BY u.created_at DESC, u.id DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
**预期结果:**
|
||||
|
||||
| table | type | key | rows | Extra |
|
||||
|---|---|---|---|---|
|
||||
| u | **index** | **idx_created_id** | **10** | **Backward index scan** |
|
||||
| p | eq_ref | uk_user_id | 1 | (空) |
|
||||
|
||||
主表倒序走复合索引只读 10 行(LIMIT 提前终止),被 join 表逐行点查,无排序开销。保序计划下 SELECT 索引外列不影响走索引(逐行回表而已)。
|
||||
|
||||
### 实验二:仅追加一个 LEFT JOIN 空表 → 计划全面崩坏
|
||||
|
||||
```sql
|
||||
EXPLAIN
|
||||
SELECT u.id, u.username, u.nickname, u.created_at, p.city, t.tag
|
||||
FROM users u
|
||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
||||
LEFT JOIN user_tags_empty t ON t.user_id = u.id
|
||||
ORDER BY u.created_at DESC, u.id DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
**预期结果(SELECT 含索引外列时):**
|
||||
|
||||
| table | type | key | rows | Extra |
|
||||
|---|---|---|---|---|
|
||||
| u | **ALL** | NULL | **100003** | **Using temporary; Using filesort** |
|
||||
| p | eq_ref | uk_user_id | 1 | (空) |
|
||||
| t | **ALL** | NULL | 1 | **Using where; Using join buffer (hash join)** |
|
||||
|
||||
三个观察点:
|
||||
|
||||
1. 空表 t 的索引 `uk_user_id` 明明存在,却 `type=ALL` + hash join(空表上这是合理的成本选择)
|
||||
2. 主表 u 的 `idx_created_id` 被**整个放弃**:全表扫描 + 临时表 + 文件排序,rows 从 10 涨到全量(LIMIT 提前终止失效)
|
||||
3. 注意 p 的 eq_ref 没有受影响——**别的表的索引并没有被禁用,坏掉的是主表的"排序专用访问路径"**
|
||||
|
||||
**变体:主表退化的两种形态(重要,实测踩过的坑)**
|
||||
|
||||
如果 SELECT **只查索引内的列**(如仅 `u.id, u.created_at`),主表退化后不是 `ALL`,而是:
|
||||
|
||||
```
|
||||
u: type=index, key=idx_created_id, rows=100003, Extra: Using index; Using temporary; Using filesort
|
||||
```
|
||||
|
||||
机制:排序计划作废后优化器选"最便宜的读取方式"——查询列全在索引里时,扫**窄的二级索引**(覆盖扫描,`Using index` 免回表)比扫**宽的聚簇索引**(ALL)更便宜。`type=index` 全索引扫描与 `ALL` 全表扫本质相同:**都是全量读取 + 显式排序**,同样没有 LIMIT 提前终止。
|
||||
|
||||
判断"是否退化"的可靠标志是 `rows` 是否涨到全量、`Using temporary; Using filesort` 是否出现,而**不是** type 显示 ALL 还是 index。生产查询通常 SELECT 大量业务列(覆盖不成立),多表现为 ALL 形态;本实验因此故意带上索引外列。
|
||||
|
||||
> 加 `FORMAT=TREE` 可以更直观看到 `Left (hash join)` 节点:
|
||||
> `EXPLAIN FORMAT=TREE <实验二的查询>;`
|
||||
|
||||
### 实验三:FORCE INDEX 把空表钉回点查 → 计划恢复
|
||||
|
||||
```sql
|
||||
EXPLAIN
|
||||
SELECT u.id, u.username, u.nickname, u.created_at, p.city, t.tag
|
||||
FROM users u
|
||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
||||
LEFT JOIN user_tags_empty t FORCE INDEX (uk_user_id) ON t.user_id = u.id
|
||||
ORDER BY u.created_at DESC, u.id DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
**预期结果:** t 变为 `eq_ref`,主表 u 恢复 `index + Backward index scan, rows=10`,temporary/filesort 消失。
|
||||
|
||||
结论:只要所有 JOIN 都是保序的嵌套循环点查,主表就能借索引序免排序。
|
||||
|
||||
### 实验四:空表填入数据 + 刷新统计信息 → 自愈
|
||||
|
||||
```sql
|
||||
INSERT INTO user_tags_empty (user_id, tag)
|
||||
WITH RECURSIVE seq AS (
|
||||
SELECT 1 AS n
|
||||
UNION ALL
|
||||
SELECT n + 1 FROM seq WHERE n < 100000
|
||||
)
|
||||
SELECT n, 'vip' FROM seq;
|
||||
|
||||
ANALYZE TABLE user_tags_empty;
|
||||
|
||||
-- 重跑实验二的 EXPLAIN
|
||||
```
|
||||
|
||||
**预期结果:** 与实验一/三相同,t 走 `eq_ref`,主表走 `idx_created_id`,无需任何 hint。
|
||||
|
||||
补充观察:只插入少量行(比如 50 行)时优化器可能仍选 hash join——对几十行的表,hash join 确实可能更便宜,这属于**合理决策**而非问题;关注点应是"数据量与计划是否匹配",而不是"必须永远走索引"。
|
||||
|
||||
### 进阶实验五:与空无关——任何原因导致的 hash join 都会触发同样的退化
|
||||
|
||||
证明"根因是 hash join 破坏保序计划,空表只是诱因之一":
|
||||
|
||||
```sql
|
||||
-- 有 10 万行、但 join 列上【没有索引】的表
|
||||
CREATE TABLE user_tags_noindex (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT UNSIGNED NOT NULL,
|
||||
tag VARCHAR(32) NOT NULL DEFAULT ''
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO user_tags_noindex (user_id, tag)
|
||||
WITH RECURSIVE seq AS (
|
||||
SELECT 1 AS n
|
||||
UNION ALL
|
||||
SELECT n + 1 FROM seq WHERE n < 100000
|
||||
)
|
||||
SELECT n, 'new' FROM seq;
|
||||
|
||||
EXPLAIN
|
||||
SELECT u.id, u.username, u.nickname, u.created_at, p.city, t.tag
|
||||
FROM users u
|
||||
LEFT JOIN user_profiles p ON p.user_id = u.id
|
||||
LEFT JOIN user_tags_noindex t ON t.user_id = u.id
|
||||
ORDER BY u.created_at DESC, u.id DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
**预期结果:** 与实验二相同的崩坏形态(t 为 `ALL + hash join`,主表全量读取 + temporary + filesort)。
|
||||
|
||||
这说明日常更常见的场景——**join 列漏建索引**——和空表是同一个病:hash join 一出现,依赖主表索引序的 ORDER BY + LIMIT 计划就没了。
|
||||
|
||||
---
|
||||
|
||||
## 五、机制小结(对照表)
|
||||
|
||||
| JOIN 实现方式 | 输出是否保序 | LIMIT 能否提前终止 | 对主表排序索引的影响 |
|
||||
|---|---|---|---|
|
||||
| 嵌套循环 + eq_ref 点查 | ✅ 顺序=驱动表访问序 | ✅ 读够即停 | 可借索引序免排序 |
|
||||
| hash join | ❌ 哈希探测序 | ❌ 需物化后整体排序 | 排序计划作废,主表退化为全量读取 |
|
||||
|
||||
主表退化后的具体形态由 SELECT 列集合决定:
|
||||
|
||||
| SELECT 列 | 退化形态 | 特征 |
|
||||
|---|---|---|
|
||||
| 含索引外列(生产常态) | `type=ALL` 全表扫 | key=NULL |
|
||||
| 全在索引内 | `type=index` 全索引扫 | key=该索引,`Using index`(覆盖免回表) |
|
||||
|
||||
两种形态共同点:rows ≈ 全量、`Using temporary; Using filesort`、LIMIT 无法提前终止。
|
||||
|
||||
优化器的行为链条:
|
||||
|
||||
```
|
||||
被 join 表选了 hash join
|
||||
→ 结果集顺序不可保证
|
||||
→ ORDER BY 必须显式排序(temporary + filesort)
|
||||
→ "主表走排序专用索引"失去意义
|
||||
→ 主表改用最便宜的读取方式(ALL 或覆盖索引全扫)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、实践建议
|
||||
|
||||
1. **排查口诀**:`ORDER BY ... LIMIT` 查询变慢时,先看 EXPLAIN 里是否出现 `Using join buffer (hash join)`——它常常是主表索引"无故失效"的信号
|
||||
2. **判断退化看 rows 和 temporary/filesort**,不要只盯 type:`type=index` 也可能是全索引扫描的退化形态(对比正常时 rows 应接近 LIMIT 值)
|
||||
3. **根治**:给被 join 表的 join 列建索引(唯一列建唯一索引,非唯一建普通索引),让点查比 hash join 更便宜
|
||||
4. **空表/新表场景**(刚上线、还没数据的表):如果线上已出现该退化且影响明显,可用 `FORCE INDEX` 过渡;表有数据后可移除 hint(低频后台查询甚至可以不处理)
|
||||
5. **不要盲信"必须走索引"**:空表/极小表走全表扫本身是正确的成本决策,要修的是"hash join 破坏排序计划"这个副作用,而不是全表扫本身
|
||||
6. **改动 join 结构后**(新增关联表、调整关联条件)重新 EXPLAIN 一次带排序的查询,确认 `Backward index scan` / rows 接近 LIMIT 值的计划仍在
|
||||
|
||||
---
|
||||
|
||||
## 七、清理
|
||||
|
||||
```sql
|
||||
DROP TABLE IF EXISTS users, user_profiles, user_tags_empty, user_tags_noindex;
|
||||
SET SESSION cte_max_recursion_depth = DEFAULT;
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
## `errors.AsType`
|
||||
|
||||
> *用泛型把* `errors.As` *的“先声明变量再传指针”简化为“直接返回* `(E, bool)`*”,是* `errors.As` *的语法糖升级*
|
||||
|
||||
**使用场景:**
|
||||
|
||||
- 与 `errors.As` 完全相同(按类型提取错误),但写起来更简洁
|
||||
- 由于类型参数 `E` 必须是 `error` 接口的实现类型,**能在编译期避免 `errors.As` 那种“传错指针类型导致运行时 panic”的问题**
|
||||
|
||||
```go
|
||||
// 传统 errors.As:需要先声明变量
|
||||
var mysqlErr *mysql.MySQLError
|
||||
if errors.As(err, &mysqlErr) {
|
||||
return true, mysqlErr.Number
|
||||
}
|
||||
|
||||
// errors.AsType(Go 1.26+):无需预先声明变量
|
||||
if mysqlErr, ok := errors.AsType[*mysql.MySQLError](err); ok {
|
||||
return true, mysqlErr.Number
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
### excelize库
|
||||
|
||||
读写Excel的Go库
|
||||
|
||||
[github](https://github.com/qax-os/excelize)
|
||||
|
||||
## 基本用法
|
||||
|
||||
#### 创建工作簿
|
||||
|
||||
```go
|
||||
f := excelize.NewFile()
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
// 创建一个新的Sheet,返回的index为该工作簿的索引
|
||||
index, err := f.NewSheet("SheetName")
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建Sheet失败: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
#### 打开文件
|
||||
|
||||
```go
|
||||
f, err := excelize.OpenFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开 Excel失败: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
#### 写入cell值
|
||||
|
||||
```go
|
||||
f.SetCellValue("SheetName", "B2", 100)
|
||||
```
|
||||
|
||||
#### 设置工作簿默认活动工作表
|
||||
|
||||
```go
|
||||
f.SetActiveSheet(index)
|
||||
```
|
||||
|
||||
#### 创建或更新excel表
|
||||
|
||||
```go
|
||||
if err := f.SaveAs("Book1.xlsx"); err != nil {
|
||||
return fmt.Errorf("保存失败: %w", err)
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取指定cell值
|
||||
|
||||
```go
|
||||
cell, err := f.GetCellValue("Sheet1", "B2")
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取cell失败: %w", err)
|
||||
}
|
||||
fmt.Println(cell)
|
||||
```
|
||||
|
||||
#### 获取所有工作簿的cell值(二维数组)
|
||||
|
||||
```go
|
||||
rows, err := f.GetRows("Sheet1")
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取失败: %w", err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
for _, colCell := range row {
|
||||
fmt.Print(colCell, "\t")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# MySQL 字符集的终极巨坑 —— `utf8` vs `utf8mb4`
|
||||
|
||||
### 一句话总结
|
||||
|
||||
MySQL 中的 `utf8` 是个“假货”(最多只支持 3 字节),`utf8mb4` 才是真正的 UTF-8(支持 4 字节)。
|
||||
|
||||
### 为什么会有这个坑
|
||||
|
||||
- **历史遗留**:早期 MySQL 开发时,Unicode 字符确实只用 3 个字节就能存下。
|
||||
- **现象**:一旦用户输入 **Emoji 表情**(如 😀)或部分**生僻字**(如 𰻝),数据库会直接抛出错误:`Incorrect string value`。
|
||||
|
||||
### 📊 核心对比
|
||||
|
||||
| 字符集 | 真实身份 | 最大字节数 | 能否存 Emoji | 状态 |
|
||||
| :------------ | :----------------- | :--------- | :----------- | :--------- |
|
||||
| **`utf8`** | `utf8mb3` (阉割版) | 3 字节 | ❌ 报错 | ⚠️ 已废弃 |
|
||||
| **`utf8mb4`** | 真正的 UTF-8 | 4 字节 | ✅ 随便存 | ✅ 推荐使用 |
|
||||
|
||||
### 🛠️ 各版本现状与未来
|
||||
|
||||
- **MySQL 5.7 及以前**:默认字符集是 `latin1`,`utf8` 是重灾区。
|
||||
- **MySQL 8.0 及以后**:**默认字符集已改为 `utf8mb4`**(新项目安全系数提升)。
|
||||
- **官方态度**:目前 `utf8`(即 `utf8mb3`)已被标记为 Deprecated(废弃)。官方计划在**未来的某个大版本中彻底删除 `utf8mb3`**,并让 `utf8` 这个名字正式指向 `utf8mb4`。
|
||||
@@ -1,82 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
> 私钥永不出生(生成)于服务器,且永不离开客户端
|
||||
|
||||
|
||||
|
||||
### 客户端生成密钥对
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -C "mail"
|
||||
```
|
||||
|
||||
- `-t`:指定密钥算法。`ed25519` 在同等安全强度下密钥更短、性能更好、是当前最佳实践;`RSA 4096` 依然安全,且兼容性更广,如果是很老的系统(OpenSSH < 6.5),可能需要`RSA`
|
||||
- `-C`:添加注释,不影响加密、解密或登录认证的安全性,纯粹是一个给人看的备注字段
|
||||
|
||||
使用上述命令会生成一对密钥,通常位于`~/.ssh`目录下:公钥是`id_ed25519.pub`,私钥是`id_ed25519`。
|
||||
|
||||
### 上传公钥到目标服务器
|
||||
|
||||
方式1
|
||||
|
||||
```bash
|
||||
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@服务器IP
|
||||
```
|
||||
|
||||
方式2
|
||||
|
||||
```bash
|
||||
cd ~/.ssh
|
||||
echo "公钥内容" >> ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
chmod 700 ~/.ssh
|
||||
```
|
||||
|
||||
> 手动 echo 是通用方法,但更推荐 `ssh-copy-id`,它会自动处理目录创建、权限设置和追加
|
||||
|
||||
### 测试密钥登录
|
||||
|
||||
```bash
|
||||
ssh root@服务器IP
|
||||
```
|
||||
|
||||
- 如果直接登录成功,没有询问密码,说明密钥配置完全正确,继续下一步。
|
||||
- 如果仍然要求输密码,需要检查上一步的公钥是否粘贴完整,或 `authorized_keys` 权限是否为 `600`。
|
||||
|
||||
### 修改`sshd_config`
|
||||
|
||||
修改服务器的`/etc/ssh/sshd_config`文件,将下方这些行取消注释或者添加:
|
||||
|
||||
```
|
||||
PubkeyAuthentication yes # 开启密钥认证
|
||||
PasswordAuthentication no # 关闭密码认证
|
||||
PermitRootLogin prohibit-password # root账户禁用密码登录
|
||||
```
|
||||
|
||||
保存退出后,使用`sshd -t`检查配置文件语法是否正确
|
||||
|
||||
重启SSH服务
|
||||
|
||||
> SSH服务名因发行版而异:
|
||||
>
|
||||
> `Debian`/`Ubuntu`使用`systemctl restart ssh`
|
||||
>
|
||||
> ``RHEL`/`CentOS`/`Fedora` 上使用`systemctl restart sshd`
|
||||
|
||||
### 最终验证
|
||||
|
||||
1. 使用SSH发起连接后没有输入密码的提示,能直接连接服务器
|
||||
|
||||
```bash
|
||||
ssh root@服务器IP
|
||||
```
|
||||
|
||||
2. 临时删除私钥后再次登录会报错,大概如下
|
||||
|
||||
```
|
||||
root@服务器IP: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
|
||||
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# 正则表达式
|
||||
|
||||
正则表达式是一种用于匹配和操作文本的强大工具,用于描述要匹配的文本模式。
|
||||
|
||||
## 匹配字符
|
||||
|
||||
### 普通匹配字符
|
||||
|
||||
| 字符 | 描述 |
|
||||
| :------- | :----------------------------------------------------------- |
|
||||
| `[xyz]` | 匹配 `[xyz]` 中列出的所有字符。例如 `[xyz]`,能够匹配目标的所有 x、y、z 字符。 |
|
||||
| `[^xyz]` | 匹配 `[^xyz]` 中列出的字符外的所有字符。例如 `[^xyz]`,能够匹配目标的除去 x、y、z 外的所有字符。 |
|
||||
| `[A-Z]` | `[A-Z]` 表示一个区间,匹配所有大写字母;`[a-z]` 匹配所有小写字母;`[0-9]` 匹配所有数字。 |
|
||||
| `[\s\S]` | 匹配所有字符(包括换行符)。`\s` 匹配所有空白符(包括换行),`\S` 匹配所有非空白符(不包括换行),两者合并即可匹配任意字符。 |
|
||||
| `.` | 匹配除换行符 `\n`、`\r` 之外的任何单个字符,等价于 `[^\n\r]`。 |
|
||||
| `\w` | 匹配字母、数字、下划线,等价于 `[A-Za-z0-9_]`。 |
|
||||
| `\d` | 匹配任意一个阿拉伯数字(0 到 9),等价于 `[0-9]`。 |
|
||||
|
||||
### 非打印字符
|
||||
|
||||
| 字符 | 描述 |
|
||||
| :---- | :----------------------------------------------------------- |
|
||||
| `\cx` | 匹配由 x 指明的控制字符。例如,`\cM` 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一,否则将 c 视为一个原义的 'c' 字符。 |
|
||||
| `\f` | 匹配一个换页符,等价于 `\x0c` 和 `\cL`。 |
|
||||
| `\n` | 匹配一个换行符,等价于 `\x0a` 和 `\cJ`。 |
|
||||
| `\r` | 匹配一个回车符,等价于 `\x0d` 和 `\cM`。 |
|
||||
| `\s` | 匹配任何空白字符,包括空格、制表符、换页符等,等价于 `[ \f\n\r\t\v]`。注意 Unicode 正则表达式会匹配全角空格符。 |
|
||||
| `\S` | 匹配任何非空白字符,等价于 `[^ \f\n\r\t\v]`。 |
|
||||
| `\t` | 匹配一个制表符,等价于 `\x09` 和 `\cI`。 |
|
||||
| `\v` | 匹配一个垂直制表符,等价于 `\x0b` 和 `\cK`。 |
|
||||
|
||||
### 特殊字符
|
||||
|
||||
| 特殊字符 | 描述 |
|
||||
| :------- | :----------------------------------------------------------- |
|
||||
| `$` | 匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 `$` 也匹配 `\n` 或 `\r`。要匹配 `$` 字符本身,请使用 `\$`。 |
|
||||
| `( )` | 标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 `\(` 和 `\)`。 |
|
||||
| `*` | 匹配前面的子表达式零次或多次。要匹配 `*` 字符本身,请使用 `\*`。 |
|
||||
| `+` | 匹配前面的子表达式一次或多次。要匹配 `+` 字符本身,请使用 `\+`。 |
|
||||
| `.` | 匹配除换行符 `\n` 之外的任何单字符。要匹配 `.` 本身,请使用 `\.`。 |
|
||||
| `[` | 标记一个中括号表达式的开始。要匹配 `[`,请使用 `\[`。 |
|
||||
| `?` | 匹配前面的子表达式零次或一次,等价于 `{0,1}`。要匹配 `?` 字符本身,请使用 `\?`。例如,`do(es)?` 可以匹配 "do" 和 "does",但不能匹配 "dog"。 |
|
||||
| `\` | 将下一个字符标记为特殊字符、原义字符、向后引用或八进制转义符。例如,`\\` 匹配 `\`。 |
|
||||
| `^` | 匹配输入字符串的开始位置。在方括号表达式中使用时,表示不接受方括号内的字符集合(取反)。要匹配 `^` 字符本身,请使用 `\^`。 |
|
||||
| `{` | 标记限定符表达式的开始。要匹配 `{`,请使用 `\{`。 |
|
||||
| `|` | 指明两项之间的一个选择。要匹配 `|`,请使用 `\|`。 |
|
||||
|
||||
### 限定符
|
||||
|
||||
| 字符 | 描述 |
|
||||
| :------ | :----------------------------------------------------------- |
|
||||
| `{n}` | n 是一个非负整数,匹配确定的 n 次。例如,`o{2}` 不能匹配 "Bob" 中的 o,但是能匹配 "food" 中的两个 o。 |
|
||||
| `{n,}` | n 是一个非负整数,至少匹配 n 次。例如,`o{2,}` 不能匹配 "Bob" 中的 o,但能匹配 "foooood" 中的所有 o。`o{1,}` 等价于 `o+`,`o{0,}` 等价于 `o*`。 |
|
||||
| `{n,m}` | m 和 n 均为非负整数,其中 n <= m。最少匹配 n 次且最多匹配 m 次。例如,`o{1,3}` 将匹配 "fooooood" 中的前三个 o。`o{0,1}` 等价于 `o?`。 |
|
||||
|
||||
## 常用正则表达式
|
||||
|
||||
| 正则表达式 | 匹配内容 |
|
||||
| ------------------------------------------------ | ------------ |
|
||||
| `[\u4e00-\u9fa5]` | 中文字符 |
|
||||
| `^1[3-9]\d{9}$` | 国内手机号 |
|
||||
| `[1-9]\d{5}(?!\d)` | 国内邮政编码 |
|
||||
| `^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$` | 18位身份证号 |
|
||||
|
||||
Reference in New Issue
Block a user