ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库部署与SQL执行链路:从SELinux到事务锁优化

数据库部署与SQL执行链路:从SELinux到事务锁优化

简介:这是一份郑州大学计算机与人工智能学院《数据库系统原理》课程的实验报告文档,适合郑大计算机类学生及需要完成GreatSQL环境搭建与数据库管理系统实验的初学者参考。报告完整覆盖了从认识DBMS系统、配置VMware+CentOS运行环境、关闭SELinux与防火墙、安装依赖包,到部署万里数据库GreatSQL、管理服务与查看逻辑/物理组件等环节,并配有实验截图、命令解释、结果分析与问题反思。资源为1个docx文件,压缩包大小约3.69MB,内容可直接对照翻阅或作为报告撰写格式范本。已有509人学习下载,尤其适合在实验任务中快速定位安装命令、理清实验步骤和规范化呈现实验截图与表格的同学。

1. 数据库实验的第一关:装了 SELinux 的系统,数据库为什么“失语”

做数据库原理实验,多数人以为最大的坎是 SQL 写不对,真正卡住他们的往往是环境配置。一份数据库实验报告的第一个操作,不是 CREATE TABLE,而是一行setenforce=0和一次防火墙禁用。这个动作看起来像“偷懒”,背后其实是 SELinux 的强制访问控制(MAC)机制会拦截数据库进程的文件读写与网络通信,让它明明启动着,却对外“失语”。搞懂这一步,等于拿到了理解整个 DBMS 的钥匙:数据库要文件、要端口、要特权操作,而这些恰恰都是安全机制默认不放行的东西。这也是为什么这份实验报告的安装部分读起来像系统课——它逼着你从进程的视角看待一个数据库的生存条件。

2. 安装链路拆解:二进制包、最小权限用户与 systemd 的同一个目标

这一步实验的目标不是“把 SQL 跑起来”,而是先把 mysqld 变成一个可控的系统服务。很多人把下载解压后的启动失败归结为玄学,实际上整条链路——依赖包选择、用户创建、服务文件注册、全局配置文件——都在围绕同一件事:让数据库进程以正确的权限、正确的库、正确的方式运行。拆开看,每个步骤背后都有明确的设计意图。

2.1 glibc 2.17 和那串依赖包:二进制包并不是“免安装”

实验下载的是GreatSQL-8.0.32-25-Linux-glibc2.17-x86_64-minimal.tar.xz。这个文件名里信息量很大:glibc 2.17 意味着二进制是在 CentOS 7 的 libc 版本下编译的,直接决定了它能跑在什么系统上;minimal表示精简安装,不含客户端全家桶,只保留服务端核心。但“二进制包”不等于“免安装”——它只省去了编译,运行时的动态库依赖一个都不能少。实验里的这条命令决定了 mysqld 能不能正常拉起:

yum install -y pkg-config perl libaio-devel numactl-devel numactl-libs \ net-tools openssl openssl-devel jemalloc jemalloc-devel \ perl-Data-Dumper perl-Digest-MD5 python2 perl-JSON perl-Test-Simple

逐个看几个关键包:libaio-devel是 InnoDB 原生异步 IO 的基础,数据库刷盘不能靠同步等待磁盘;numactl-devel提供 NUMA 感知能力,MySQL 的大缓冲池场景最怕内存跨节点访问,绑定 NUMA 能显著减少远端内存延迟;jemalloc则是把 glibc 默认的内存分配器替换掉,多线程高并发下 glibc malloc 的锁竞争和内存碎片是出了名的痛点,jemalloc 用 arena 分片把分配压力摊开。perl-JSON、perl-Test-Simple这些看起来八竿子打不着,它们是管理脚本的运行时依赖。所以这串依赖不是“多点几个包没关系”,而是每一个都对应一个运行时行为。

2.2 mysql 用户没有家目录:最小权限的边界设计

安装流程里有一段很容易被跳过的操作:

