当前位置:   article > 正文

数据库设计思想深究----Mysql(图文)_mysql设计思想

mysql设计思想

在探索开始前,我们先试想一个问题:存储为什么要分缓存与磁盘?

一、为什么要区分缓存与磁盘?

我们利用高级语言,编写逻辑,最终被解释为指令集合,委托CPU去处理。

根据计算机组成原理,我们知道CPU在执行指令时,操作的对象就是存储单元的存储内容,而这部分内容根据数字电路原理,我们可以知道就是信号灯的亮与不亮所代表的二元状态。

而获取这种状态的方法,就是通过不断提供能量不断的刷新,不断观察得知。刷新的速度是无上限的,与材料和方式(还有光路)有关。因此有成本的限制。

那我们肯定希望处理逻辑的速度越快越好啊,当然希望使用最快的速度实现I/O。

于是最初的计算机,便使用这样的方案:运算器+最快的存储单元(寄存器)

  

随着需求数据的不断膨胀,不断扩大寄存器也意味着不断扩大的成本。有人发现了一个问题:运算器一次指令并不需要这么多数据。于是提出了内存的概念。内存相比于寄存器,使用更廉价的材料(当然这部分存储单元的I/O速度也将对降低),将CPU暂时不需要的数据存放在内存中,指令需要的数据缓存到寄存器即可:

随着技术的不断提升,运算器与寄存器之间的技术绑定越来越密切,于是将它们绑定作为中央处理器(CPU),内存作为相对廉价的存储单元则分离出了,并实现模块化:

 随着计算机模块的成熟化,工程型需求逐渐依赖计算机,此时数据量的膨胀,使得多加几根内存条无法满足,而且人们发现,海量的数据并不是时刻都被需要快速访问的,并且工程周期的长度让我们有了持久化的需求(因为人的缓存记忆有限),按需使用的设计方案更加合理。于是人们利用一种相对于内存廉价的存储单元(磁盘),用于存放所有数据,并在需要使用时,读磁盘写内存,CPU进而开始处理。磁盘的出现,也代表着静态资源定义的出现(并不需要每次指令都刷新状态的资源):

自此,存储单元已经发展到了持久化的概念,此后技术不断发展,但都会将持久化的数据容器称作:磁盘/硬盘。

我们不难发现,不断的技术更迭,不断的结构进化都是为了解决一个矛盾:

成本 vs 速度

完全可以暴力,在需求范围内全部使用寄存器级别的材料作为存储单元,利用成本提升性能。也可以完全全部使用磁盘级别的材料作为存储单元,降低成本降低性能。

这种矛盾的问题,实际上是一种线性规划问题,求出最优解的问题。因此在设计一台计算机设备时,要根据数据,去计算最适合的寄存器:缓存:磁盘的比例。因此,我们为了解耦成本与速度的依赖,设计了缓存与磁盘。

二、为什么需要数据库?

原因很简单:我们需要高效的管理数据读写。

我们通常会将存储单元设计成线性结构,那我们访问数据,如果没有策略在,那就是从头到尾的遍历。显然这样是一种很简单但又低效的方式。

这里举一个贯穿全文的例子:假设存储器的容量为1MB=8388608 bit,访问1bit所需时间为0.01ms,那么有一个数据在末尾,就需要8388608*0.01ms = 83886.08ms = 83.89 秒才能被加载到内存。

因此我们需要中间件来管理这些数据,而这些中间件通常称作 数据库 DataBase

通过上文的了解,我们可以知晓,是不是可以设计三类数据库:

寄存器数据库、缓存数据库、磁盘数据库

由于寄存器直接被运算器访问,容量十分小,几乎不需要管理策略。因此寄存器数据库暂时没有需求。

然而,发展自今日,内存和磁盘容量已经十分庞大。对于缓存数据库、磁盘数据库需求也十分迫切。于是位于寄存器与内存之间的面向内存的数据库,位于内存与磁盘之间的面向硬盘的数据库便为我们熟知。

我们先探索常见的面向硬盘的数据库。

三、面向硬盘的数据库

实际上这部分是最早发展的内容,因为硬盘具有持久数据特性,没有刷新策略,容量还是最大的存储器,因此对于快速访问硬盘的需求十分迫切。

我们对于数据的需求,无非CRUD(Create Remove Update Delete)

