前情回顾
在上一篇文章 星辰考古:TiDB v1.0 再回首 中,我们“回忆”了 TiDB 1.0 的内容,本文将介绍 TiDB 2.x 的相关内容。
从 TiDB 2.x 开始,正式引入 TiSpark 大数据组件,用于解决用户复杂的 OLAP 需求。
TiDB 2.x 整体架构图“升级”如下。
主要时间线
TiDB 2.x 系列在 2 年时间迭代了40个小版本,包含了大量的优化和改进,也是从这个大版本开始,商业案例逐渐增长,并且今日仍有客户的生产环境运行着 2.x 版本。
- 2018-04-27,TiDB 发布 2.0 GA 版。相比 1.0 版本,该版本对 MySQL 兼容性、系统稳定性、优化器和执行器做了很多改进。
- 2018-11-30,TiDB 发布 2.1 GA 版。相比 2.0 版本,该版本对系统稳定性、性能、兼容性、易用性做了大量改进。
- 2019-01-03,TiDB 发布 2.0.11 版。这是 2.0 系列的最后一个小版本。
- 2019-12-27,TiDB 发布 2.1.19 版。这是 2.1 系列的最后一个小版本。
TiDB 2.x 特性清单
下面摘录了一些 TiDB 2.x 的重要特性,更多内容还请参阅官方文档。
从 2.0.5 版本开始,TiDB 文档也有所改进,标记了每一项对应的 PR 链接,并按模块、新特性、优化改进、问题修复等进行分类,文档结构更加清晰。
TiDB
- 增加 tidb_config session 变量,输出当前 TiDB 配置 (批注:此时还没有 I_S.cluster_config 表)
- 添加 Session 变量 tidb_auto_analyze_ratio 控制统计信息自动更新阈值
- 支持只在一天中的某个时间段开启统计信息自动更新的功能 #7570
- 新增 update-stats 配置项,控制是否更新统计信息 #10772
- 支持通过配置文件设置单条 SQL 语句使用内存的大小,减少程序 OOM 风险
- 支持 Delete 语句中使用 USE INDEX 的语法
- 支持用科学计数法显示浮点数
- ADMIN SHOW DDL JOBS 输出更详细的 DDL 任务状态信息,输出信息中添加表名、库名等信息
- 支持 admin show ddl jobs 命令,支持输出 number 个 DDL jobs #7028
- 支持 ADMIN SHOW DDL JOB QUERIES 查询当前正在运行的 DDL 任务的原始语句
- 支持使用 admin show slow 语句来获取 SLOW QUERY LOG #7785
- 支持并行 DDL 任务执行 #6955
- 增加控制 DDL 并发度的选项 #7563
- 支持 ADMIN RECOVER INDEX 命令,用于灾难恢复情况下修复索引数据
- 完成 Admin Restore Table 功能方案设计 #7383
- 新增 ADMIN PLUGINS ENABLE/DISABLE SQL 语句,支持通过 SQL 动态开启/关闭 Plugin #11189
- 在日志中记录 ADD INDEX 执行过程中的慢操作,便于定位问题 #7083
- 支持 Add Index 语句与其他 DDL 语句并行执行,避免耗时的 Add Index 操作阻塞其他操作
- 添加统计 Add Index 操作进度的监控 #12389
- 支持 SET TRANSACTION 语法
- 支持 SHOW GRANTS FOR CURRENT_USER();
- 新增 SHOW TABLE REGIONS 的语句,支持通过 SQL 查询表的 Region 分布情况 #11238
- SHOW TABLE REGIONS 语法新增 WHERE 条件子句
MySQL [s1]> show table t regions where region_id = 2;
+-----------+-----------+---------+-----------+-----------------+-------+------------+
| REGION_ID | START_KEY | END_KEY | LEADER_ID | LEADER_STORE_ID | PEERS | SCATTERING |
+-----------+-----------+---------+-----------+-----------------+-------+------------+
| 2 | t_35_ | | 3 | 1 | 3 | 0 |
+-----------+-----------+---------+-----------+-----------------+-------+------------+
1 row in set (0.01 sec)
- 支持在 SHOW 语句中使用子查询,现在可以支持诸如 SHOW COLUMNS FROM tbl WHERE FIELDS IN (SELECT ‘a’) 的写法 #11461
MySQL [s1]> SHOW COLUMNS FROM t WHERE FIELD IN (SELECT 'id');
+-------+---------+------+------+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+---------+------+------+---------+-------+
| id | int(11) | YES | | NULL | |
+-------+---------+------+------+---------+-------+
1 row in set (0.00 sec)
- 支持 HTTP API 获取 TiDB 参数信息 (批注:非常实用的功能)
- 添加 HTTP API 打散 table 的 Regions 在 TiKV 集群中的分布
- 支持在线更改日志级别
- 在 information_schema 里添加 PROCESSLIST 表 #7286
MySQL [s1]> desc information_schema.processlist;
+---------+----------------------+------+------+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+----------------------+------+------+---------+-------+
| ID | bigint(21) UNSIGNED | YES | | NULL | |
| USER | varchar(16) | NO | | NULL | |
| HOST | varchar(64) | NO | | NULL | |
| DB | varchar(64) | YES | | NULL | |
| COMMAND | varchar(16) | NO | | NULL | |
| TIME | int(7) UNSIGNED | YES | | NULL | |
| STATE | varchar(7) | YES | | NULL | |
| INFO | binary(512) UNSIGNED | YES | | NULL | |
| MEM | bigint(21) UNSIGNED | YES | | NULL | |
+---------+----------------------+------+------+---------+-------+
9 rows in set (0.01 sec)
- 添加 auto_analyze_ratio 系统变量控制自动 analyze 的阈值
- 添加 tidb_retry_limit 系统变量控制事务自动重试的次数
- 增加一个系统变量 tidb_disable_txn_auto_retry ,用于关闭事务自动重试 #6877
- 支持 select tidb_is_ddl_owner() 语句,方便判断 TiDB 是否为 DDL Owner
- 增加变量 ddl_reorg_batch_size 来控制添加索引的速度 #8614
- 将缓存 100 个 Schema 变更相关的表信息调整成 1024 个,且支持通过 tidb_max_delta_schema_count 系统变量修改 #12515
- 将表的默认字符集和排序规则改为 utf8mb4 和 utf8mb4_bin #8590
MySQL [s1]> create table t (id int);
Query OK, 0 rows affected (0.13 sec)
MySQL [s1]> show create table t\G
*************************** 1. row ***************************
Table: t
Create Table: CREATE TABLE `t` (
`id` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
1 row in set (0.00 sec)
PD
- 支持手动 split Region,可用于处理单 Region 热点的问题
- 开启 Region merge 功能
- 支持批量 split Region
- 添加 Raft Learner 支持
- TSO 分配不再受系统时间回退影响
- PD 新增 config-check 选项,用于检查 PD 配置项是否合法 #1725
- pd-ctl 新增 remove-tombstone 命令,支持清理 tombstone store 记录 #1705
TiKV
- 限制接收 snapshot 时的内存使用,解决极端情况下的 OOM
- 可以配置 Coprocessor 在遇到 warnings 时的行为
- TiKV 支持导数据模式
- 支持 Region 从正中间分裂
- tikv-ctl 支持 compact 指定的 Region
- Raw KV 支持 Batch Put、Batch Get、Batch Delete 和 Batch Scan
- tikv-importer 作为独立的 binary 从 TiKV 中分离出来
- 空集群默认打开 dynamic-level-bytes 参数减少空间放大
说明:
关于 TiDB Ansible,TiSpark 等其他组件这里没有列出。
TiDB Ansible 项目已归档,如果你需要部署新版本的 TiDB,请使用强大的 TiDB 集群管理工具 TiUP。
在 TiDB 最新版本中,TiFlash 组件补齐了 HTAP 的重要一环,或许 TiSpark 已经可以退出历史舞台。
升级提示
- 由于新版本存储引擎更新,不支持在升级后回退至 2.0.x 或更旧版本
- 从 2.0.6 之前的版本升级到 2.1 之前,最好确认集群中是否存在正在运行中的 DDL 操作,特别是耗时的 Add Index 操作,等 DDL 操作完成后再执行升级操作
- 因为 2.1 版本启用了并行 DDL,对于早于 2.0.1 版本的集群,无法滚动升级到 2.1,可以选择下面两种方案:
- 停机升级,直接从早于 2.0.1 的 TiDB 版本升级到 2.1
- 先滚动升级到 2.0.1 或者之后的 2.0.x 版本,再滚动升级到 2.1 版本
- 从 2.1 开始,引入版本控制机制,支持集群滚动兼容升级
本系列文章是为 TiDB 版本升级材料包的一部分,如果你对 TiDB 社区升级互助活动感兴趣,欢迎点击下面的链接报名。
https://asktug.com/t/topic/1025499
使用 TiDB Ansible 部署 TiDB 集群
注:这里只为考古演示,并非生产环境部署步骤。
Ansible 是一个开源 IT 自动化引擎,可自动执行配置、配置管理、应用程序部署、编排和许多其他 IT 流程。它可免费使用,并且该项目受益于其数千名贡献者的经验和智慧。
TiDB Ansible 是 PingCAP 基于 Ansible playbook 功能编写的集群部署工具。它可以帮助你快速部署一个新的 TiDB 集群,其中包括 PD、TiDB、TiKV 和集群监控等组件。
准备环境
当前演示环境为 CentOS 7 操作系统,需要准备 Python 环境。
[root@centos7 ~]# hostnamectl
Static hostname: centos7.shawnyan.cn
...
Operating System: CentOS Linux 7 (Core)
CPE OS Name: cpe:/o:centos:centos:7
Kernel: Linux 3.10.0-1160.119.1.el7.x86_64
Architecture: x86-64
[root@centos7 ~]# python --version
Python 2.7.5
[root@centos7 ~]# python3 --version
Python 3.6.8
创建用户
创建 tidb
用户,配置 sudo 免密,并生成 ssh key。
[tidb@centos7 ~]$ id
uid=1001(tidb) gid=1001(tidb) groups=1001(tidb)
[tidb@centos7 ~]$ sudo uname -n
centos7.shawnyan.cn
下载对应版本的 TiDB Ansible 代码,安装依赖,使用 Ansible 命令部署机器互信和 sudo 规则。
[tidb@centos7 tidb-ansible]$ pwd
/home/tidb/tidb-ansible
[tidb@centos7 tidb-ansible]$ ansible --version
ansible 2.6.20
config file = /home/tidb/tidb-ansible/ansible.cfg
...
python version = 3.6.8 (default, Nov 14 2023, 16:29:52) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
[tidb@centos7 tidb-ansible]$ ansible-playbook -i hosts.ini create_users.yml -u root -k
SSH password:
...
Congrats! All goes well. :-)
下载安装包
执行 local_prepare 剧本,下载 v2.1.19 安装包。
[tidb@centos7 tidb-ansible]$ ansible-playbook local_prepare.yml
PLAY [do local preparation]
TASK [local : create downloads and resources directories]
TASK [local : create cert directory]
TASK [local : create packages.yml]
TASK [local : create specific deployment method packages.yml]
TASK [local : download tidb binary] ************************************************************************************************************************************************************************************************************
changed: [localhost] => (item={'name': 'tidb', 'version': 'v2.1.19', 'url': 'http://download.pingcap.org/tidb-v2.1.19-linux-amd64.tar.gz'})
TASK [local : download common binary] **********************************************************************************************************************************************************************************************************
changed: [localhost] => (item={'name': 'fio', 'version': 3.8, 'url': 'http://download.pingcap.org/fio-3.8.tar.gz', 'checksum': 'sha256:15739abde7e74b59ac59df57f129b14fc5cd59e1e2eca2ce37b41f8c289c3d58'})
changed: [localhost] => (item={'name': 'grafana_collector', 'version': 'latest', 'url': 'http://download.pingcap.org/grafana_collector-latest-linux-amd64.tar.gz'})
changed: [localhost] => (item={'name': 'kafka_exporter', 'version': '1.1.0', 'url': 'http://download.pingcap.org/kafka_exporter-1.1.0.linux-amd64.tar.gz'})
TASK [local : download diagnosis tools] ********************************************************************************************************************************************************************************************************
changed: [localhost] => (item={'name': 'tidb-insight', 'version': 'v0.2.5-1-g99b8fea', 'url': 'http://download.pingcap.org/tidb-insight-v0.2.5-1-g99b8fea.tar.gz'})
...
TASK [local : download other binary under gfw] *************************************************************************************************************************************************************************************************
changed: [localhost] => (item={'name': 'prometheus', 'version': '2.2.1', 'url': 'http://download.pingcap.org/prometheus-2.2.1.linux-amd64.tar.gz'})
changed: [localhost] => (item={'name': 'alertmanager', 'version': '0.14.0', 'url': 'http://download.pingcap.org/alertmanager-0.14.0.linux-amd64.tar.gz'})
changed: [localhost] => (item={'name': 'node_exporter', 'version': '0.15.2', 'url': 'http://download.pingcap.org/node_exporter-0.15.2.linux-amd64.tar.gz'})
changed: [localhost] => (item={'name': 'pushgateway', 'version': '0.4.0', 'url': 'http://download.pingcap.org/pushgateway-0.4.0.linux-amd64.tar.gz'})
changed: [localhost] => (item={'name': 'grafana', 'version': '4.6.3', 'url': 'http://download.pingcap.org/grafana-4.6.3.linux-x64.tar.gz'})
changed: [localhost] => (item={'name': 'blackbox_exporter', 'version': '0.12.0', 'url': 'http://download.pingcap.org/blackbox_exporter-0.12.0.linux-amd64.tar.gz'})
TASK [local : download TiSpark packages] *******************************************************************************************************************************************************************************************************
changed: [localhost] => (item={'name': 'spark-2.4.3-bin-hadoop2.7.tgz', 'version': '2.4.3', 'url': 'http://download.pingcap.org/spark-2.4.3-bin-hadoop2.7.tgz', 'checksum': 'sha256:80a4c564ceff0d9aff82b7df610b1d34e777b45042e21e2d41f3e497bb1fa5d8'})
changed: [localhost] => (item={'name': 'tispark-core-2.1.8-spark_2.4-jar-with-dependencies.jar', 'version': '2.1.8', 'url': 'https://download.pingcap.org/tispark-core-2.1.8-spark_2.4-jar-with-dependencies.jar'})
changed: [localhost] => (item={'name': 'tispark-sample-data.tar.gz', 'version': 'latest', 'url': 'http://download.pingcap.org/tispark-sample-data.tar.gz'})
...
TASK [local : cp tidb binary] ******************************************************************************************************************************************************************************************************************
changed: [localhost] => (item={'name': 'tidb', 'version': 'v2.1.19', 'url': 'http://download.pingcap.org/tidb-v2.1.19-linux-amd64.tar.gz'})
...
localhost : ok=29 changed=20 unreachable=0 failed=0
Congrats! All goes well. :-)
分配资源,部署集群
修改 inventory.ini
配置文件中的变量,初始化、部署 TiDB。
[tidb@shawnyan tidb-ansible]$ ansible-playbook bootstrap.yml
PLAY [initializing deployment target]
PLAY [check node config]
TASK [pre-ansible : disk space check - fail when disk is full]
TASK [pre-ansible : Get distro name from /etc/os-release]
TASK [pre-ansible : python check]
TASK [pre-ansible : Redhat/CentOS - Make sure ntp, ntpstat have been installed]
TASK [bootstrap : setting absent kernel params]
TASK [bootstrap : setting present kernel params]
TASK [bootstrap : update /etc/security/limits.conf]
TASK [bootstrap : disable swap]
TASK [bootstrap : create group]
TASK [bootstrap : create account]
PLAY [create ops scripts]
TASK [ops : create check_tikv.sh script]
TASK [ops : create pd-ctl.sh script]
Congrats! All goes well. :-)
[tidb@shawnyan tidb-ansible]$
[tidb@shawnyan tidb-ansible]$ ansible-playbook deploy.yml
PLAY [check config locally]
TASK [check_config_static : Ensure TiDB host exists]
TASK [check_config_static : Ensure PD host exists]
TASK [check_config_static : Ensure TiKV host exists]
TASK [check_config_static : Check ansible_user variable]
TASK [check_config_static : Ensure timezone variable is set]
PLAY [initializing deployment target]
TASK [check_config_dynamic : Set enable_binlog variable]
TASK [check_config_dynamic : Set deploy_dir if not set]
TASK [check_config_dynamic : environment check (deploy dir)]
PLAY [Pre-check PD configuration]
TASK [check_config_pd : set_fact]
TASK [check_config_pd : Create temporary check directory]
TASK [check_config_pd : Load PD vars]
TASK [check_config_pd : Load customized config: tidb-ansible/conf/pd.yml]
TASK [check_config_pd : Load default config]
TASK [check_config_pd : Generate dynamic config]
TASK [check_config_pd : Generate final config]
TASK [check_config_pd : Create configuration file]
TASK [check_config_pd : Deploy PD binary]
TASK [check_config_pd : Check PD config]
TASK [check_config_pd : Delete temporary check directory]
TASK [check_config_pd : Check result]
PLAY [Pre-check TiKV configuration]
TASK [check_config_tikv : Load customized config: tidb-ansible/conf/tikv.yml]
TASK [check_config_tikv : Deploy TiKV binary]
TASK [check_config_tikv : Check result]
PLAY [Pre-check TiDB configuration]
TASK [check_config_tidb : Load TiDB default vars]
TASK [check_config_tidb : Load TiDB group vars]
TASK [check_config_tidb : Load customized config: tidb-ansible/conf/tidb.yml]
TASK [check_config_tidb : Load default config]
TASK [check_config_tidb : generate dynamic config]
TASK [check_config_tidb : Deploy TiDB binary]
TASK [check_config_tidb : Check result]
PLAY [deploying PD cluster]
TASK [common_dir : create deploy directories]
TASK [common_dir : create status directory]
TASK [common_dir : create deploy binary directory]
TASK [pd : create deploy directories]
TASK [pd : load customized config: tidb-ansible/conf/pd.yml]
TASK [pd : load default config]
TASK [pd : generate dynamic config]
TASK [pd : generate final config]
TASK [pd : create configuration file]
TASK [pd : backup conf file]
TASK [pd : deploy binary]
TASK [pd : backup binary file]
TASK [pd : create startup script]
TASK [include_role : systemd]
TASK [systemd : create systemd service configuration]
TASK [systemd : create startup script - common start/stop]
TASK [systemd : reload systemd]
PLAY [deploying TiKV cluster]
TASK [tikv : Preflight check - Does tikv data dir meet ext4 file system requirement]
TASK [tikv : generate final config]
TASK [tikv : deploy binary]
TASK [include_role : systemd]
PLAY [deploying TiDB cluster]
TASK [tidb : combine final config]
TASK [tidb : deploy binary]
TASK [include_role : systemd]
Congrats! All goes well. :-)
启动 TiDB
执行 start 剧本,启动 TiDB 集群。
[tidb@shawnyan tidb-ansible]$ ansible-playbook start.yml
PLAY [check config locally]
TASK [check_config_static : Ensure TiDB host exists]
TASK [check_config_static : Ensure PD host exists]
TASK [check_config_static : Ensure TiKV host exists]
TASK [check_config_static : Check ansible_user variable]
TASK [check_config_static : Ensure timezone variable is set]
...
PLAY [gather all facts, and check dest]
TASK [check_config_dynamic : Set enable_binlog variable]
TASK [check_config_dynamic : Set deploy_dir if not set]
TASK [check_config_dynamic : environment check (deploy dir)]
TASK [check_config_dynamic : Preflight check - Does deploy dir have appropriate permission]
...
PLAY [pd_servers]
TASK [start PD by supervise]
TASK [start PD by systemd]
TASK [wait until the PD port is up]
TASK [wait until the PD health page is available]
TASK [wait until the PD health page is available when enable_tls]
...
PLAY [tikv_servers]
TASK [start TiKV by supervise]
TASK [start TiKV by systemd]
TASK [wait until the TiKV port is up]
TASK [wait until the TiKV status page is available]
TASK [wait until the TiKV status page is available when enable_tls]
TASK [wait until TiKV process is up]
TASK [display new tikv pid]
...
PLAY [tidb_servers]
TASK [start TiDB by systemd]
TASK [wait until the TiDB port is up]
TASK [wait until the TiDB status page is available]
TASK [wait until the TiDB status page is available when enable_tls]
Congrats! All goes well. :-)
TiDB 2.x 上手
连接 TiDB
使用 MySQL 客户端连接 TiDB 集群。
[tidb@shawnyan tidb-ansible]$ mysql -u root -h 192.168.8.121 -P 4000
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.7.25-TiDB-v2.1.19 MySQL Community Server (Apache License 2.0)
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MySQL [(none)]> select tidb_version()\G
*************************** 1. row ***************************
tidb_version(): Release Version: v2.1.19
Git Commit Hash: fcbcc4569fbd7aba9eb92092149f092346b934ec
Git Branch: HEAD
UTC Build Time: 2019-12-27 11:32:43
GoVersion: go version go1.13 linux/amd64
Race Enabled: false
TiKV Min Version: 2.1.0-alpha.1-ff3dd160846b7d1aed9079c389fc188f7f5ea13e
Check Table Before Drop: false
1 row in set (0.00 sec)
调用 HTTP API 获取 TiDB 信息
调用 HTTP API 查看 TiDB 数据库状态。
[tidb@shawnyan tidb-ansible]$ curl -s localhost:10080/info | jq
{
"is_owner": true,
"version": "5.7.25-TiDB-v2.1.19",
"git_hash": "fcbcc4569fbd7aba9eb92092149f092346b934ec",
"ddl_id": "0b203125-0b11-4723-8cba-8e7272746dc3",
"ip": "192.168.8.121",
"listening_port": 4000,
"status_port": 10080,
"lease": "45s",
"binlog_status": "Off"
}
查看 I_S 下的表
MySQL [information_schema]> show tables;
+---------------------------------------+
| Tables_in_information_schema |
+---------------------------------------+
| CHARACTER_SETS |
| COLLATIONS |
| COLLATION_CHARACTER_SET_APPLICABILITY |
| COLUMNS |
| COLUMN_PRIVILEGES |
| ENGINES |
| EVENTS |
| FILES |
| GLOBAL_STATUS |
| GLOBAL_VARIABLES |
| KEY_COLUMN_USAGE |
| OPTIMIZER_TRACE |
| PARAMETERS |
| PARTITIONS |
| PLUGINS |
| PROCESSLIST |
| PROFILING |
| REFERENTIAL_CONSTRAINTS |
| ROUTINES |
| SCHEMATA |
| SCHEMA_PRIVILEGES |
| SESSION_STATUS |
| SESSION_VARIABLES |
| SLOW_QUERY |
| STATISTICS |
| TABLES |
| TABLESPACES |
| TABLE_CONSTRAINTS |
| TABLE_PRIVILEGES |
| TIDB_INDEXES |
| TRIGGERS |
| USER_PRIVILEGES |
| VIEWS |
+---------------------------------------+
33 rows in set (0.00 sec)
未完待续
在 TiDB 2.x 阶段还有几件重要的事情:
18年8月,CNCF 接纳 TiKV 作为 CNCF Sandbox 的云原生项目。
19年5月,CNCF 宣布正式将 TiKV 从沙箱项目晋级至孵化项目。
19年6月,TiDB User Group 正式成立。
同月,TiDB 3.0 GA 发布,这个,我们下期接着聊。
🌻 往期精彩 ▼
[Oracle]
- Oracle 数据库全面升级为 23ai
- python-oracledb 已率先支持 Oracle 23ai
- 一文带你了解 Oracle 23ai 新特性 Vector 的基础用法
[MySQL]
- 「合集」MySQL 8.x 系列文章汇总
- 如何选择适合的 MySQL Connector/J 版本
- MySQL 8.4.0 LTS 发布 (MySQL 第一个长期支持版本)
[TiDB]
- 星辰考古:TiDB v1.0 再回首
- TiDB x KubeBlocks 集成案例
- TiDB v7.5.0 LTS 升级必读 | 新特性补充说明
[PG]
- 哈喽,国产数据库!Halo DB!
- 即将告别PG 12,建议升级到PG 16.3版本
- 后 EL 7 时代,PG 16 如何在 CentOS 7 上运行
– / END / –
👉 这里可以找到我
- 微信公众号:少安事务所 (mysqloffice)
- 墨天轮:严少安
- ITPUB:少安事务所
如果这篇文章为你带来了灵感或启发,就请帮忙点『赞』or『在看』or『转发』吧,感谢!(๑˃̵ᴗ˂̵)