在传统的数据库表中,由于磁盘的物理结构限制,表和索引的结构为B-Tree,这就使得该类索引在大并发的OLTP环境中显得非常乏力,虽然有很多办法来解决这类问题,比如说乐观并发控制,应用程序缓存,分布式等。但成本依然会略高。而随着这些年硬件的发展,现在服务器拥有几百G内存并不罕见,此外由于NUMA架构的成熟,也消除了多CPU访问内存的瓶颈问题,因此内存数据库得以出现。
内存的学名叫做Random Access Memory(RAM),因此如其特性一样,是随机访问的,因此对于内存,对应的数据结构也会是Hash-Index,而并发的隔离方式也对应的变成了MVCC,因此内存数据库可以在同样的硬件资源下,Handle更多的并发和请求,并且不会被锁阻塞。
在SQL Server的Hekaton引擎由两部分组成:内存优化表和本地编译存储过程。虽然Hekaton集成进了关系数据库引擎,但访问他们的方法对于客户端是透明的,这也意味着从客户端应用程序的角度来看,并不会知道Hekaton引擎的存在。
首先内存优化表完全不会再存在锁的概念(虽然之前的版本有快照隔离这个乐观并发控制的概念,但快照隔离仍然需要在修改数据的时候加锁),此外内存优化表Hash-Index结构使得随机读写的速度大大提高,另外内存优化表可以设置为非持久内存优化表,从而也就没有了日志(适合于ETL中间结果操作,但存在数据丢失的危险)
SQL Server 2014内存数据库针对传统的表和存储过程引入了新的结构:
memory optimized table(内存优化表)和native stored procedure(本地编译存储过程)。
1.默认情况下Memory optimized table是完全持久的(即为durable
memory optimized table),如传统的基于磁盘的表上的事务一样,并且完全持久的事务也是支持原子、一致、隔离和持久
(ACID) 的。所不同的是内存优化表的整个表的主存储是在内存中,即为从内存读取表中的行,和更新这些行数据到内存中。
并非像是传统基于磁盘的表按照数据库数据库页面装载数据库。内存优化表的数据同时还在磁盘上维护着另一个副本,但仅用于持续性目的。
在数据库恢复期间,内存优化的表中的数据再次从磁盘装载。 除了默认持久的内存优化表之外,还支持non-durable
memory optimized table(非持久化内存优化表),不记录这些表的日志且不在磁盘上保存它们的数据。
这意味着这些表上的事务不需要任何磁盘 IO,但如果服务器崩溃或进行故障转移,则无法恢复数据。
2.Native compiled stored procedure(本地编译存储过程)是针对传统的存储过程而言的,是本机编译存储过程后生成DLL,由于本机编译是指将编程构造转换为本机代码的过程,这些代码由处理器指令组成,无需进一步编译或解释。与传统TSQL
相比,本机编译可提高访问数据的速度和执行查询的效率。故通过本机编译的存储过程,可在存储过程中提高查询和业务逻辑处理的效率。
内存数据库既可以包含内存优化表和本地编译存储过程,又可以包含基于磁盘的表和传统存储过程,各个对象之间数据存储、和访问的架构如下所示:
内存数据库的数据存储
内存数据库的创建过程其实就是将表存放到内存中,而不是整个数据库。在内存数据库中,磁盘上存储的数据文件不在是区、页的存储方式,而是基于文件流存储。文件流存储的一个特点之一就是支持快速的读操作,这在数据库重启时将文件流中的数据load到内存中时很能提高效率。内存数据库的数据文件分data
file和delta file,而且是成对出现,内存数据库中插入、更新的数据和删除的数据物理分开存储的,分别用data
file和delta file保存。
1、Data file
Data file用来保存”插入”或者”更新”的数据行,data file中数据行的存储顺序严格按照事务执行的顺序组织,比如data
file中第一行的数据来自于事务1,第二行数据来自于事务2,这两行可以是同一个表的数据,也可以是不同表的数据,取决于这两个连续的事务操作的内存优化表是否相同。
这种方式的好处是保证了磁盘IO的连续性,避免随机IO。
Data file的大小是固定的,为128MB,当一个data file被写满了后,SQL会自动新建一个data
file。因为数据在data file中保存的顺序是按照事务的执行顺序进行的,所以一张表的数据行(来自多个事务)可能跨越了多个data
file,当对多行进行更新操作时,写操作可以分配到多个文件上,并且同时进行,这样就可以加快更新的效率。
如下图,一共有4个data files(浅蓝色),第一个data file的事务范围为100-200,第二个data
file的事务范围为200-300……(100、200表示时间戳)
在Data file中,如果一行被删除或者更新了,这行不会从data file中移除,而是通过delta
file(上图黄色框)来标记删除的行,(update的本质是delete和insert的集合,所以执行update时也会有删除的动作),这样可以消除不必要的磁盘IO。
如果data file的数据永不删除,那文件岂不是无限制的增大,以后备份不是得用很大的磁盘才行?当然不是,SQL在处理这个问题用到方法其实很简单——”合并”,根据合并策略,将多个data
file和delta file合并起来,依据delta file的内容删除data file中的多余记录,然后将多个data
file合并成一个文件,从而减小数据文件占用的磁盘空间大小。
2、Delta file
每个data file都有一个与之匹配的Delta File,这个匹配是指事务范围上的匹配,两者记录的是同一段事务(包括一个或者多个事务)上的数据,Delta
File中记录了data file中被删除行的标记,这个标记其实就是一个关联信息{inserting_tx_id,
row_id, deleting_tx_id }。它跟data file一样,也是严格按照事务操作的顺序来保存删除的行的信息。
如上图,该内存数据库有5个data file,分别存放了事务范围在100-200、200-300、300-400、400-500及500的数据。如果有一个时间戳为501的事务需要删除时间戳为150、250、450的事务所产生的数据和增加一些新数据时,相应的IO请求就会被分配到第1、2、4的
delta file上和第5的data file上。删除操作可以分配到多个文件上,并且同时进行,这样就可以加快删除的效率。
内存优化表存储结构
内存优化表是基于行版本存储的,同一行在内存中会有多个版本,可以将内存优化表的存储结构看作是该表中所有行的多个行版本的集合。内存优化表中的行跟传统数据库的行结构是不一样的,下图描述了内存优化表中一行的数据结构:
在内存优化表中,一行有两个大部分组成:Row header和Row body,
1.Row header记录这个行的有效期(开始时间戳和结束时间戳)和索引指针
2.Row body记录了一行的实际数据。
在内存优化表中,行版本的数量是由针对该行的操作次数决定的,比如:每更新一次,就会新产生一行,增加一个行版本,新行有新的开始时间戳,新行产生后,原来的数据行会自动填充结束时间戳,意味这行已经过期。
备注:上图实际上只有3行,第1行有3个行版本,第2行有2个行版本,第3行有4个行版本。
在传统数据库中,表中每一行都是唯一的,一个事务如想找到一行,通过文件号、页号、槽位就可以了。在内存数据库中,每一行有多个行版本,一个事务不可能对将每个行版本都操作一遍,实际上,一个事物只能操作同一行的一个行版本,至于它能对哪个行版本进行操作,取决于事务执行时间是否在这行的两个时间戳之间。除此之外的其他行版本对该事务而言是不可见的。由于一行可能存在多个行版本,一张上百万行的表,内存哪够呀。前文介绍过了,每个行实际上是有时间戳的,对于已经打上结束时间戳且没有活动事务访问的行,SQL
Server会通过garbage collection机制回收它占用的内存,从而节省内存。所以不要担心内存不够。
内存数据库的锁
锁的一个重要作用就是避免多个进程同时修改数据,从而造成数据不一致。常见的冲突现象包括读写互锁和写写互锁。那内存数据库是如何通过行版本来解决这两种锁定现象的呢?
1、读写互锁
在内存数据库中,所有对内存优化表的事务隔离都是基于快照的,准确的说是基于行的快照。从上文行的
结构可以知道,每行的行头包括开始时间戳和结束时间戳的,一个事务能不能访问到这行关键在于事务的启动时间是不是在这行的两个时间戳内。如果某个事务正在修改一行(快照),但还未提交到内存优化表中,也就是说”新行”还没有结束时间戳,对”读事务”而言,它读还是是原来行(快照),因此不会存在脏读的现象。
2、写写互锁
两个事务同时更新一行时,就会发生写写互锁。内存数据库冲突发生的概率比传统数据库小很多,但如果实在遇到了冲突,只能调整应用程序,在应用程序中加入”重试逻辑”(等待一会,然后再重新发起事务)来解决。
或许有同学觉得这种方式好像也没有什么大的性能改变。其实不然,举个例子,在传统数据库中一个锁可能将整个表都管住了,在表锁期间只能等待这个事务做完才能执行其他事务,而实际上这个事务可能只是修改了小部分行,因为表锁的存在,其他行那些不需要被这个事务操作的行。但内存数据库中写写冲突总是发生在行级别的,这个粒度小多了,影响没这么大。
内存数据库的持久性
1、事务日志
内存数据库的”写日志”和”写数据”在一个事务中进行,在事务执行期间,SQL会先”写数据”然后在才”写日志”,这点与传统数据库不同,在传统数据库中,不管是在内存中还是磁盘中,”写数据”总是在”写日志”之后,也就是通常所说的WAL(Write-Ahead
Transaction Log)。但是,在事务提交时,内存数据库和传统数据库在”写日志”上没有什么区别:日志会先于数据写入到磁盘中。因此,即使服务器发生了宕机或者断电,下次数据库重启时会按照已经保存在磁盘中事务日志将业务redo(重做),所以不要担心数据会丢失。另外,内存数据库只会对持久性表将已提交的事物日志保存到磁盘中。这样做的好处可以减少写磁盘的次数。内存数据库支持频繁、快速的增、删、改等操作,这个强度远远高于传统数据库,数据库需要为每笔操作写日志,这样就会产生大量磁盘IO,写日志操作将有可能成为性能瓶颈,不记录未提交的事务日志就减少写日志的数量,从而可以提高数据库的性能。
2、CheckPoint
在内存数据库中,CheckPoint的主要目的就是将内存中的”数据”写入到磁盘中,从而在数据库崩溃或者重启时减少数据恢复的时间。不需要数据库逐条读取所有的日志来恢复数据。默认情况下Checkpoint是周期性进行的,当日志至上次checkpoint后增加了512M时会触发新一轮CheckPoint。在传统数据库这种,Checkpoint可以将未提交的数据flush到磁盘的mdf文件中,这个现象在内存数据库中不会发生,因为内存数据库只将已提交事务的日志,而在写日志(到磁盘)之前不可能将数据先写到磁盘中,因此可以保证写到磁盘中的数据一定是已提交事务的数据。
内存数据库应用场景
传统基于磁盘的表,通常会遇到内存页面置换、死锁、造成了吞吐量有限、事务延迟较长等问题,内存数据库的内存优化表由于常驻内存,适用于低延迟、高并发、快速数据传输和装载等场景。各场景的使用、机制具体如下:
低延迟:由于内存优化表和本地编译存储过程直接生成DLL,本机编译可提高访问数据的速度和执行查询的效率响应速度快,作为参与处理业务逻辑的存储过程而言,大大降低了存储过程作为中间层执行和访问的效率。提高了应用的访问效率,降低了延迟性。内存优化表的创建和装载过程如下:
本地编译存储过程的创建和装载过程如下:
高吞吐量:由于内存优化表直接从内存中读取、写入数据,当访问数据时,不再使用latch,故不同于基于磁盘的表,对于insert/update/delete的操作,latch争用、以及死锁问题随即消失。
与此同时,可大大提高了应用的吞吐量。 随着配置的增加,其性能呈直线上升。
快速数据传输、装载:由于非持久化内存优化表仅常驻内存,并无基于磁盘的副本。当需要将一些外部数据通过ETL装载到内存数据库,可以使用无任何IO和logging的非持久化内存优化表作为过渡表,可有效的加快装载数据库的速度。
内存数据库所需资源
内存数据库在使用硬件资源与传统表有着一定的特殊性,为了提高内存数据库性能,对存储内存数据库的各方面的资源有着比传统数据库更高的要求。可参考如下具体需求:
1.内存:所有内存优化表是常驻内存的,因此需足够的物理内存来存储内存优化表。但这并不意味着需要将整个数据库放入内存中,而是仅将频繁访问的热数据常驻内存优化表中。且最高可以支持到256GB的数据量。
2.磁盘:同样存在log和data两类文件。Log文件依然记录事务信息。针对于持久性的内存优化表,为了降低log
IO的竞争、保证低延迟,一般建议至少SSD。
3.CPU:可根据OLTP环境的负载考虑CPU的配置,如两个CPU socket支撑一个中等级别的服务器。
4.Network:针对于单机的内存数据库,由于数据存储于数据库服务器的内存中,对于数据交互仍然为应用层到数据层的访问,如以往数据交互,对于网络并未有较高的依赖性。对于内存数据库应用于数据库高可用和异地灾备的情况下(如同步/异步模式的Always-on),同一数据中心的网络延迟,以及不同数据中的网络延迟对于使用与高可用性和灾备的内存数据库的事务有一定量的影响。
|