赞
踩
在探索开始前,我们先试想一个问题:存储为什么要分缓存与磁盘?
我们利用高级语言,编写逻辑,最终被解释为指令集合,委托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也不例外。
顾名思义,传输层协议就是客户端与服务端之间传输数据的约定。
常见的约定有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 传输速度
如何选择就需要考虑数据是否需要稳定等。
HTTP协议解释起来很简单,基于2.1.1介绍的传输层协议,传输层传输的都是二进制码流。如果发送端与接收端没有一个封装开箱的协议,那么将无法识别出有效的内容。因此需要HTTP协议来做传输前后的封装开箱。
HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并返回。
HTTP定义了4个客户端与服务器交互的方法:
POST一般用于更新资源信息,会修改资源内容。
GET一般用于获取/查询资源信息,不对资源产生副作用。
为什么没有解释Put与Delete呢?因为SpringWeb将增删改都归于了Post范畴。
自此介绍完了客户端与数据舒服端的数据是如何交换的。再回到这个图,我们可以看到除了身份User与凭据Password,剩下的就是Database。
Database俗称DB,数据库,其结构如下。
可以抽象为:
可以发现:
磁盘上的线性数据被数据库有计划(Schema)地管理起来了。
这个计划就是用Table管理静态数据。
值得一提的是,存储空间写入数据时,是寻找连续的存储地址,就写入,所以整个过程是十分随机的,即table中的数据在实际存储单元上是随机分布的,如果我们全盘遍历,查找一个table的数据将十分低效,因此table是作为一种映射的集合(序号->存储地址)。以此使得相似的数据内聚。
我们通过观察Mysql建表的窗口可以发现表内部的管理模式:
这是可视化工具将常用的设置在了界面, 实际上Table中只存在这么些东西:
Table结构是一个二维数组,它将一个对象线性的映射(a->b)在了Table中,所以列就是对象的属性(b),而行序号是线性映射的key(a)。
约束是指对表中数据的一种限制条件。
能够帮助数据库管理员(ServerObjs)更好地管理数据库,并且能够确保数据库中数据的正确性和有效性。
常见的约束5个:
约束类型 | 作用 |
主键约束 Primary Key | 使该Colunm成为唯一地标识表中的每一条记录的标志,其唯一索引保证了这个约束的实现。 为了保证(对象->记录)映射有意义,必须与非空约束组合使用。 Mysql会强制这一行为。主键约束与其唯一索引共生共灭。 |
非空约束 Not Null | 使该Column不能为NULL,否则抛出数据库异常。 |
默认约束 Default | 使该Column填入默认值,通常使用在创建时间等属性,插入时字段填default即可。如果插入时非要自己给一个值,则必须与默认值相同,否则会报数据库一致性异常。 |
自增约束 AutoIncreament | 使该Column填入数据库主存计数器的值。(Oracle中则可通过全局变量:序列完成) |
唯一约束 Unique | 使Column不能出现重复的值。与主键约束完全不同,与其唯一索引共生共灭。 |
索引是记录的检索映射。
可以通过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通过旋转不断进行深度约束,避免查找树退化为链表,以此来保持查找的时间复杂度O(logn)。
跳表是利用双向链表的直接寻址特性,在有序链表上加入多级索引:
这样我们通过最上级索引,可以快速确定查找值所在的区间。
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 读。
- //查询磁盘页大小
- 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增删改造成一定的性能困扰,因为具有一定概率需要重新组织索引结构,这是一笔开销。因此,如果表数据查找次数明显大于修改次数,那么使用索引的确是个不错的提升性能的选择。
权限、外键等根据阿里设计规范都应在应用层实现控制:
自此,基本结束了对面向DataBase的基本结构,下面介绍客户端如何与服务端交互。
我们都知道SQL是客户端对于数据库服务端的数据请求。
那么这个请求,从我们编写经历了怎样的历程呢?下面还是以Mysql为例。
MySQL C/S端,我们都知道双端通信的前提是建立连接Connection。这个连接实际上是由TCP/IP运输层协议规范,因此我们的客户端与数据库服务端要开始数据传输,首先需要双端处于Established状态。这就是涉及到 3.1.2中提到的三次握手了。这些逻辑相同的步骤,MySQL驱动都会自动帮我们完成。
网络中的连接都是由线程来处理的,所谓网络连接说白了就是一次请求,每次请求都会有相应的线程去处理的。每个请求都会接入MySQL驱动,委托MySQL驱动建立Connnection,直到事务结束,回收资源。
当然,Mysql面临的线程请求并不会串行,大部分情况是多线程的。因此MySQL内部采用类似线程池的连接池来管理(请求->Connection)的封装:
对于MySQL驱动提出的同步握手请求,数据库也对此提供了并发的管理,称作:数据库连接池:
通过(业务请求->客户端连接)的阻塞队列管理对应(客户端连接->数据库服务端连接)
在连接建立成功后,便可以进行业务操作,下面介绍数据库服务端如何处理SQL。
MySQL 中处理请求的线程在获取到请求以后获取 SQL 语句去交给 SQL 接口去处理。
下一步,SQL将被SQL解析器解析为MySQL内部的执行计划。
SQL优化器接收到执行计划后,将根据最小成本规则优化执行计划,这里的成本主要包括两个方面, IO 成本和 CPU 成本:
读取磁盘页内容,写入内存的开销。通常,MySQL每次读取大小为1个磁盘页。
内容写入内存后,将内容以执行计划中的条件(条件包括where、orderBy等)组织。
MySQL 优化器 会计算 「IO 成本 + CPU」 成本最小的方案作为执行计划。执行计划通常为横向树形结构。
ps:MySQL 优化器优化执行计划的过程委托存储引擎API运算出。
在确定执行计划后,SQL执行器回根据执行计划,这个过程是:最右最上先执行。
ps:MySQL 执行器执行执行计划的过程也是委托存储引擎API。
整个过程图示如下:
数据流图为:
MySQL默认的存储引擎为InnoDB。
存储引擎执行SQL所需的数据段,要么在内存中、要么在磁盘中。如果大部分在磁盘中,每步计划的执行,都需要对磁盘进行随机I/O扫描,将会造成巨大的额外开销。因此MySQL会将本次计划步骤所需的数据加载到内存中。加载的内存就是InnoDB中十分重要的一个组件:BufferPool 缓冲池
顾名思义,缓冲池其实就是类似 Redis 一样的作用,起到一个缓存的作用。
MySQL 的数据最终是存储在磁盘中的,如果没有这个 Buffer Pool 那么我们每次的数据库请求都会磁盘中查找,这样必然会存在 IO 操作,这是无法接受的。
因此,InnoDB处理SQL时:
->判断所需的记录是否缓存
->没有缓存的情况下,通过I/O加载到BufferPool
->BufferPool的记录组加排他锁,确保计划执行的原子性、有序性、可见性。
同时,很重要的一点是:本次计划所需的数据段,都会写入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,用以事务提交后缓存丢失的。
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的完整执行过程。
以及介绍完了面向硬盘的关系型数据库基本原理。
JDBC API 是Java为开发者提供一套数据库访问接口。
JDBC中的核心类有:DriverManager(驱动管理员)、Connection(连接)、Statement(声明),和ResultSet(结果集)
执行流程:
连接数据源,如:数据库。
JDBC通过反射机制,创建一个指定的Driver对象。
- Class.forName(driverClass)
- //1.加载驱动程序
- Class.forName("com.mysql.jdbc.Driver");
我们分析代码,这里反射的是MySQL.Driver类,此时JVM会将这个类以及其继承的父类缓存至元空间MetaSpace,
通过阅读其父类,发现具有一个静态代码块:
- static {
- try {
- DriverManager.registerDriver(new Driver());
- } catch (SQLException var1) {
- throw new RuntimeException("Can't register driver!");
- }
- }
根据JVM的类加载机制,在准备阶段会初始化静态块,于是便执行了registerDriver方法,执行这个方法会注册一个空Driver对象到DriverManager.registeredDrivers:
- public static synchronized void registerDriver(java.sql.Driver driver,
- DriverAction da)
- throws SQLException {
-
- /* Register the driver if it has not already been added to our list */
- if(driver != null) {
- registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
- } else {
- // This is for compatibility with the original DriverManager
- throw new NullPointerException();
- }
-
- println("registerDriver: " + driver);
-
- }
看起来Driver是空的?其实并不是,在调用 registerDriver方法前,需要加载registerDriver方法所属的类DriverManage,而DriverManager也具有一个静态代码块,在准备阶段初始化:
- static {
- loadInitialDrivers();
- println("JDBC DriverManager initialized");
- }
进入驱动初始化加载的方法后,发现调用了AccessController.doPrivileged(),首先加载了System.getProperty("jdbc.drivers"),系统全局设置的驱动集合:
- try {
- drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
- public String run() {
- return System.getProperty("jdbc.drivers");
- }
- });
- } catch (Exception ex) {
- drivers = null;
- }
继续,又进行了一次加载,利用ServiceLoader将实现Driver接口的实体类装入,这一步是根据services映射完成的:
- AccessController.doPrivileged(new PrivilegedAction<Void>() {
- public Void run() {
-
- ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
- Iterator<Driver> driversIterator = loadedDrivers.iterator();
-
- try{
- while(driversIterator.hasNext()) {
- driversIterator.next();
- }
- } catch(Throwable t) {
- // Do nothing
- }
- return null;
- }
- });
小结:JDBC通过反射动态装载数据库驱动
获取连接
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
读取源码可以发现,DriverManager委托了数据库驱动进行connect:
为数据库传递查询和更新指令。
- //用于执行静态SQL语句并返回其生成的结果的对象
- 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
- String sql = "insert Table(column1,column2) value (?,?)";
- PreparedStatement ptmt = conn.prepareStatement(sql); //预编译SQL,减少sql执行
- //通配符替换
- ptmt.setInt(1);
- ptmt.setString("1");
- ...
- pstmt.addBatch();
- //通配符替换
- ptmt.setInt(2);
- ptmt.setString("2");
- ...
- pstmt.addBatch();
-
- pstmt.executeBatch()
此时缓存中会拼接出一个SQL集合的SQL:
- insert Table(column1,column2) value (1,"1");
- 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有3个核心的组件:SqlSessionFactoryBuilder、XML ConfigBuilder、XML MapperBuilder
SqlSessionFactoryBuilder,顾名思义,是SqlSessionFactory的构造器。
通过源码我们可以发现,构造器可以通过多种方式实例化SqlSessionFactory。
- // 传递配置文件的输入流对象
- public SqlSessionFactory build(InputStream inputStream) {
- return build(inputStream, null, null);
- }
- // 通过XMLConfigBuilder工具类解析xml配置
- public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
- ...
- }
- // 通过Configuration实例构建SqlSessionFactory对象
- public SqlSessionFactory build(Configuration config) {
- return new DefaultSqlSessionFactory(config);
- }
无论何种方式,最终都会实例化new DefaultSqlSessionFactory(config)。所以之间的过程,均是为了初始化config而做。
->过程中会委托XMLConfigBuilder去parse输入流,并返回Configuration。
->解析的过程中便包括mappaerElements,去解析出SQL静态映射,并缓存至Configuration中:
configuration.addMappers(mapperPackage);
进一步阅读 SqlSessionFactory实体类,发现一但实例化,SqlSessionFactory便会实例化一个常量Configuration对象,而Configuration又会实例化一个常量Environment,Environment中又包括了TransactionFactory与DataSource。
build过程将这些属性填充,自此SqlSessionFactory便成立了。
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'
因此主键约束与唯一约束不同。
- create table Tst
- (
- column_1 int not null,
- constraint Tst_pk
- UNIQUE key (column_1),
- constraint Tst_pk
- PRIMARY KEY (column_1)
- );
通过上述的SQL,可以发现,生成了两个约束以及一个索引:
这代表了什么呢?这实际上是两种约束策略都驻守在这个Table上。
当业务规模突然扩大到大数级别时,会出现如下问题:
以上情况都在不断积压数据库压力,如何避免以上情况,答:未雨绸缪。寻找一种可持续发展的策略。
分库就是在单库的每秒查询率越来越高,对应的数据引擎对于磁盘页的I/O次数也会越来越多的情况下,将数据库拆分为多个数据库,并设置每个数据库的最大每秒查询率。
比如mydb{table1,table2} 每秒查询率上限 = 1000,可以拆分为mydb1{table1,table2},mydb2{table1,table2},每秒查询率上限分别为 = 500,以此提高CRUD速度。
这种策略旨在将大数级别的表拆分为若干子表,使其维护成本、性能保持在一个可接受的范围内。
分表通常有2个依据:
上层服务将服务按一定逻辑拆解,形成一个个功能完备,独立运行的服务。(微服务)如果下层数据库服务没有对应上层横向的扩展,将会面临高并发的流量冲击。因此,下层数据库服务也应对应上层拆分,将表中不同的业务拆分为不同的业务表并与对应的客户端服务对应。
比如mydb{table1,table2} ->mydb{table1_bizA,table1_bizA,table2_bizA,table2_bizB},以此提高CRUD速度。
随着业务的不断发展,单表查询也逐渐到了容量瓶颈。这时便可以水平的以一定逻辑将一个业务表拆分为多个表,比如时间,将不同年份的业务表数据,分别拆分为 业务_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:如何优化数据库性能?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。