而数据库作为中间件,就是帮我们把CRUD的策略给处理了。

我们这里通过Mysql来进一步探索其原理。

 我们可以看到连接数据库需要Host、Port和Url,采用的是Client/Server模式。用户本可以通过I/O直接读取为流,显然数据库在存储单元上制造了一层端,端内里的数据保护在里面。这与B/S模式相同类似。

既然是端到端的数据传输,那么就有传输协议,C/S模式下的数据库常用的都是TCP/IP协议,Mysql也不例外。

3.1 传输层协议 TCP与UDP

顾名思义,传输层协议就是客户端与服务端之间传输数据的约定。

常见的约定有UCP与TCP

3.1.1 UDP(用户报文协议)

有个很简单的理解方式,我给你丢个沙包,爱要不要

UDP是一种不可靠的协议,它传输数据前不需要通过连接确定状态。

举个例子,如果C端与S端采用UDP协议传输数据,只要知道S端的IP地址,就把IP数据包发给你。如果S端处于可接收的状态,就接收到IP数据包开始根据应用层协议(通常为HTTP协议)拆包。如果是非接收状态,那这个IP数据包(实际上就是一串信号)到达IP地址后,就消失了。

至于为什么不可靠,快递员把快递丢门口,说送到了,然后没人拿,快递就消失了。

3.1.2 TCP协议

TCP是一种面向连接的、可靠的传输层协议。

也就是UDP协议的靠谱版。

这份靠谱靠什么实现呢?

就是著名的三次握手。

三次握手

我们首先明确一个事情:不论是客户端还是服务端,都有一个二元状态:上线与离线

第一次握手:在吗?

第二次握手:(声音达到接收端所需要的时间后)在的,你还在吗?

第三次握手:(声音达到接收端所需要的时间后)在呢,那我们聊吧

这就是最基本的确认两者处于上线状态可以通信的方式。

为什么需要3次?因为信息到达对方是需要一个时间T的。这个时间T内,均可能发生一方离线的可能性,所以必须是3次。

Q1:那怎么可以保证信息一定会被对方接受呢?

A1:TCP/IP协议实际上采用了一个很直接的方法,我设定一个时间t,时间t内没收到响应,我就把上次的提问作废,再问你一次,直到回复我。

Q2:你怎么保证是我回复的?

A2:看回复的ip是不是和我问的ip一致不就知道了。

Q3:那你怎么保证我是回复你这句话的?

A3:这个有点像混合加密,比如我给这次的提问“在吗”编号x,

我们必须遵循一个协议:收到对方的消息编号后要在编号上+1