/sbin/groupadd mysql /sbin/useradd -g mysql mysql -d /dev/null -s /sbin/nologin mkdir /var/lib/mysql/ chown -R mysql:mysql /var/lib/mysql/

-d /dev/null表示不为这个用户创建家目录,-s /sbin/nologin表示这个用户永远无法登录 shell。为什么要这么“苛刻”?数据库进程本身只需要一个能标识文件属主的 uid,不需要任何交互能力。凡是能登录的用户都会扩大攻击面——一旦数据库被注入或文件权限配置失误,攻击者能拿到的只是一个无法登录的账号,而不是一个 shell。

接下来的chown -R mysql:mysql /var/lib/mysql/是启动的前提。mysqld 在初始化数据目录时会检查目录属主,属主不是当前运行用户就直接拒绝工作。这一步如果做错,后续systemctl start会看到服务“启动即失败”,错误日志里全是权限相关报错。所以数据目录的属主必须和进程用户严格一致,这是数据库部署里最容易踩的坑,也是最容易排查的坑。

2.3 systemd 服务与 my.cnf:托管生命周期,而不是手动启停

把二进制解压到/usr/local之后,还需要让系统“认识”这个数据库。实验用了一个非常标准的做法:写 systemd 服务文件。

[Unit] Description=GreatSQL Server After=network.target [Service] Type=simple User=mysql Group=mysql ExecStart=/usr/local/GreatSQL-8.0.32-25-Linux-glibc2.17-x86_64-minimal/bin/mysqld \ --defaults-file=/etc/my.cnf LimitNOFILE=65535 Restart=on-failure [Install] WantedBy=multi-user.target

写完文件必须执行systemctl daemon-reload,否则 systemd 会报Unit not found。这是因为 systemd 在启动时已经把单元文件加载进缓存,新增或修改后必须重载才能生效。这一步是新手最容易卡住的地方——文件明明写对了,服务就是起不来。

my.cnf是全局配置文件,关键项包括datadir(数据目录)、socket(本地连接套接字)、port(监听端口)、character-set-server(默认字符集)和log-error(错误日志路径)。数据目录放哪里,本质就是配置文件里这一个路径决定,这也呼应了后面“查看数据库物理存储位置”的实验目的。

启动之后,实验要求用三条命令做三层验证,这个习惯非常好:

systemctl status greatsql ps -ef | grep mysqld ss -lntp | grep mysqld

systemctl status看的是服务状态,ps看进程是否存活,ss看端口是否真的在监听。这三层必须逐层确认,因为服务显示 active 不代表端口已就绪,mysqld 在初始化早期、权限出错或启动参数有误时,都可能出现“进程活着但没监听”的中间态。

3. 逻辑组件与物理存储:基本表、视图、触发器之间的虚实边界

建表之后,数据库里除了“表”还多出一批看不见的东西:视图、触发器、存储过程、约束。它们都被记录在information_schema数据字典里,这也是实验里大量查询information_schema的原因。理解这一章的关键,是分清哪些组件真正占有物理存储,哪些只是“定义”。这个区分决定了你对数据库逻辑结构的认知是否准确。

3.1 基本表与视图:一个占物理页,一个只存定义

基本表是“实物”——数据行落在表空间的数据页上。视图则是“投影”:它不存储数据,只保存一条 SELECT 定义,每次被引用时动态展开成对底层表的查询。实验里的两条命令放在一起看非常有意思:

SHOW TABLES; SHOW FULL PROCESSLIST;

前者确实能看到包括视图在内的表列表,但后者根本不是“查看视图”的命令——它列出的是当前所有连接线程。这是这份实验报告里一个隐蔽的错位,后面避坑部分会细说。查视图的正确入口是information_schema.VIEWS,里面存着视图名、定义者、字符集和完整的VIEW_DEFINITIONSQL 文本。

