【cfengDB】自己实现数据库第0节 ---整体介绍及事务管理层实现

LearnProj

内容管理

    • MySQL系统结构
      • 一条SQL执行流程
    • cfengDB整体结构
    • 事务管理TM模块
      • TID文件规则定义
      • 文件读写 -- NIO
      • RandomAccessFile、FileChannel、ByteBuffer
      • 接口实现
        • 文件合法检测
        • begin()
        • commit(tid)
        • rollback(tid)
        • tid文件创建


本文作为数工底层的项目CfengDB开始篇章,介绍开发缘由和实现思路


cfeng之前对数据库研究不深入,之前只是能够做到基本的SQL查询和基本的慢SQL优化,之前拿到数据库系统工程师证书还是只在业务上对于DB系统使用更深入,但是cfeng基于work的理解,当作为一个优秀的产品使用者之后,再来作为产品的开发者,二者是互相促进的, 现在信息时代的发展大数据量变得非常普遍,了解DB的设计开发对于我们的code是非常有帮助的,从SQLboy变成engineer

MySQL系统结构

工作中最常使用的DB系统应该就是MySQL了,当然随着信创生态,一些国产数据库也广泛普及,如何让我们的SQL更高效,查询更迅速,一致性问题等都是需要考虑的问题,MySQL最常见的几个概念应该就是索引,锁 + 事务MVCC了, 本文就不探究这些业务层面的实现了,这里主要介绍一下MySQL的架构。

【cfengDB】自己实现数据库第0节 ---整体介绍及事务管理层实现_第1张图片

这是网络上一个比较简化的结构图, 整体SQL的执行来说就4个部分:

  • 连接器: 管理相关MySQL客户端与服务端的连接同时会进行旋前的验证
  • 分析器: 词法分析和语法分析,也就是MySQL需要读懂整条SQL的意思
  • 优化器: 客户端传送的SQL可能非常臃肿繁杂,server端需要进行简单的优化
  • 执行器: MySQL读懂SQL含义并优化之后,开始执行,底层的存储引擎是插件式的,可供选择

一条SQL执行流程

上面的各个模块都是比较简化版的,这里再实例详述一遍。

【cfengDB】自己实现数据库第0节 ---整体介绍及事务管理层实现_第2张图片

后台应用和MySQL之间都维护了连接池,用于管理所有的连接,通过Connect就可以将待执行的SQL传送给MySQL服务端执行。

SQL语句到达MySQL服务端之后

  1. server的线程会将接收到的语句交给SQL接口(组件),该interface专门由于执行SQL语句
  2. SQL接口将语句传递给SQL解析器parser,进行词法语法分析,理解SQL语句的意思(从哪张表,什么过滤条件,提取哪些字段…)
  3. 之后将语句传送给查询优化器,选择最优的路径,达到最佳的性能(比如select x from student where id =1, 可以先查找所有的x再过滤,或者先过滤再取x)这类的路径选择就是优化器的工作
  4. 选择最佳路径后,再传递给执行器将SQL语句按逻辑执行(比如调用存储引擎的接口,获取user表的数据,判断,继续…)
  5. 调用存储引擎,执行,执行器会调用存储引擎完成 SQL的操作,存储引擎按照一定的步骤查询内存缓存数据,更新磁盘数据。

为了提高效率,MySQL中也存在缓存,如果SQL命中缓存,就不会再走流程,节约时间

cfengDB整体结构

就模块划分来说,MySQL是十分优秀的,cfengDB最多只能说是帮助us更加理解数据库这种产品的一种最简单的设计而已。

因为cfengDB需要完成的最基础的任务就是识别并正确执行SQL,因此1.0.0就只针对这一过程进行结构设计(其余的向连接池化,用户管理…都后续再设计)

cfengDB整体上也是分为前端和后端,前后端通过网络Socket同学,前端的职责就是读取用户的SQL并发送给后端server指向,输出返回的结果,后台和MySQL流程一样需要识别合法的SQL并执行

为了整体的可扩展性和实现的难度,采用分层和分治的思想进行模块划分,整体上划分为事务管理模块Transaction Manager, 数据管理模块Data Manager, 版本锁管理Version Manager,索引管理Index Manager,表管理Table Manager