因为协议,你收到的我的消息编号后,需要在编号上+1(x+1)。你为了确保我在T时间内没有离线,你需要问我“还在吗?”,你为了确保我是回复你这个问题的,你也要制作你自己的消息编号y发给我(第二次握手

我收到消息后,根据协议,先你的消息编号+1(y+1),把消息里的附件编号+1,与我等待回复的消息编号集合匹配,匹配到等待回应的那个消息的编号,我就确定你是回复我这句话的,我就只把你的消息编号作为附近编号发给你,你接受到后。发现只有附件编号,匹配等待回应的那个消息的编号,便开始了无需确认的聊天。(第三次握手

Q4:你怎么区分,需不需要再次确认的?

A4: 通过判断消息编号有无,附件编号有无,就可以知晓在第几阶段了。

上述的过程中,比较口语。实际上我们每次说的话都以一个SYN(读:sin)包发送给对方的。这个SYN包里有:seq(同步序列码)+ask(确认符号) -> 消息编号+附件编号

我们对发送者与接收者的状态进行了定义:

Sender: Nan->SYN_SEND->Established

Reciver: Nan->SYN_RECIVE->Established

同时,提问失败后不断提问的机制称作:超时重传机制

三次握手

以上便介绍完了TCP/IP协议靠谱的理由以及它的一个机制:超时重传机制

实际上,它还有一个流量控制机制。这个机制就是控制发送的频率,让对方有一定时间去处理。

小结:UDP是不可靠的传输协议,TCP协议是可靠的传输协议。可以发现,如果双端都在线,TCP因为三次握手的机制,传输数据的速度上会稍逊一筹。

所以这里有一处矛盾:稳定连接 vs 传输速度 

如何选择就需要考虑数据是否需要稳定等。

3.1.2 应用层协议 HTTP

HTTP协议解释起来很简单,基于2.1.1介绍的传输层协议,传输层传输的都是二进制码流。如果发送端与接收端没有一个封装开箱的协议,那么将无法识别出有效的内容。因此需要HTTP协议来做传输前后的封装开箱。

HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并返回

HTTP定义了4个客户端与服务器交互的方法:

  • 增  Put
  • 删 Delete
  • 改 Post

POST一般用于更新资源信息,会修改资源内容。

  • 查 Get

GET一般用于获取/查询资源信息,不对资源产生副作用。

为什么没有解释Put与Delete呢?因为SpringWeb将增删改都归于了Post范畴

自此介绍完了客户端与数据舒服端的数据是如何交换的。再回到这个图,我们可以看到除了身份User与凭据Password,剩下的就是Database。

3.2 Database结构

Database俗称DB,数据库,其结构如下。

 

可以抽象为:

 可以发现:

 磁盘上的线性数据被数据库有计划(Schema)地管理起来了。

这个计划就是用Table管理静态数据。

值得一提的是,存储空间写入数据时,是寻找连续的存储地址,就写入,所以整个过程是十分随机的,即table中的数据在实际存储单元上是随机分布的,如果我们全盘遍历,查找一个table的数据将十分低效,因此table是作为一种映射的集合(序号->存储地址)。以此使得相似的数据内聚。

我们通过观察Mysql建表的窗口可以发现表内部的管理模式:

这是可视化工具将常用的设置在了界面, 实际上Table中只存在这么些东西:

  • 3.2.1 Columns 列

 Table结构是一个二维数组,它将一个对象线性的映射(a->b)在了Table中,所以列就是对象的属性(b),而行序号是线性映射的key(a)。

  • 3.2.2 Constraint 约束

约束是指对表中数据的一种限制条件。

能够帮助数据库管理员(ServerObjs)更好地管理数据库,并且能够确保数据库中数据的正确性和有效性。

常见的约束5个:

约束类型作用
主键约束 Primary Key

使该Colunm成为唯一地标识表中的每一条记录的标志,其唯一索引保证了这个约束的实现。

为了保证(对象->记录)映射有意义,必须与非空约束组合使用。

Mysql会强制这一行为。主键约束与其唯一索引共生共灭。

非空约束 Not Null使该Column不能为NULL,否则抛出数据库异常。
默认约束 Default使该Column填入默认值,通常使用在创建时间等属性,插入时字段填default即可。如果插入时非要自己给一个值,则必须与默认值相同,否则会报数据库一致性异常。
自增约束 AutoIncreament使该Column填入数据库主存计数器的值。(Oracle中则可通过全局变量:序列完成)
唯一约束 Unique使Column不能出现重复的值。与主键约束完全不同,与其唯一索引共生共灭。

  • 3.2.3 Indexes 索引

索引是记录的检索映射。

 可以通过show index from table查看索引的相关信息,可以发现通常由B树结构组织:

通俗的讲:索引实现了一种类似字典检索目录的快速检索的功能

Mysql提供了两种常见的索引有:

索引作用

主B+树索引

(聚集索引)

生成对应Colunm的B+Tree,为优化器提供BTreeSearch选择。其中非叶子结点只存储key,叶子结点由双向链互通,且缓存对应记录。

次级索引

(普通索引)

生成对应Colunm的BTree,为优化器提供BTreeSearch选择。其中非叶子结点存储value,叶子结点由双向链互通,且缓存对应主键key。

下面分别介绍一下实现原理:

B+树 实现原理(B树基于此就很容易理解了)

了解B+树前,需要了解2个数据结构:AVL树(二叉平衡查找树)与 跳表

AVL详情在该博文有详细介绍: 

Java设计思想深究----集合框架数学原理(图文)_kevinmeanscool的博客-CSDN博客

AVL树结构

我们知道,AVl通过旋转不断进行深度约束,避免查找树退化为链表,以此来保持查找的时间复杂度O(logn)。

跳表是利用双向链表的直接寻址特性,在有序链表上加入多级索引:

跳表结构

这样我们通过最上级索引,可以快速确定查找值所在的区间。 

B树结合上述优点提供了一种不同的思路:

B树结构

 将AVL树始终保持为完美AVL树,且叶子节点双向互通。

如果我们要找15 ~ 27 这个区间的数只要先找到 15 这个节点(时间复杂度 logn = 3 次)再从前往后遍历直到 27 这个节点即可。

它的速度有多快呢?假如1亿个数据 log2 n = 1亿 , n 约= 27。最多只需要27次寻址便可定位。当我们查询的数据高内聚时,速度显而易见的快。

但是有一个问题,树结构需要我们将每个节点缓存,1亿个数据的缓存显然是一笔巨大的开销,不现实。根据我们1.1中提到的,我们可以用更廉价的存储单元来缓存,内存不行,就来磁盘。也就是说,我们用计算特性补偿存储特性,这个计算就是来自于I/O的开销。

我们都知道,内存的读取速度远小于磁盘的I/O开销,即使解决了存储容量的问题,时间的问题又浮现出来,等1天?10天?不太好吧,有没有更好的办法?

我们回到B树,不难发现,定位的次数与深度(depth)有关 : 次数 = 2^depth 

因为B树叶子节点互通的特性,可以期望于 用线性搜索的开销补偿 BTS(BTreeSearch)的开销

 一下子减少了2次磁盘的I/O。假如当我们的叶子节点是100,那么 100^n =  1亿,n=4,也就是说5次I/O就可以定位到模糊度为100的区间。最坏情况也就是5次I/O开销+100次线性查找。

当然,这个叶子叉不可能无限制的增加,比如模糊度定义为1亿,非叶子结点就逐渐退化为链表,时间复杂度也退化道了O(n)。

磁盘每次读取都会预读,会提前将连续的数据读入内存中,这样就避免了多次 IO,这就是计算机中有名的局部性原理,即我用到一块数据,很大可能这块数据附近的数据也会被用到,干脆一起加载,省得多次 IO 拖慢速度。这个连续数据有多大呢?必须是操作系统页大小的整数倍,这个连续数据就是 MySQL 的页,默认值为 16 KB,也就是说对于 B+ 树的节点,最好设置成磁盘页的大小(16 KB),这样一个 B+ 树上的节点就只会有一次 IO 读。

  1. //查询磁盘页大小
  2. show GLOBAL STATUS LIKE 'INNODB_page_size';

B+tree 的叶子节点包含所有索引数据,在非叶子节点不存储数据,只存储索引,从而来组成一颗 B+tree。

图片

除此之外,MySQL没有Hash索引,但很有用,这里提供一种伪Hash索引 实现原理 

哈希索引基本散列表实现,散列表(也称哈希表)是根据关键码值(Key value)而直接进行访问的数据结构,它让码值经过哈希函数的转换映射到散列表对应的位置上,查找效率非常高。

图片

对于每一行数据,存储引擎都会对所有的索引列(上图中的 name 列)计算一个哈希码(上图散列表的位置),散列表里的每个元素指向数据行的指针,由于索引自身只存储对应的哈希值,所以索引的结构十分紧凑,这让哈希索引查找速度非常快。这是利用了HashMap的原理,详情见此文:Java设计思想深究----集合框架数学原理(图文)_kevinmeanscool的博客-CSDN博客

innoDB 引擎本身是不支持显式创建哈希索引,可以在 B+ 树的基础上创建一个自定义哈希索引

适用的场景为较长的字符串,比如URL。我们可以新增一个字段来存储URL的哈希值,通过CRC32(URL)计算出哈希值。然后在这个整型字段上建立B+树索引,制造间接哈希索引。

并在SQl条件中引入索引Colunm:

SELECT id FROM url WHERE url = $URL  AND url_crc = CRC32($URL)

小结:索引的确可以提升效率,但也会对Table增删改造成一定的性能困扰,因为具有一定概率需要重新组织索引结构,这是一笔开销。因此,如果表数据查找次数明显大于修改次数,那么使用索引的确是个不错的提升性能的选择。

  • 3.2.4 以及需要在应用层控制的属性

权限、外键等根据阿里设计规范都应在应用层实现控制:

自此,基本结束了对面向DataBase的基本结构,下面介绍客户端如何与服务端交互。

3.3 一条SQL的冒险

我们都知道SQL是客户端对于数据库服务端的数据请求。

那么这个请求,从我们编写经历了怎样的历程呢?下面还是以Mysql为例。

3.3.1 委托Mysql驱动建立Connection

MySQL C/S端,我们都知道双端通信的前提是建立连接Connection。这个连接实际上是由TCP/IP运输层协议规范,因此我们的客户端与数据库服务端要开始数据传输,首先需要双端处于Established状态。这就是涉及到 3.1.2中提到的三次握手了。这些逻辑相同的步骤,MySQL驱动都会自动帮我们完成。

 网络中的连接都是由线程来处理的,所谓网络连接说白了就是一次请求,每次请求都会有相应的线程去处理的。每个请求都会接入MySQL驱动,委托MySQL驱动建立Connnection,直到事务结束,回收资源。

当然,Mysql面临的线程请求并不会串行,大部分情况是多线程的。因此MySQL内部采用类似线程池的连接池来管理(请求->Connection)的封装:

 对于MySQL驱动提出的同步握手请求,数据库也对此提供了并发的管理,称作:数据库连接池:

通过(业务请求->客户端连接)的阻塞队列管理对应(客户端连接->数据库服务端连接)

在连接建立成功后,便可以进行业务操作,下面介绍数据库服务端如何处理SQL。

3.3.2  SQL的处理过程

MySQL 中处理请求的线程在获取到请求以后获取 SQL 语句去交给 SQL 接口去处理。

 下一步,SQL将被SQL解析器解析为MySQL内部的执行计划

SQL优化器接收到执行计划后,将根据最小成本规则优化执行计划,这里的成本主要包括两个方面, IO 成本CPU 成本

  • IO 成本

        读取磁盘页内容,写入内存的开销。通常,MySQL每次读取大小为1个磁盘页。

  • CPU 成本

        内容写入内存后,将内容以执行计划中的条件(条件包括where、orderBy等)组织。

MySQL 优化器 会计算 「IO 成本 + CPU」 成本最小的方案作为执行计划。执行计划通常为横向树形结构。

ps:MySQL 优化器优化执行计划的过程委托存储引擎API运算出。

在确定执行计划后,SQL执行器回根据执行计划,这个过程是:最右最上先执行

ps:MySQL 执行器执行执行计划的过程也是委托存储引擎API

整个过程图示如下:

数据流图为:

 

3.3.3 MySQL存储引擎 InnoDB

 MySQL默认的存储引擎为InnoDB。

存储引擎执行SQL所需的数据段,要么在内存中、要么在磁盘中。如果大部分在磁盘中,每步计划的执行,都需要对磁盘进行随机I/O扫描,将会造成巨大的额外开销。因此MySQL会将本次计划步骤所需的数据加载到内存中。加载的内存就是InnoDB中十分重要的一个组件:BufferPool 缓冲池

顾名思义,缓冲池其实就是类似  Redis  一样的作用,起到一个缓存的作用。

MySQL 的数据最终是存储在磁盘中的,如果没有这个 Buffer Pool  那么我们每次的数据库请求都会磁盘中查找,这样必然会存在 IO 操作,这是无法接受的。

因此,InnoDB处理SQL时:

->判断所需的记录是否缓存

->没有缓存的情况下,通过I/O加载到BufferPool

->BufferPool的记录组加排他锁,确保计划执行的原子性、有序性、可见性。

同时,很重要的一点是:本次计划所需的数据段,都会写入undo Log

3.3.4 支持事务的可靠方案 Undo Log

InnoDB引擎最大的特点就是:支持事务 Transaction。

所谓事务是指业务的集合,也是SQL的集合,这代表着一个业务间有很强的顺序依赖时,从开始到结束这段时间所发生的业务的集合即为事务,因此事务中途发生异常,回滚事务是十分重要的。

回滚事务的实现依赖Undo Log:

 ->当计划被执行前,将计划所需的数据段加锁后,将数据执行前的值写入到Undo Log

->如果事务中途发生异常,回滚rollback:将Undo Log中对应的数据写回内存中。

->如果事务无异常,执行计划

Undo日志是内存日志,并不会持久化

这里的计划便是经典的:CRUD

其中增、删、查都不会修改BufferPool的数据,只有更新会将BufferPool更新。这时就会导致BufferPool与数据库磁盘记录出现数据不一致情况。

当然,读者可能在想,我把更新完的值,刷新回主要存储器(磁盘),即commit事务,反正BufferPool已经上锁,保证了计划的原子性、有序性与可见性。但实际上,内存上的存储单元刷新回磁盘也需要一次I/O,而这段时间并不是时刻,期间如果发生Mysql服务端宕机()的情况,内存便会清空,即缓存丢失。重新启动的MySQL会认为本次事务失败(因为tanscation也不见了),磁盘页数据不受影响。客户端收到异常提醒后,在服务恢复后依然会发起请求。

InnoDB对此提供了一种方案RedoLog,用以事务提交后缓存丢失的。

3.3.4 数据恢复方案 Redo Log

Redo Log会记录数据页修改后的值,不是某一行或某几行修改成怎样怎样

redo log包括两部分:

一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;

二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

BufferPool中更新但未刷新回磁盘的数据页称为脏日志(dirty log)。innodb不仅仅只会在有commit动作后才会刷日志到磁盘,会定期redo log buffer刷新到redo log file。

在事务commit后,先将redo log刷新到redo log file中。

如果提交事务后,发生宕机,bufferPool磁盘页没来得及刷新到磁盘页,则会在重启MySQL服务后,将最新的redo log file缓存到内存中,并刷新回磁盘片。确保事务提交后返回结果与数据库结果保持一致。

自此介绍完了一条SQL的完整执行过程。

以及介绍完了面向硬盘的关系型数据库基本原理。

四、Java接入数据库的方式---- JDBC API

JDBC API 是Java为开发者提供一套数据库访问接口。

JDBC中的核心类有:DriverManager(驱动管理员)、Connection(连接)、Statement(声明),和ResultSet(结果集)

执行流程:

  • 连接数据源,如:数据库。

JDBC通过反射机制,创建一个指定的Driver对象。

  1. Class.forName(driverClass)
  2. //1.加载驱动程序
  3. Class.forName("com.mysql.jdbc.Driver");

我们分析代码,这里反射的是MySQL.Driver类,此时JVM会将这个类以及其继承的父类缓存至元空间MetaSpace,

通过阅读其父类,发现具有一个静态代码块:

  1. static {
  2. try {
  3. DriverManager.registerDriver(new Driver());
  4. } catch (SQLException var1) {
  5. throw new RuntimeException("Can't register driver!");
  6. }
  7. }

根据JVM的类加载机制,在准备阶段会初始化静态块,于是便执行了registerDriver方法,执行这个方法会注册一个空Driver对象到DriverManager.registeredDrivers:

  1. public static synchronized void registerDriver(java.sql.Driver driver,
  2. DriverAction da)
  3. throws SQLException {
  4. /* Register the driver if it has not already been added to our list */
  5. if(driver != null) {
  6. registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
  7. } else {
  8. // This is for compatibility with the original DriverManager
  9. throw new NullPointerException();
  10. }
  11. println("registerDriver: " + driver);
  12. }

看起来Driver是空的?其实并不是,在调用 registerDriver方法前,需要加载registerDriver方法所属的类DriverManage,而DriverManager也具有一个静态代码块,在准备阶段初始化:

  1. static {
  2. loadInitialDrivers();
  3. println("JDBC DriverManager initialized");
  4. }

进入驱动初始化加载的方法后,发现调用了AccessController.doPrivileged(),首先加载了System.getProperty("jdbc.drivers"),系统全局设置的驱动集合:

  1. try {
  2. drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
  3. public String run() {
  4. return System.getProperty("jdbc.drivers");
  5. }
  6. });
  7. } catch (Exception ex) {
  8. drivers = null;
  9. }