视图的价值有两个:一是把复杂查询封装成“看起来像表”的对象,让业务侧只面对简单接口;二是做行级或列级的安全隔离,比如只暴露学生的姓名和专业,不暴露出生日期等隐私列。代价也很直接:每次查视图都会重新执行底层 SQL,如果视图嵌套多层,性能可能不如直接查表。所以视图是逻辑层的“虚表”,不是物理层的“复制表”。

3.2 触发器与存储过程:事件钩子和预编译单元

触发器和存储过程都记录在数据字典里。实验用下面两条命令读取它们:

SELECT * FROM information_schema.TRIGGERS\G SELECT ROUTINE_NAME, ROUTINE_TYPE, CREATED FROM information_schema.ROUTINES WHERE ROUTINE_TYPE = 'PROCEDURE';

TRIGGERS表里的关键字段是EVENT_MANIPULATION(INSERT/UPDATE/DELETE)、ACTION_TIMING(BEFORE/AFTER)、ACTION_STATEMENT(触发器体里的 SQL)。从数据字典视角看,触发器就是一个“事件绑定的钩子”:某张表发生指定事件时,数据库自动执行一段语句块。它常用于数据一致性维护,比如订单删除时联动清理明细,或禁止在非工作时间修改核心表。

存储过程和触发器在数据字典里是两类不同的对象,但有一个共同点:都是服务端预编译的 SQL 逻辑。存储过程被封装成独立单元,业务只需传参数调用,减少客户端与服务器之间的往返;触发器则完全由事件驱动,业务方甚至感知不到它的存在。这也意味着触发器有隐形成本——它对调用方透明,出现问题后排查链路更长,写日志、做审计时要格外小心。

3.3 约束不是免费的:主键、唯一索引与外键的物理代价

约束在逻辑层是“规则”,在物理层却对应真实的数据结构。实验二的建表语句正好展示了这一点:

CREATE TABLE STUDENTS( Sno char(9) PRIMARY KEY, Sname char(10) NOT NULL, Sex char(2), Birthday DATE, Speciality char(20), Dno char(4) ); CREATE UNIQUE INDEX Course_Cno ON Courses (Cno); DROP INDEX Course_Cno ON Courses;

主键约束在 InnoDB 里会生成聚簇索引,整张表的数据行按主键的 B+ 树组织;唯一约束会生成一个强制键值不重复的二级索引,唯一性的检查靠索引结构本身来完成,不是逐行扫描比对。外键则更贵——每次插入、更新子表记录时,数据库都要去父表检查引用是否存在,这背后的锁和事务开销容易被低估。

所以建约束时值得多问一句:这条规则真的需要数据库来强制,还是应用层可以兜底?实验里对 SC 表采用复合主键PRIMARY KEY (Sno, Cno)而不是额外的自增 id,这正是教科书里的标准做法——业务语义本身就是唯一标识,不需要再引入一个无业务含义的代理键。约束是数据完整性的“后悔药”,但它不是免费的。

4. SQL 执行链路:从解析器到数据页,一条查询怎么“走”到磁盘

实验一要求了解数据库系统架构,给出的组件清单是:连接池、系统管理与控制工具、SQL 接口、查询解析器、查询优化器、可插拔存储引擎,以及系统和日志文件。这些组件不是并列的装饰,它们构成了一条完整的执行链路。这一章把这条链路从入口到磁盘走一遍。

4.1 数据页:为什么物理存储的单位不是“行”而是“页”

实验里有一句结论:物理存储的存储单位是数据页。这是一个值得展开的机制点。InnoDB 的默认页大小是 16KB,表的数据按主键聚簇成 B+ 树,树的叶子节点就是数据页;磁盘 IO、缓冲池换入换出都以页为粒度。

为什么不用“行”作为 IO 单位?因为磁盘的最小寻址和传输成本很高,按页读写可以利用空间局部性,一次读取就带回相邻的多行,后续查询命中缓冲池的概率更高。缓冲池(Buffer Pool)是 InnoDB 性能的核心,它以页为缓存单位,脏页由后台线程统一刷盘,而不是每条语句实时落盘。理解了这一点,就理解了为什么ls /var/lib/mysql看到的是ibdata1、#innodb_redo这样的文件,而不是一张表一个 txt——数据文件和内存页之间,隔着一整套缓冲与刷盘机制。