最终需要实现的就是表管理Table Manager,通过表管理模块就可以提供server的相关服务,模块分层和依赖关系如下:【cfengDB】自己实现数据库第0节 ---整体介绍及事务管理层实现_第3张图片

  • 事务管理TM: 维护TID文件来维护事务的状态,提供接口供其他的模块来查询事务(参考的MySQL的MVCC)
  • 数据管理DM: 直接管理数据库DB文件和日志文件,需要分页管理DB文件并且需要缓存,同时需要管理日志文件以供故障回复,抽象DB文件类提供上层使用
  • 锁管理VM: 并发管理模块,基于2PL协议实现调度可串行化,利用MVCC实现隔离级别
  • 索引管理IM: 基于B+树实现索引
  • 表管理TBM: 整体上的表和字段管理,完成SQL解析和执行

事务管理TM模块

从模块层级来看,TM作为底层是最基础的部分,所以先从最基本的模块开始讲解:

TM主要是通过维护TID来维护事务的状态,提供接口供其他的模块查询状态

TID文件规则定义

cfengDB中每一个事务都会有一个TID进行表示,事务ID从1开始自增,不重复; 特殊规定 ----- TID为0表示无事务noneTransaction,代表事务不申请事务直接执行,该类型事务状态永远都是committed

TM中维护一个TID格式文件,记录Transaction的状态,其中cfengDB中规定事务的状态为三种:

  • 0 running: 正在执行,还没有结束
  • 1 commited: 已提交
  • 3 rollback: 撤销回滚

TID中,每一个事务有1字节空间保存状态,同时,TID头部还有8字节的数字,记录TID文件管理的事务的个数,那么事务tid在文件中的状态就存储在8 +(tid-1) 字节位置,【从0开始计数,并且TID为0代表none】

文件读写 – NIO

IO种类很多,有磁盘IO和网络IO, BIO、NIO、IO多路复用、AIO的概念大多用于网络IO — 用户程序从网络中获取数据socket

  1. 用户进程系统调用进入内核态
  2. OS等待远程客户端发送数据(TCP建立连接),OS从网卡设备获取数据,从Socket协议栈拷贝到内核缓冲区
  3. 把内核缓冲区的数据拷贝到用户缓冲区
  4. 用户进程获取到数据,继续执行

【cfengDB】自己实现数据库第0节 ---整体介绍及事务管理层实现_第4张图片

NIO相比BIO,AIO的区别就是:

步骤1,用户进程要进行IO了,是阻塞挂起,还是非阻塞

步骤3: 内核缓冲区数据拷贝到用户缓冲区,用户进程是阻塞还是非阻塞

BIO是同步阻塞,数据的读写都会阻塞在一个线程中

NIO是同步非阻塞,通过Selector监听不同的channel上的数据变化,当channel数据发生变化时,通知该线程进行读写操作,然后线程就会自己进行读写

AIO是异步,接收到客户端管道后,交给底层处理IO通信,自己本身做其余的事情

(同步和异步的意思是是否需要自己处理,而阻塞和非阻塞就是点餐后是否需要一直等着饭做好)

【步骤一只是通知(点餐),3就是真正开始读写】,1,3都阻塞,就是BIO,1不阻塞,3阻塞,就是NIO; 1,3都不阻塞,就是AIO

  • BIO:阻塞IO,包括常见的ServerSocket/Socket, accept(); //连接阻塞; InputStream/OutputStream; IO读写阻塞
  • NIO: new IO,noBlock; Selector复路器,缓冲区Buffer,ServerSocketChannel; IO未就绪的时候都是非阻塞的; 通过Linux提供的epoll + Selector + Channel实现多路复用【一个线程处理N个连接】

NIO核心组件:

  • Channel: 通道,NIO数据源头/目的地,缓冲区Buffer的唯一接口: 双向读写【可读出和写入】、数据in/out总是先到缓冲区; {文件IO: FileChannel 从文件中读取数据, DataChannel: UDP读写网络数据,SocketChannel: TCP读写 ServerSocketChannel:服务端监听新TCP连接,每进入一个创建SocketChannel}
  • Buffer: 缓冲区, NIO数据读写的中转地【一块连续内存块】,Buffer和传入的数组共享相同的数据存储区域; 可以简单理解两指针指向相同的块,作为数据缓存, 适用于除了boolean之外所有基本类型, 比如ByteBuffer、IntBuffer… allocate()动态分配yi