继续,又进行了一次加载,利用ServiceLoader将实现Driver接口的实体类装入,这一步是根据services映射完成的:

  1. AccessController.doPrivileged(new PrivilegedAction<Void>() {
  2. public Void run() {
  3. ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  4. Iterator<Driver> driversIterator = loadedDrivers.iterator();
  5. try{
  6. while(driversIterator.hasNext()) {
  7. driversIterator.next();
  8. }
  9. } catch(Throwable t) {
  10. // Do nothing
  11. }
  12. return null;
  13. }
  14. });

小结:JDBC通过反射动态装载数据库驱动 

  • 获取连接

Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);

读取源码可以发现,DriverManager委托了数据库驱动进行connect: 

  • 为数据库传递查询和更新指令。

  1. //用于执行静态SQL语句并返回其生成的结果的对象
  2. Statement stmt = conn.createStatement();

  • 处理数据库响应并返回的结果。

ResultSet rs = stmt.executeQuery("SELECT user_name, age FROM imooc_goddess");

通过Statement对象执行SQL语句时,需要将SQL语句发送给DBMS 数据库 管理 系统 (Database Management System)。

由DBMS进行解析、优化、再执行。 

如果需要反复利用一段SQL,采取预编译可以提高性能:

PreparedStatement ptmt = conn.prepareStatement(sql); //预编译SQL,减少sql执行