4.2 六大组件与一条命令的旅程

一条 SQL 从客户端到磁盘,要顺序经过以下组件:

组件职责一句话理解
连接池管理客户端连接与认证分配线程,管理会话状态
SQL 接口接收 SQL 语句DML/DDL 的入口通道
解析器词法、语法分析生成语法树,校验语义
优化器生成执行计划决定走哪个索引、什么连接顺序
存储引擎读写表数据与索引负责任务、锁、页与事务日志
日志文件记录重做与撤销信息redo 保证崩溃恢复,undo 支持回滚

解析器只负责“看懂”语句,不负责“做好”:它把 SQL 拆成语法树,检查表名、列名是否存在,权限是否足够。真正决定性能的是优化器——它会基于统计信息估算代价,在“全表扫描”和“走索引”之间做选择。实验里介绍的 MGR(组复制)位于引擎之上,通过 binlog 在节点间广播事务,属于高可用层面的能力,单机执行链路并不依赖它。

4.3 一条 WHERE 查询是怎么走到数据页的

实验三有大量查询练习,其中一条很典型:

SELECT Sname, Speciality FROM Students WHERE Year(Birthday) BETWEEN 1997 AND 1999;

从执行链路看,这条语句先经过解析器生成语法树,然后优化器评估访问路径。关键问题在于Year(Birthday):优化器面对被函数包裹的列时,通常会放弃该列上的索引——因为 B+ 树存储的是原始值,无法直接对“年份”做范围定位。执行计划会退化成全表扫描,即使 Birthday 上建了索引也用不上。

执行计划可以用 EXPLAIN 查看,典型的退化特征如下:

+----+-------------+----------+------------+------+---------------+ | id | select_type | table | type | rows | Extra | +----+-------------+----------+------------+------+---------------+ | 1 | SIMPLE | Students | ALL | 6 | Using where | +----+-------------+----------+------------+------+---------------+

type = ALL就是全表扫描。正确的写法是直接让索引可比较:

SELECT Sname, Speciality FROM Students WHERE Birthday BETWEEN '1997-01-01' AND '1999-12-31';

Bloomfield 这样的细节在教材查询里看不出差别,但数据量一上来,就是几百倍的时间差。实验报告里特别提到 GreatSQL 的 InnoDB 并行查询能把 TPC-H 性能平均提升 15 倍、最高 40 多倍,这个提升背后正是优化器把大查询拆给多个 worker 线程并行扫描分区或索引——本质是拿调度开销换大查询吞吐,适用于 OLAP 场景和周期性统计。这和执行链路的关系是:查询优化不只发生在索引选择这一步,已经延伸到引擎层的并行执行策略。

提示:写 SQL 时把函数套在列上,等于主动告诉优化器“这个索引用不上了”。条件写在函数里,吃亏的是执行计划。

5. 避坑记录:SELinux、字符集与级联删除的五类现场

这一章是血泪经验。实验报告里明确说“安装过程中没有遇到什么问题”,但那些没有被记录下来的错位、误判和隐藏条件,恰恰是新手最容易翻车的地方。按“现象 → 原因 → 解决”的格式整理五条。

5.1 现象:SHOW FULL PROCESSLIST 被当成“查看视图”的命令

实验报告在“视图”这一条目下给出的命令是SHOW FULL PROCESSLIST,这实际上是查看当前所有连接线程的命令。执行后你会看到一列Command字段,里面是Sleep、Query之类,跟“视图”没有任何关系。原因是在“查视图”这个操作上抄错了命令,没有核对输出内容。解决方法是改用SHOW FULL TABLES WHERE Table_type = 'VIEW',或者直接查数据字典:

