参考何人听我楚狂声的代码,深入理解数据库知识,顺便作为自己项目的准备。
什么是事务
Transaction是关系型数据库的核心组成,它将数据有条理地保存在储存介质(磁盘)中,
并在逻辑上,将数据以结构化的形态呈现给用户。支持数据的增、删、改、查,并在过程中保障数据的正确且可靠。
为了保证数据正确可靠,对应的拥有四大特性:
- 原子性(Atomicity): 事务要么全部完成,要么全部取消。 如果事务崩溃,状态回到事务之前(事务回滚)。
- 隔离性(Isolation): 如果2个事务 T1 和 T2 同时运行,事务 T1 和 T2 最终的结果是相同的,不管 T1和T2谁先结束。
- 持久性(Durability): 一旦事务提交,不管发生什么(崩溃或者出错),数据要保存在数据库中。
- 一致性(Consistency): 只有合法的数据(依照关系约束和函数约束)才能写入数据库。
如何保证原子性
begin; -- 开始一个事务
update table set A = A - 1亿; -- 伪sql,仅作示意
update table set B = B + 1亿;
-- 其他读写操作
commit; -- 提交事务
要保证上面操作的原子性, 就得等begin和commit之间的操作全部成功完成后,才将结果统一提交给数据库保存,如果途中任意一个操作失败,就撤销前面的操作,且操作不会提交数据库保存,这样就保证了同生共死。
如何保证隔离性
引入数据的隔离机制,确保同时只能有一个事务在修改A,一个修改完了,另一个才来修改。 这需要对数据A加上互斥锁。在事务中更新某条数据获得的互斥锁,只有在事务提交或失败之后才会释放,在此之前,其他事务是只能读,不能写这条数据的。
这也就引出了隔离性的强度,四大级别。(不展开了)
如何保证持久性
如果在事务提交后,事务的数据还没有真正落到磁盘上,此时数据库奔溃了,事务对应的数据会不会丢?
事务会保证数据不会丢,当数据库因不可抗拒的原因奔溃后重启,它会保证:
- 成功提交的事务,数据会保存到磁盘
- 未提交的事务,相应的数据会回滚
事务日志
数据库通过事务日志来达到这个目标。 事务的每一个操作(增/删/改)产生一条日志,内容组成大概如下:
- LSN:一个按时间顺序分配的唯一日志序列号,靠后的操作的LSN比靠前的大。
- TransID:产生操作的事务ID。
- PageID:被修改的数据在磁盘上的位置,数据以页为单位存储。
- PrevLSN:同一个事务产生的上一条日志记录的指针。
- UNDO:取消本次操作的方法,按照此方法回滚。
- REDO:重复本次操作的方法,如有必要,重复此方法保证操作成功。
磁盘上每个页(保存数据的,不是保存日志的)都记录着最后一个修改该数据操作的LSN。数据库会通过解析事务日志,将修改真正落到磁盘上(写盘),随后清理事务日志(正常情况下)。
这也是数据库在保证数据安全和性能这两个点之前的折中办法:
- 如果每次更新都写盘,由于数据是随机的,会造成大量的随机IO,性能会非常差
- 如果每次更新不马上写盘,那一旦数据库崩溃,数据就会丢失
折中的办法就是:
- 将数据的变更以事务日志的方式,按照时间先后追加到日志缓冲区,由特定算法写入事务日志,这是顺序IO,性能较好
- 通过数据管理器解析事务日志,由特定的算法择机进行写盘
数据库恢复
当数据库从崩溃中恢复时,会有以下几个步骤:
- 解析存在的事务日志,分析哪些事务需要回滚,哪些需要写盘(还没来得及写盘,数据库就崩溃了)。
- Redo,进行写盘。检测对应数据所在数据页的LSN,如果数据页的LSN>=事务操作的LSN,说明已经写过盘,不然进行写盘操作。
- Undo, 按照LSN倒序进行回滚
经过这几个阶段,在数据库恢复后,可以达到奔溃前的状态,也保证了数据的一致性。
XID
XID是什么
MySQL Binlog 文件由 event 组成,event 有不同的类型,而XID_EVENT 表示一个事务的提交操作。
当事务提交时,在 binlog 依赖的内部 XA 中,额外添加了 Xid 结构,binlog 有多种数据类型:
- statement 格式,记录为基本语句,包含 Commit
- row 格式,记录为基于行
- mixed 格式,日志记录使用混合格式
不论是 statement 还是 row 格式,binlog 都会添加一个 XID_EVENT 作为事务的结束,该事件记录了事务的 ID 也就是 Xid,在 MySQL 进行崩溃恢复时根据 binlog 中提交的情况来决定如何恢复。
XID如何生成
MySQL 内部维护了一个全局变量 global_query_id,每次执行语句的时候将它赋值给 Query_id,然后给这个变量加 1。如果当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid。
XID是唯一的吗
global_query_id 是一个纯内存变量,重启之后就清零了。所以在同一个数据库实例中,不同事务的 Xid 也是有可能相同的。
但是 MySQL 重启之后会重新生成新的 binlog 文件,这就保证了,同一个 binlog 文件里,Xid 一定是惟一的。
虽然 MySQL 重启不会导致同一个 binlog 里面出现两个相同的 Xid,但是如果 global_query_id 达到上限后,就会继续从 0 开始计数。从理论上讲,还是就会出现同一个 binlog 里面出现相同 Xid 的场景。
在本项目中的设计
在 MYDB 中,每一个事务都有一个 XID,这个 ID 唯一标识了这个事务。事务的 XID 从 1 开始标号,并自增,不可重复。并特殊规定 XID=0 是一个超级事务(Super Transaction)。当一些操作想在没有申请事务的情况下进行,那么可以将操作的 XID 设置为 0,这样就不会产生记录。XID 为 0 的事务的状态永远是 committed。
XID文件存储格式
事务的个数(8个字节) | 事务 xid1 的状态(1个字节) | 事务 xid2 的状态(1个字节)| ...
TransactionManager 维护了一个 XID 格式的文件,用来记录各个事务的状态。MYDB 中,每个事务都有下面的三种状态:
- active,正在进行,尚未结束
- committed,已提交
- aborted,已撤销(回滚)
XID 文件给每个事务分配了一个字节的空间,用来保存其状态。同时,在 XID 文件的头部,还保存了一个 8 字节的数字,记录了这个 XID 文件管理的事务的个数。于是,事务 xid 在文件中的状态就存储在 (xid-1)+8 字节处,xid-1 是因为 xid 0(Super XID) 的状态不需要记录。
SQL解析器
MySQL的SQL解析器(SQL parser)是一个负责将SQL语句转换为可执行的指令的组件。其主要功能是将输入的SQL语句分解为语法单元,然后将这些语法单元转换为内部表示的数据结构,最终生成一个可执行的查询计划。解析器是MySQL中的一个重要组成部分,它直接影响查询的性能和正确性。
MySQL的SQL解析器基于传统的编译原理中的语法分析技术。在解析SQL语句的过程中,它会进行词法分析、语法分析、语义分析等操作。
- 词法分析:将SQL语句分解为语法单元(token),如SELECT、FROM、WHERE等关键字、表名、列名、运算符等。词法分析器会识别和记录每个语法单元的类型和位置。
- 语法分析:将词法分析器生成的语法单元按照SQL语法规则组合成语法树。语法分析器会检查SQL语句是否符合语法规则,同时生成语法树,确定查询的逻辑结构。
- 语义分析:在语义分析阶段,MySQL的SQL解析器会对SQL语句的语义进行检查。这一阶段的任务包括对表和列名进行解析,检查SQL语句的语义正确性,并将SQL语句转换为适当的内部数据结构。
最终,SQL解析器将SQL语句转换为一个可执行的查询计划,交给MySQL的查询执行引擎进一步处理。
数据库整体结构
MYDB 分为后端和前端,前后端通过 socket 进行交互。前端(客户端)的职责很单一,读取用户输入,并发送到后端执行,输出返回结果,并等待下一次输入。MYDB 后端则需要解析 SQL,如果是合法的 SQL,就尝试执行并返回结果。不包括解析器,MYDB 的后端划分为五个模块,每个模块都又一定的职责,通过接口向其依赖的模块提供方法。五个模块如下:
- Transaction Manager(TM)
- Data Manager(DM)
- Version Manager(VM)
- Index Manager(IM)
- Table Manager(TBM)
每个模块的职责如下:文章来源:https://www.toymoban.com/news/detail-610784.html
- TM 通过维护 XID 文件来维护事务的状态,并提供接口供其他模块来查询某个事务的状态。
- DM 直接管理数据库 DB 文件和日志文件。DM 的主要职责有:1) 分页管理 DB 文件,并进行缓存;2) 管理日志文件,保证在发生错误时可以根据日志进行恢复;3) 抽象 DB 文件为 DataItem 供上层模块使用,并提供缓存。
- VM 基于两段锁协议实现了调度序列的可串行化,并实现了 MVCC 以消除读写阻塞。同时实现了两种隔离级别。
- IM 实现了基于 B+ 树的索引,目前 where 只支持已索引字段。
- TBM 实现了对字段和表的管理。同时,解析 SQL 语句,并根据语句操作表。
Transaction Manager
TransactionManager 提供了一些接口供其他模块调用,用来创建事务和查询事务状态。更具体的:文章来源地址https://www.toymoban.com/news/detail-610784.html
public interface TransactionManager {
long begin(); // 开启一个新事务
void commit(long xid); // 提交一个事务
void abort(long xid); // 取消一个事务
boolean isActive(long xid); // 查询一个事务的状态是否是正在进行的状态
boolean isCommitted(long xid); // 查询一个事务的状态是否是已提交
boolean isAborted(long xid); // 查询一个事务的状态是否是已取消
void close(); // 关闭TM
}
实现
常量定义
// XID文件头长度
static final int LEN_XID_HEADER_LENGTH = 8;
// 每个事务的占用长度
private static final int XID_FIELD_SIZE = 1;
// 事务的三种状态
private static final byte FIELD_TRAN_ACTIVE = 0;
private static final byte FIELD_TRAN_COMMITTED = 1;
private static final byte FIELD_TRAN_ABORTED = 2;
// 超级事务,永远为commited状态
public static final long SUPER_XID = 0;
// XID 文件后缀
static final String XID_SUFFIX = ".xid";
到了这里,关于我的项目准备(数据库篇)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!