预编译即在执行前,先进行解析、优化过程,将执行计划缓存在存储引擎中,等多次执行时便无需再次解析、优化,只需执行。复用执行计划来提高性能。

还有一种基于预编译的更为快速的方式:批处理

批处理的原理:DBMS动态拼接SQL

  1. String sql = "insert Table(column1,column2) value (?,?)";
  2. PreparedStatement ptmt = conn.prepareStatement(sql); //预编译SQL,减少sql执行
  3. //通配符替换
  4. ptmt.setInt(1);
  5. ptmt.setString("1");
  6. ...
  7. pstmt.addBatch();
  8. //通配符替换
  9. ptmt.setInt(2);
  10. ptmt.setString("2");
  11. ...
  12. pstmt.addBatch();
  13. pstmt.executeBatch()

此时缓存中会拼接出一个SQL集合的SQL:

  1. insert Table(column1,column2) value (1,"1");
  2. insert Table(column1,column2) value (2,"2");

其更为高效的原因在于整个过程利用预编译与缓存,只进行了1次事务,即至进行了1次通信,减少了获取连接池线程的次数,因此相比于单SQL预编译提交执行更快。

当然,并不是说批处理就一定是最好的选择,以上3种方式各有利弊。

1次事务,只执行1次SQL, 选用Statement即可,减少不必要的准备开销。