SELECT TABLE_NAME FROM information_schema.VIEWS;

这个错位不致命,但它提醒一件事:不是命令执行成功就代表操作正确,要养成看输出内容验证的习惯。

5.2 现象:systemctl start 显示 active,端口却没有任何监听

服务状态正常、进程存在,但ss -lntp | grep mysqld没有输出。常见原因是 mysqld 还处在初始化早期,或my.cnf里的port、socket配置被覆盖。另一个隐蔽原因是/var/lib/mysql的属主不是 mysql,导致 InnoDB 在打开数据文件时失败。解决路径是看错误日志:tail -f /var/log/mysqld.log,权限问题会直接在日志里报Permission denied,配置问题则会报unknown variable或端口冲突。

5.3 现象:插入中文数据后查询乱码

数据库默认字符集不是 utf8mb4,客户端连接时走的又是另一套字符集,两端不一致就会在写入和读取时发生编码转换错误。解决方法是统一字符集:在/etc/my.cnf里配置character-set-server = utf8mb4,建库时显式指定CREATE DATABASE DB DEFAULT CHARACTER SET utf8mb4,同时保持客户端连接字符集与服务端一致。

5.4 现象:DROP TABLE students 被外键挡住

SC表通过外键引用了Students,直接删父表会报外键约束错误。解决办法是先删除引用表,或先移除外键约束。这也是为什么实验里第一次执行drop table students cascade会失败——CASCADE是 PostgreSQL 风格的级联删除语法,GreatSQL/MySQL 并不支持在 DROP TABLE 时带CASCADE来级联清理引用表。遇到这种情况,正确顺序是:

DROP TABLE SC; DROP TABLE Students;

5.5 现象:SELinux 一开,数据库就“失联”

这是第 1 章那个问题的现场版。SELinux 开启时,即使 mysqld 进程活着,它对数据目录的文件访问也可能被 MAC 策略拦截,网络端口也可能无法正常监听,客户端连接直接超时。实验选择了关闭 SELinux 和防火墙,这是开发环境里最省事的做法——数据库需要文件读写、网络监听和特权操作,这些恰好都是安全机制默认不放行的。原因需要理解清楚:这不是“Linux 安全机制没用所以关掉”,而是“在隔离的虚拟机实验环境里,安全策略的收益低于配置成本”。

6. 最后的取舍:事务锁从红黑树到无锁哈希,到底在换什么

实验一里提到一个很值得琢磨的改造:GreatSQL 把 InnoDB 的事务锁数据结构从红黑树优化成无锁哈希结构。红黑树是平衡二叉搜索树,插入、删除、查找都是 O(log n);哈希表在等值查找下接近 O(1)。锁对象查找恰恰是高并发场景下的热路径。

我一般会这么理解:事务锁的查找本质是“精确匹配”——一个事务要判断自己与哪些已持有锁的事务冲突,查的是特定行或特定间隙的锁对象,这正是哈希的主场。红黑树虽然支持范围遍历和有序输出,但高并发下,树旋转和全局结构竞争带来的开销都落在每一次加锁解锁上,属于纯成本。换成无锁哈希后,查找路径变短,并发度上去了,事务并行性能随之改善。这个方向落地时还会配合更细分的锁分区,避免所有线程挤在同一个锁结构上。

但是无锁哈希不是白捡的性能:它为等值查找优化,牺牲了范围查询能力;调试死锁、打印锁等待链时需要额外辅助结构来重建有序视图。这就像把工具箱里的万用扳手换成了成套的专用套筒——常用场景更快了,冷门场景则需要另找工具。同一个思路也贯穿在 GreatSQL 对 MGR 的流控优化和大事务检测里:都是先识别最痛的热路径,再用针对性的数据结构去替换通用方案。

从那以后,我每次看到某个数据库“特性优化”,都会先问三个问题:原来是什么结构、换成了什么结构、放弃了什么能力。想清楚这三点,性能提升的“为什么”就不再是玄学。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进