文件读写采用的是NIO方式,(NIO非阻塞,支持基于通道的IO操作,区别传统的Input/OutputStream),使用FileChannel,创建TxManager后,需要对TID文件校验,保证TID文件合法

校验方式: 文件头的8byte数字推断文件的理论长度,如果不同就认为不合法,不合法的,直接panic强制停机【对于无法回复的错误只能先粗暴停机】

//事务tid在tid文件中的位置
LEN_TID_HEADER_LEN + (tid - 1)* TID_FIELD_SIZE  [头长度 + (序号 -1* 每个tid长度]

所有的文件操作,执行后立刻刷入文件,防止崩溃后丢失, 使用NIO的fileChannel的force方法,和BIO的flush类似

【为了方便,java8开始支持在interface中定义静态方法体,static的方法: 调用时直接采用接口名称调用即可】

create()和open() 创建TID文件,从TID文件创建TransactionManager,创建XID文件时需要写一个空的TID的header,设置tidCounter为0(数量为0),否则后续会不合法

RandomAccessFile、FileChannel、ByteBuffer

IO的底层涉及的概念: 缓冲区操作内核空间与用户空间虚拟内存分页技术

在这里插入图片描述

磁盘操作属于内存管理,是OS系统级功能,需要在内核态执行,所以读取的数据是到内核缓冲区,之后再拷贝到用户态缓冲区

java中的IO常见操作read()和write()完成的作用: 数据在内核缓冲区和用户缓冲区进行交换, 传统IO是面向流(按字节读取),而NIO则是面向Buffer的

在java的内存结构中, 直接内存不受JVM管理, 而堆heap是受JVM用户进程管理; NIO中的Selector可以监听channel,【channel是数据源的抽象】,只有当某个channel准备好之后,线程才会阻塞去取数据操作,而不需要一直等待【BIO从最开始准备数据就开始阻塞】

  • RandomAccessFile允许随机读写文件,也就是可以按照设定的位置开始读取(部分读取); 访问模式包括: r: 只读; rw: 读写; rws:读写,每次文件内容和元数据修改都同步磁盘; rwd: 读写,每次文件内容同步磁盘
  • FileChannel: 通道, 通过文件位置指定开始读写, instance可以通过RandomAccessFile.getChannel()获得, 读写buffer后会自动向后移动pos
  • ByteBuffer: 缓冲区,对byte数组的一种封装, position表示当前的小标,limit结束标记,capacity就是底层的byte数组的容量, 可以通过wrap或者allocate进行内存的分配,通过getXXX方法可以进行类型转换; 每次进行write的时候可以进行force来避免数据丢失

在这里为了方便进行各种类型的转换, 实现了一个byte[] 和 类型的转换工具类:

	public static byte[] long2Byte(long value) {
        return ByteBuffer.allocate(Long.SIZE/Byte.SIZE).putLong(value).array();
    }

    public static long parseLong(byte[] buf) {
        ByteBuffer buffer = ByteBuffer.wrap(buf,0,8);
        return buffer.getLong();
    }

接口实现

首先TM中都是对于事务的操作,NO Transaction使用TID为0, TID的状态变化和事务的新增都是通过操作TID文件实现的, 所以TM的实现就是对于TID文件的创建和修改

//事务管理大多是对tid文件的管理,所以需要nio的RandomAccessFile和相关的channel
    private RandomAccessFile randomAccessFile;
    private FileChannel fileChannel;
    private long tidCounter; //tid计数器
    private Lock counterLock; //使用Lock锁保证线程安全

再进行TM初始化的时候就会进行TIdCounter的检查,检查的方式就是先检查头的长度,之后再整体计算文件长度

文件合法检测

getTidPosition就可以获取tid对应事务的状态字节开始的位置, 而实际的长度就需要再加上一个TID状态的长度 再和 fileLen相比即可

fileLen = randomAccessFile.length(); //文件实际长度

if(fileLen < TransactionConstant.LEN_TID_HEADER_LEN)
    
this.tidCounter = ByteBufferParser.parseLong(buffer.array()); //tid就是头8个字节表示的数据
long end = getTidPosition(this.tidCounter) + TransactionConstant.TID_FIELD_SIZE; //getTidPosition相当于取得的是tid该状态byte开始的位置,结束位置需要
if(end != fileLen)
    
	private long getTidPosition(long tid) {
        return  TransactionConstant.LEN_TID_HEADER_LEN + (tid - 1) * TransactionConstant.TID_FIELD_SIZE;
    }

而修改事务TID的状态的方法也很简单, 直接获取到TID的开始位置,之后将待写入的byte进行wrap为Buffer,写入再force即可

	long offset = getTidPosition(tid);
        byte[] tmp = new byte[TransactionConstant.TID_FIELD_SIZE];
        tmp[0] = status; //修改状态为status
        ByteBuffer buffer = ByteBuffer.wrap(tmp);
        try {
            fileChannel.position(offset);
            fileChannel.write(buffer);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        //每次写buffer时候强制刷新一下,避免丢失
        try {
            fileChannel.force(false);

而检查状态就是读取对应位置的Buffer通过buffer.array()获取之后再进行比较即可

对于定义的相关的接口对于事务的相关操作,就是修改TID文件中的TID的状态; eg:

begin()

开始一个新的事务,所以首先tid ++,之后将该新事务的状态设置为运行RUNNING, 再将头部tidCounter数量加一写入头部

因为需要修改类属性,在并发状态下存在安全问题,所以使用ReentrantLock进行加锁修改

	tidCounter ++;
        //修改数量
        ByteBuffer buffer = ByteBuffer.wrap(ByteBufferParser.long2Byte(tidCounter));
        try {
            fileChannel.position(0);
            fileChannel.write(buffer);
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        //每次写buffer时候强制刷新一下,避免丢失
        try {
            fileChannel.force(false);
            
            
           this.counterLock.lock();
        try {
            long tid = tidCounter + 1; //当前事务
            updateStatus(tid, TransactionConstant.FIELD_TRAN_RUNNING);  //事务激活
            incrTidCounter(); //头部size增加
            return tid;
        } finally {
            this.counterLock.unlock();
        }

commit(tid)

提交事务,有了之前的操作,这里就很好实现 — 直接修改事务TID文件中tid的状态为commited即可

rollback(tid)

和commit同理, 直接修改TID的状态(文件中)

isXXXX(tid)

状态检查,直接buffer读取对应tid位置的事务的状态再进行比较即可

tid文件创建

tid的文件创建可以直接利用File即可,因为TM的类属性有RandomAccessFile和FileChannel,所以再初始化一下

	//创建文件
        File file = new File(path + TransactionConstant.TID_SUFFIX);
        try {
            if(!file.createNewFile()) {
                //文件创建失败已存在,直接粗暴处理,因为不能进行后续操作了
                FaultHandler.forcedStop(new DatabaseException(ErrorCode.FILE_EXISTS));
            }
        }catch (Exception e) {
            FaultHandler.forcedStop(e);
        }
        //查看文件是否可读写,直接调用File的接口
        if(!file.canRead() || !file.canWrite()) {
            FaultHandler.forcedStop(new DatabaseException(ErrorCode.FILE_CANNOT_READ_OR_WRITE));
        }
        //使用NIO进行文件读写
        FileChannel fc = null;
        RandomAccessFile raf = null; //与简单IO不同,可以调转到文件任何位置进行IO,访问文件部分内容
        try {
            raf = new RandomAccessFile(file,"rw"); //将file转为随机读写File,文件权限为rw
            fc = raf.getChannel(); //利用randomAccessFile创建channel通道
        } catch (FileNotFoundException e) {
            FaultHandler.forcedStop(e);
        }

        //利用buffer和channel进行文件读写
        //写空的文件头,将byte[]包装为buffer
        ByteBuffer buf = ByteBuffer.wrap(new byte[TransactionConstant.LEN_TID_HEADER_LEN]);
        try {
            fc.position(0); //RandomAccessFile定位到0处开始
            fc.write(buf); //缓冲区写入
        } catch (IOException e) {
            FaultHandler.forcedStop(e);
        }
        return new TransactionManagerImpl(raf, fc);
    }

整个TM的实现都是依靠的tid文件的操作,主要是定义好规则,实现不难,这里的两重点技术: 可重入锁保证线程安全 + NIO方式进行文件操作提升性能

你可能感兴趣的:(数据库养成,数据库,adb,android)