1次事务,出现复用SQL,选择PreparedStatement,减少重复解析优化的开销,内存几乎无压力。

1次事务,1次执行批量SQL,选择PreparedStatement,减少重复解析优化的开销,内存有压力,存在OOM风险。

以上过程,读者可能发现了,存在着程序化重复的操作,即:

业务请求->装载数据库驱动->建立连接->创建声明->执行SQL->返回结果。

很适合用框架实现,MyBatis便应运而生。

五、MyBatis框架

节点 四 的最后我们提到了Mybatis将作为框架将一个业务请求响应到数据库服务端。那么这个过程是怎样的?

5.1 MyBatis启动

 从上图可以清晰的看到MyBatis有3个核心的组件:SqlSessionFactoryBuilder、XML ConfigBuilder、XML MapperBuilder

SqlSessionFactoryBuilder,顾名思义,是SqlSessionFactory的构造器。

通过源码我们可以发现,构造器可以通过多种方式实例化SqlSessionFactory。

  1. // 传递配置文件的输入流对象
  2. public SqlSessionFactory build(InputStream inputStream) {
  3. return build(inputStream, null, null);
  4. }
  5. // 通过XMLConfigBuilder工具类解析xml配置
  6. public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  7. ...
  8. }
  9. // 通过Configuration实例构建SqlSessionFactory对象
  10. public SqlSessionFactory build(Configuration config) {
  11. return new DefaultSqlSessionFactory(config);
  12. }

无论何种方式,最终都会实例化new DefaultSqlSessionFactory(config)。所以之间的过程,均是为了初始化config而做。

->过程中会委托XMLConfigBuilder去parse输入流,并返回Configuration。

->解析的过程中便包括mappaerElements,去解析出SQL静态映射,并缓存至Configuration中:

configuration.addMappers(mapperPackage);

进一步阅读 SqlSessionFactory实体类,发现一但实例化,SqlSessionFactory便会实例化一个常量Configuration对象,而Configuration又会实例化一个常量Environment,Environment中又包括了TransactionFactory与DataSource。

build过程将这些属性填充,自此SqlSessionFactory便成立了。

5.2 MyBatis启动

一些疑问解答

Q:主键约束与唯一约束相同吗?

A:不一样。主要体现在判定过程的不同。

对于主键约束而言,你可以理解为这个约束本身就是一个协议集合。

这个协议包括3个条款:(1)一个Table只能有1个主键约束(2)必须由非空约束作为前置条件;(3)colunm必须唯一;

因此在建表时,只增加主键约束,它会在条款(2)时返回数据库异常,提示colunm不能为空。

或是后续事务中对colunm的非查询操作执行前都会进行条款链(1)->(2)->(3)的判断,通过线性遍历对比是否有重复。

Duplicate entry '1' for key 'PRIMARY'

而唯一约束是另一个协议集合:(1)colunm必须唯一 ;(2)与唯一索引同生同灭。

如果重复,则抛出数据库异常。唯一约束是基于索引树的非线性遍历判定

Duplicate entry '1' for key 'Test_column_1_uindex'

因此主键约束与唯一约束不同。

  1. create table Tst
  2. (
  3. column_1 int not null,
  4. constraint Tst_pk
  5. UNIQUE key (column_1),
  6. constraint Tst_pk
  7. PRIMARY KEY (column_1)
  8. );

通过上述的SQL,可以发现,生成了两个约束以及一个索引:

 这代表了什么呢?这实际上是两种约束策略都驻守在这个Table上。

Q:什么是分库分表?

当业务规模突然扩大到大数级别时,会出现如下问题:

  • 一旦出现未命中的SQL缓存,BufferPool将会切换磁盘页去找到该SQL执行计划所需要的数据段,这是一种难以想象的I/O次数,巨大的系统开销。这将不断需要更多的资源去维护数据库;
  • 如果试图用索引提高查找效率,如此大的数据是非常吃力的,而且发生锁库锁表会直接影响线上服务;

以上情况都在不断积压数据库压力,如何避免以上情况,答:未雨绸缪。寻找一种可持续发展的策略。

  • 策略一:分库

分库就是在单库的每秒查询率越来越高,对应的数据引擎对于磁盘页的I/O次数也会越来越多的情况下,将数据库拆分为多个数据库,并设置每个数据库的最大每秒查询率。

比如mydb{table1,table2} 每秒查询率上限 = 1000,可以拆分为mydb1{table1,table2},mydb2{table1,table2},每秒查询率上限分别为 = 500,以此提高CRUD速度。

  • 策略二:分表

这种策略旨在将大数级别的表拆分为若干子表,使其维护成本、性能保持在一个可接受的范围内。

分表通常有2个依据:

  1. 垂直拆分

上层服务将服务按一定逻辑拆解,形成一个个功能完备,独立运行的服务。(微服务)如果下层数据库服务没有对应上层横向的扩展,将会面临高并发的流量冲击。因此,下层数据库服务也应对应上层拆分,将表中不同的业务拆分为不同的业务表并与对应的客户端服务对应。

比如mydb{table1,table2} ->mydb{table1_bizA,table1_bizA,table2_bizA,table2_bizB},以此提高CRUD速度。

  1. 水平拆分

随着业务的不断发展,单表查询也逐渐到了容量瓶颈。这时便可以水平的以一定逻辑将一个业务表拆分为多个表,比如时间,将不同年份的业务表数据,分别拆分为 业务_Time ,在应用层动态拼接SQL,实现不同时间业务请求 对应不同时间业务表。

比如mydb{table1_bizA,table1_bizA,table2_bizA,table2_bizB}->mydb_time1{table1_bizA,table1_bizA,table2_bizA,table2_bizB}+

  mydb_time2{table1_bizA,table1_bizA,table2_bizA,table2_bizB},以此提高CRUD速度。

分库分表结合起来便是:

mydb_time1_1{table1_bizA,table1_bizA,table2_bizA,table2_bizB}+

mydb_time1_2{table1_bizA,table1_bizA,table2_bizA,table2_bizB}+

mydb_time2_1{table1_bizA,table1_bizA,table2_bizA,table2_bizB}+

mydb_time2_2{table1_bizA,table1_bizA,table2_bizA,table2_bizB},以此提高CRUD速度。

当然,还可以将每个数据库分别部署在不同服务器上,分布式数据库。

或者,利用庞大的缓存提高服务速度。

Q:如何优化数据库性能?

参见另一篇文章:如何优化MySQL数据库性能?_kevinmeanscool的博客-CSDN博客

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/599910
推荐阅读
相关标签
  

闽ICP备14008679号