SQL Server 内存 OLTP 项目 Hekaton CTP1 内部概述
在最初设计SQL Server时由于内存价格高昂,因此将不需要处理的数据存储在磁盘上。在过去的30年里内存价格已经大幅下降,因此不再需要这样的设计。与此同时,多核服务器已广泛普及,今天,可以以低于5万美元的价格购买一台具有32内核和1TB内存的服务器。在许多生产环境,OLTP数据库都不会大于1TB,即使这种情况不适用大多数,我们也需要重新评估将数据存储在磁盘上的利弊以及将数据读入内存时产生的I/O开销。此外,在OLTP数据库中,被更新的数据需要重新写回磁盘,由此产生的性能开销不容忽视。内存优化表与基于磁盘的表在存储方式上完全不同,这种新的数据结构可以更加高效的访问和处理数据。
技术的发展让服务器拥有更多内核及内存,为此,微软SQL Server团队开始构建为大容量内存和多核CPU进行优化的数据库引擎,该引擎的开发代号为“Hekaton”,本文将详细介绍。
设计初衷和目的
开发真正的内存数据库需满足三个基本要求:1)将工作所需的大部分或全部数据存储至内存;2)更低的数据操作延迟;3)针对某类特定工作而优化的数据库。由于摩尔定律影响着内存价格,内存容量会越来越大且满足要求(1)并部分满足要求(2)(更大的内存可以降低数据读取延迟,但不影响传统数据库向磁盘写入数据的延迟)。在本文档将要讨论到的SQL Server Hekaton其它特性,能够大幅降低数据修改操作的延迟。由于针对某类工作而优化的系统往往比通用系统更加出色,因此需要专业化的数据库引擎,包括复杂事件处理(CEP),DW/BI和OLTP在内的大多数专业化系统都针对内存结构对数据结构和算法进行了优化。
内存容量飞速增长并且价格持续降低是微软开发Hekaton的源动力。此外,64位架构和多核处理器日益普及,在大多数情况下,我们没有理由不把OLTP数据库或所有性能敏感的工作数据集存放在内存中。许多大型金融机构、在线零售和航空订票系统的数据量介于500GB到5TB之间,但密集访问的数据要小很多。截至2012年,即使只有两个内存插槽的服务器也可通过32GB的DIMM获得最大2TB的DRAM(如IBM x3680、X5)。展望未来,将完全可能在几年之内构建DRAM容量在1-10 PB的分布式系统,而每GB内存的单位成本将小于5美元。与此同时,非易失性RAM的应用也只是时间问题。
如果大多数或所有应用程序的数据都存储在内存中,SQL Server早期版本针对磁盘所做的数据读取优化就变得毫无意义,因为读取优化是基于所有的页读取都可能需要从磁盘物理读。如果不再从磁盘读取数据,就需要不同的算法优化开销。此外,如果没有磁盘读取和等待锁释放、等待锁可用的时间延迟,包括等待日志写入等相关统计数据也会失去意义。Hekaton将解决这些问题,它通过全新的开放式多版本并发控制解决了等待锁释放的问题,它生成的日志数据量更小,需要的写入次数也更少,从而改善了日志写入等待时间。后续白皮书将详细介绍不使用锁和闩锁的并发管理及内存优化表的日志细节。
术语
本文将使用Hekaton描述SQL Server中有关内存优化表的技术。相较于内存优化表,SQL Server一直采用的都是基于磁盘的表。本文将出现的术语包括:
- 内存优化表 在开发代码为Hekaton的项目中新引入的数据结构构建的数据库表,本文将对其进行详细介绍。
- 基于磁盘的表 不同于内存优化表,它是指SQL Server一直采用的数据结构,磁盘上的数据在读取时以页面作为单元,每页容量为8K。
- 本地编译存储过程 是指Hekaton支持的一种被编译为机器代码的对象类型,它有助于提高内存优化表的性能。与之相对的是SQL Server一直采用的解析型TSQL存储过程。本地编译存储过程仅适用于内存优化表。
- 跨容器事务 是指同时引用了内存优化表和基于磁盘的表的事务。
- 互操作 是指在解析型TSQL中引用内存优化表。
功能概述
在Hekaton中的大多数数据操作,您可能完全没有意识到在使用内存优化表而非基于磁盘的表。然而,SQL Server会采用非常不同的方式处理内存优化表中的数据。本节将为您介绍Hekaton的操作和数据处理与基于磁盘的表之间的区别,并简要介绍其它竞争对手在内存优化数据库方面采用的解决方案, 同时指出SQL Server Hekaton的独到之处。
Hekaton的特别之处
尽管 SQL Server关系引擎已完全集成Hekaton并可在同一界面使用,但在内部处理和能力上两者还是具有相当大的差异。图1展示了包含Hekaton组件的SQL Server引擎架构。
图1 集成Hekaton的 SQL Server引擎架构
注意,客户端应用使用同一个TDS处理程序访问内存优化表和基于磁盘的表,这也同时适用于本地编译存储过程和解析型TSQL。解析型TSQL拥有访问内存优化表的互操作能力,但本地编译存储过程只能访问内存优化表。
内存优化表
内存优化表与基于磁盘的表最大的区别是无需将数据从磁盘读取到缓存,内存优化表的所有数据都始终存储在内存。检查点文件建立在文件流文件组上,它仅用于数据恢复,其中记录了数据的变更,检查点文件只可追加(append-only)。
内存优化表使用与基于磁盘的表相同的事务日志,这些事务日志存储在磁盘上。如果系统崩溃或服务器宕机,内存优化表中的行数据可以根据检查点文件和事务日志进行恢复。
在Hekaton中可以使用SCHEMA_ONLY选项创建非持久性且不生成日志的表。对该表来说,虽然数据不持久,但表的定义是持久的。处理事务时这些表不会产生IO操作,但只有SQL Server运行时内存中的数据才可用,当SQL Server宕机或AlwaysOn故障转移时这些数据会丢失,在数据库恢复时只能重建表定义但无法挽回其中的数据。这些表在有些情况下可能非常有用,例如在ETL过程中的临时表或者存储Web服务器会话状态。尽管这些数据是非持久的,但这些表上的操作具有原子性、独立性和一致性,完全符合事务性要求。在“创建表”一节中将详细介绍非持久性表的创建语法。
基于内存优化表的索引
内存优化表使用的索引并不以传统的B-Tree形式存储。内存优化表支持哈希索引,索引以BW-Tree的形式存储为哈希表,并用链表连接所有散列到相同值和范围索引的行。有关哈希索引的内容将在后文详细介绍。由于CTP 1尚不支持范围索引及BW-Tree,这些内容将在未来进一步介绍。
任何内存优化表都需要至少一个索引,因为是索引将所有行结合到一个单一表中。内存优化表不支持无序存储结构,例如在基于磁盘的表中使用的堆结构。
索引不会存储在磁盘上,也不会体现在磁盘上的检查点文件及索引操作不记录日志。与基于磁盘的表所使用的B-Tree索引一样,在内存优化表上进行数据操作时会自动维护索引数据,但如果SQL Server重新启动,数据流式传输到内存时索引会完全重建。
并发改进
SQL Server采用乐观多版本并发控制访问内存优化表。尽管SQL Server 2005引入了基于快照的隔离级别并表示支持乐观并发控制,但在数据修改操作时必须有锁参与。内存优化表可以在没有锁的情况下可以实现该功能,同时消除了阻塞等待。
需要注意的是,这并不意味着在内存优化表中进行操作时不会遇到等待,有些情况无法避免,例如等待日志写入完成。但相较于基于磁盘的表来说,内存优化表可以更加高效的记录日志,由此产生的等待时间也更短。同时避免了磁盘读取数据或数据行锁定产生的等待。
功能集
本地编译存储过程可以让内存优化表获得最佳性能。但本地编译存储过程对TSQL有限制,解析型TSQL的功能更加丰富。此外,本地编译存储过程仅支持内存优化表且无法引用基于磁盘的表。
Hekaton只是改进了的DBCC PINTABLE?
DBCC PINTABLE是SQL Server旧版本中的一项重要功能,从磁盘读取页面后无法从“驻留”在内存的表中移除任何数据。由于初始状态需要读取页面,因此无法避免首次读取页面产生的开销,例如访问表的操作。这些驻留的表与基于磁盘的表没有区别,它们不仅需要锁,闩锁和日志记录,而且相同的索引结构也使用锁和日志记录。Hekaton内存优化表与基于磁盘的表完全不同,它采用全新的数据和索引结构,避免锁的同时更加高效处理日志记录。
竞争产品
处理OLTP数据需要使用两种专用引擎:主内存数据库,代表产品有Oracle TimesTen和IBM SolidDB以及其它嵌入式数据库;应用程序缓存或键值存储(例如Velocity – App Fabric Cache和Gigaspaces),它们在应用程序和中间层内存之间进行调节以减轻数据库系统负担。现在,日益复杂的缓存已逐步具备数据库功能,包括事务、范围索引和查询能力(Gigaspaces已具备这些功能),同时,数据库系统也获得了许多缓存特性,包括高性能哈希索引和跨集群能力(VoltDB就是一个例子)。Hekaton引擎旨在结合不同引擎的优势,它不仅拥有缓存的性能还拥有数据库的功能,它可以让您将表和索引完全存储在内存中,创建一个完全存储在内存中的数据库系统,它拥有高性能索引和日志记录,及其它功能来显著提高查询性能。
SQL Server Hekaton提供如下功能,只有少数竞争产品拥有,也有一些是独有的:
- 同时集成内存优化表和基于磁盘的表,因此可以逐渐向内存数据库过渡
- 本地编译存储过程可大幅降低基本数据处理所需时间
- 为内存访问特别优化的哈希索引
- 数据不再存储在页面上,避免了页面闩锁
- 真正的多版本乐观并发控制,所有操作都无需锁或闩锁
Hekaton入门
使用Hekaton
Hekaton作为SQL Server的一部分将在SQL Server 2014中开始提供,2013年6月将发布CTP版本。Hekaton的安装可通过SQL Server通用安装程序完成。SQL Server 2014 64位版本缺省安装Hekaton,而32位版本SQL Server不支持该功能。
创建数据库
为了使用内存优化表,创建数据库时应至少包含一个MEMORY_OPTIMIZED_DATA文件组。该文件组用于存储内存优化表在恢复时所需的检查点和增量文件,创建语法与新建普通文件流文件组几乎相同,但必须指定选项CONTAINS MEMORY_OPTIMIZED_DATA。以下是CREATE DATABASE语法示例,该语句用于创建支持内存优化表的数据库:
CREATE DATABASE HKDB
ON
PRIMARY(NAME = [HKDB_data],
FILENAME = 'Q:\data\HKDB_data.mdf', size=500MB),
FILEGROUP [SampleDB_mod_fg] CONTAINS MEMORY_OPTIMIZED_DATA
(NAME = [HKDB_mod_dir],
FILENAME = 'R:\data\HKDB_mod_dir'),
(NAME = [HKDB_mod_dir],
FILENAME = 'S:\data\HKDB_mod_dir')
LOG ON (name = [SampleDB_log], Filename='L:\log\HKDB_log.ldf', size=500MB)
COLLATE Latin1_General_100_BIN2;
注意,上述代码中的语句将在三个磁盘(Q: , R: 和 S:)上创建文件,您在使用时应根据自身情况更改路径。磁盘R:和S:上的这两个文件名必须是唯一的,如果您将所有文件创建在同一磁盘,应该修改这两个文件的名称以确保唯一性。
还需注意,此处指定并使用了二进制排序规则。目前,内存优化表中所有索引仅支持对列进行Windows (non-SQL) BIN2排序规则,本地编译存储过程仅支持在此排序规则 下的比较、排序和分组。您可以将二进制排序规则设置为整个数据库的默认值(如上述代码所示),也可以在CREATE TABLE语句中为字符数据指定相应排序规则(也可以在查询中为比较、排序或分组操作指定排序规则)。
您可以将MEMORY_OPTIMIZED_DATA文件组添加至当前数据库,再将文件添加至该文件组。例如:
ALTER DATABASE AdventureWorks2012
ADD FILEGROUP hk_mod CONTAINS MEMORY_OPTIMIZED_DATA;
GO
ALTER DATABASE AdventureWorks2012
ADD FILE (NAME='hk_mod', FILENAME='c:\data\hk_mod')
TO FILEGROUP hk_mod;
GO
创建表
创建内存优化表所使用的语法与创建基于磁盘的表基本相同,需要在语句中指定扩展参数,但也有一些限制。使用语句MEMORY_OPTIMIZED = ON将表指定为内存优化,内存优化表支持的数据类型有:
- bit
- 所有整型:tinyint, smallint, int, bigint
- 所有货币类型:money, smallmoney
- 所有浮点型:float, real
- 日期及时间类型:datetime, smalldatetime, datetime2, date, time
numeric 和 decimal 类型
- 所有非LOB字符串类型:char(n), varchar(n), nchar(n), nvarchar(n), sysname
非LOB二进制类型:binary(n), varbinary(n)
- Uniqueidentifier
注意,内存优化表不接受任何LOB数据类型,也不支持XML、CLR或max数据类型,不接受行外数据且所有行被限制在8060字节内。实际上,在创建表时就会强制执行8060字节限制,因此,与基于磁盘的表不同的是,您无法创建包含两列varchar(5000) 数据的内存优化表。
内存优化表的DURABILITY值可以是SCHEMA_AND_DATA(默认值)或SCHEMA_ONLY。如果将内存优化表设置为DURABILITY=SCHEMA_ONLY,该表不会记录日志且数据不会存储在磁盘上,但表的定义会作为数据元数据存储,因此在SQL Server重新启动后的恢复过程会生成相应的空表。
正如前文所述,内存优化表至少要有一个索引以支持主键约束,使用自动创建的索引即可满足该要求。所有未设置SCHEMA_ONLY选项的表都需声明主键,而且主键(PRIMARY KEY)约束要求至少声明一个索引。下例展示了如何将PRIMARY KEY索引创建为HASH索引并为其指定bucket数量。本文将在随后哈希索引存储一节中详细介绍如何设置bucket的数量。
可以在CREATE TABLE语句创建单列索引(下文突出显示)。在哈希索引一节中将详细介绍参数BUCKET_COUNT的意义。
CREATE TABLE T1
(
[Name] varchar(32) not null PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1024),
[City] varchar(32) null,
[LastModified] datetime not null,
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
另外,完成列的创建后即可创建复合索引(下文突出显示),通过添加参数NONCLUSTERED (Range) 可对其进行定义,如下例所示(注意,CTP1不支持NONCLUSTERED索引):
CREATE TABLE T2
(
[Name] varchar(32) not null PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1024),
[City] varchar(32) null,
[LastModified] datetime not null,
INDEX T1_ndx_c2c3 NONCLUSTERED ([c2],[c3])
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
内存优化表创建完成后,Hekaton引擎会生成并编译访问表所需的DML例程并将例程作为DLL加载。对于内存优化表,SQL Server本身并不执行在内存优化表上的数据操作(record cracking),而是在访问这些表时根据操作调用相应的DLL。
创建内存优化表只有少数几项限制,有关数据类型的限制已在上文列出。
- 不支持DML触发
- 不支持FOREIGN KEY或CHECK约束
- 不支持IDENTITY列
- 除PRIMARY KEY以外不支持UNIQUE索引
- 索引最大数量为8,包括用于PRIMARY KEY的索引
另外,一旦表创建完成就无法通过ALTER TABLE更改表定义,如果要进行修改就必须删除并重新创建内存优化表。此外,不支持索引DDL命令(如:CREATE INDEX, ALTER INDEX, DROP INDEX),索引的创建必须在创建表的同时完成。
存储概述
Hekaton内存优化表及其索引与基于磁盘的表在存储上有很大差异。
行
与基于磁盘的表不同,为了优化字节编址的内存并替代块编址的磁盘,内存优化表不使用页面进行存储,也不存储在分配空间。分配的行在结构上被称为堆,它与基于磁盘的表使用的堆不同。单表的行无需存储在其它行旁边,SQL Server通过表的索引判断行和表之间的归属关系,由于索引为表提供了架构关系,因此必须拥有至少一个索引。
此外,与基于磁盘的表相比,内存优化表的行结构也与之不同。每个行都有标题及包含行属性的负载。图2展示了行的结构,并详描述了行标题的组成。
图 2 内存优化表使用的行结构。
行标题
标题部分用两个8字节区域存储Hekaton时间戳。所有包含内存优化表的数据库都维护着一个内部时间戳或事务计数器,引用内存优化表的任何事务提交都会使该值递增。Begin-Ts记录了事务插入到该行的时间戳,End-Ts记录了事务从该行删除的时间戳,对于尚未被删除的事务,End-Ts会使用特殊值(称为 ‘infinity’)表示。行的可见性由Begin-Ts和End-Ts共同决定。
状态 ID在标题中占据4字节。事务中所有状态都有唯一的StmtId值,创建行时,StmtId会保存创建行的状态值。相同语句重复访问行时会忽略该值。
最后,标题中还有一个2字节的区域(idxLinkCount)记录引用该行的索引数量,其后的idxLinkCount是一组索引指针,它与索引数相等,这部分内容将在下一节介绍。行的起始使用1作为引用值,这样即使改行不再连接到任何索引也可以被垃圾回收(GC)机制处理。垃圾回收被认为是初始值的所有者,只有它才能将该值设置为0。
如前文所述,表的所有索引都有指针,这些指针将行连接到一起。索引指针是唯一将行汇合成表的方法,除此再无其他结构,因此所有内存优化表都至少有一个索引。同样,由于指针数是行结构的一部分,且行也不再改变,因此索引必须与内存优化表一同创建。
负载区域
行的内容称为负载,包括主键列及所有其他列(这意味着内存优化表上的所有索引实际上都覆盖了索引)。负载的格式主要取决于表,在介绍创建表时曾提到Hekaton会将表操作编译生成DLL,在向表中插入行时它可以识别负载格式,同时为行操作生成相应命令。
哈希索引
哈希索引由指针数组组成,该数组中的每一元素都称为哈希bucket。内存优化表最多可以有8个索引。行中的每一索引键列都有对应的哈希函数,函数结果决定了该行该使用的bucket。通过哈希索引中的指针可以访问所有哈希值相同的键值(哈希函数运算结果相同的值),这些键值会链接成链表。向表添加行时,行中的索引键值会执行哈希函数,如果存在相同项,由于函数结果也相同,因而总会成为一条链。
图3展示了行中name列的哈希索引。为简化该例,假设此处的哈希函数只是计算索引键的字符串长度,因此第一个值 ‘Jane’ 的哈希值为4,该值成为哈希索引中第一个bucket(注意,真正的哈希函数具有更强的随机性和不可预测性,此处使用字符串长度作为哈希值仅为降低理解难度)。可以看到,哈希表中的条目4使用指针指向Jane所在的行。该行没有指向其它行,因此行中的索引指针为空(null)。
图 3 单行数据的哈希索引。
在图4中,name值为Greg的新行被添加进表中,由于Greg的哈希结果也是4,因此它与Jane的bucket相同,该行会链接到与Jane所在的同一个链上,同时生成指向Jane的指针。
图 4 两行数据的哈希索引。
如果向该表City列添加哈希索引则会生成第二组索引指针。从而使该表的每一行都有两个指向它的指针,以及两个可以指向其它行的指针,每一个指针代表一个索引。行中的两个指针分别指向Name索引链和City索引链中下一个值。在图5中,Name列的哈希索引与前文相同,只是增加了一些新数据,哈希值为4的有3行且哈希值为5的有两行,后者成为Name索引中第二个bucket;City列索引共有3个bucket,值为6的bucket共有3个值,值为7的bucket和值为8的bucket在相应的链上各对应一个值。
图 5 有两个哈希索引的表。
创建哈希索引后必须指定bucket的数量(如上文CREATE TABLE语句所示)。建议bucket的数量等于或大于索引键列的预计基数(唯一值的个数),这样可以尽可能让每个bucket对应的行在链上仅有一个值。由于bucket会占用内存因此请勿将该值设置过大。用户设置的数值会向上舍入至2的下一指数,如50,000将舍入为65,536。设置过多的bucket不会提高性能反而浪费内存,还会降低扫描每个bucket的性能。
范围索引
如果您不知道如何为特定的列设置bucket的数量,或者您将需要基于数值范围搜索数据,则应使用范围索引而非哈希索引。由于CTP 1不支持范围索引,该功能将不会在本文档中讨论。
数据操作
SQL Server Hekaton通过带有时间戳的内部事务ID控制行版本可见性,我们将其称为时间戳。时间戳由递增计数器生成,每次提交事务时值会自动增加。当事务开始时,开始时间戳(Begin-Ts)处于数据库最顶端,在提交事务时会生成一个新的时间戳作为该事务的唯一标识。时间戳的主要功能如下:
- 提交/结束时间:所有事务提交数据更改的时间点称为事务提交或结束时间戳,根据提交时间可以在序列化的历史记录中确定事务位置。
- 版本记录有效期:如图2所示,数据库中所有记录都有两个时间戳——开始时间戳(Begin-Ts)和结束时间戳(End-Ts)。开始时间戳指示创建该版本的事务提交时间,结束时间戳指示删除该版本(或被新版本替代)的事务提交时间。版本记录有效期(valid time)指示该版本对其他事务可见的时间戳范围。在图5中,Susan行中的Vienna在时间点“90”被更新为Bogota。
- 逻辑读取时间:读取时间可以是开始时间和当前时间之间的任意值,有效期与逻辑读取时间重叠时才能被读取。对于Read-Committed以外的隔离级别,事务的逻辑读取时间对应事务的开始。对于Read-Committed,对应事务中语句的开始。
Hekaton通过版本可见性的概念进行并发控制。逻辑读取时间为RT的事务只能看到开始时间戳小于RT且结束时间戳大于RT的版本。
事务隔离级别
内存优化表支持以下事务隔离级别:
- SNAPSHOT
- REPEATABLE READ
- SERIALIZABLE
事务隔离级别可以是本地编译存储过程Atomic块的一部分。另外,使用解析型Transact-SQL访问内存优化表时可以使用表级提示指定隔离级别。
事务隔离级别的设置是本地编译存储过程的必须部分。使用解析型Transact-SQL从用户事务访问内存优化表时必须设置表提示的隔离级别。
隔离级别READ COMMITTED支持自动提交事务的内存优化表,不支持显式或隐式用户事务。只有查询不需访问基于磁盘的表时隔离级别READ_COMMITTED_SNAPSHOT才支持自动提交事务的内存优化表。此外,指定SNAPSHOT隔离级别的解析型Transact-SQL事务无法访问内存优化表。同时,在隔离级别REPEATABLE READ 或 SERIALIZABLE中解析型Transact-SQL事务只能访问隔离级别为SNAPSHOT的内存优化表。
行的内存结构已在图3,4和5中给出,现在通过例子了解DML操作是如何执行的,我们将按顺序在尖括号中列出行的内容。假设事务TX1运行在隔离级别SERIALIZABLE下,其开始时间戳为240并执行以下两项操作:
- DELETE 行 <Greg , Lisbon>
- UPDATE 行 <Jane, Helsinki> 为 <Jane, Perth>
同时还有另外两个事务读取行:自动提交事务TX2在时间戳243执行了一条SELECT语句,显式事务TX3读取了一行数据并根据SELECT读取的值更新另一行数据,时间戳为246。
首先是数据变更事务,事务以获取开始时间戳(begin timestamp)作为起点,该时间戳指示了数据库事务的顺序,本例中时间戳为240。
在操作数据时,事务TX1只能访问开始时间戳小于或等于240且结束时间戳大于240的记录。
删除
事务 TX1 首先通过索引定位到 <Greg, Lisbon> 。删除会将改行的结束时间戳设置为240并添加一个额外标记位以表明事务TX1处于活动状态,尝试访问该行的其它事务需确定事务TX1是否仍处于活动状态,并以此判断 <Greg , Lisbon> 是否被成功删除。
更新和插入
UPDATE在更新 <Jane, Helsinki> 时会分为两步进行:使用DELETE删除整行然后使用INSERT插入新行。新行 <Jane, Perth> 在构建时会以 begin timestamp = 240 作为起始并添加一个标记位表明事务TX1正在活动,接着将结束时间戳设置为 ∞ (无限)。其它事务试图访问该行时会根据事务TX1的活动状态判断是否可以读取 <Jane, Perth> 。任何写-写冲突都会让后续事务执行失败。DELETE操作删除 <Jane, Helsinki> 后会插入 <Jane, Perth> 并链接至所有索引。
此时事务TX1已经完成所有操作但尚未提交。提交时需要先获取事务的结束时间戳,本例中的结束时间戳为250,在数据库中标识该事务更新已执行的序列点。事务在获取结束时间戳时会进入验证(validation)状态检查并确保未违反当前隔离级别。验证失败的事务会被放弃,更多有关验证的细节将在近期公布。在验证阶段SQL Server会写入相应的事务日志。
事务可以跟踪写集(write set)所有变化,写集是一系列与版本相关并带有指针的删除或插入操作,图6绿框内展示了当前事务的写集及变更的行。写集形成事务日志的内容,事务可以生成一条只包含时间戳及被删除或被添加版本的单一日志。与基于磁盘的表不同,没有每行单独的日志。但日志记录有上限,如果内存优化表超出限制,则会生成多个日志记录。事务状态会在日志写入存储后变为已提交(committed)并开始后期处理。
后期处理会遍历所有写集并处理每个条目,具体包括以下内容:
- 对于DELETE删除操作,将行的结束时间戳设置为事务结束时间戳(本例为250)并清除行结束时间戳上的活动标记
- 对于INSERT插入操作,将相关行的开始时间戳设置为事务结束时间戳(本例为250)并清除行结束时间戳上的活动标记
垃圾回收系统负责处理解除链接并删除的旧的行版本,这部分内容将在后续白皮书中详细介绍。
图 6 表的事务变更。
现在看看与TX1同时处理的读取事务TX2和TX3。别忘了事务TX1正在删除行 <Greg , Lisbon> 并将行<Jane, Helsinki> 更新为 <Jane, Perth> 。
自动提交事务TX2读取整个表:
SELECT Name, City
FROM T1
TX2的会话运行在默认隔离级别READ COMMITTED,但如上文所述,由于没有指定表隔离级别提示且T1是内存优化表,因此数据访问会使用隔离级别SNAPSHOT。由于TX2运行的时间戳为243因此可以访问当前行。由于行 <Greg, Beijing> 在时间戳243不再有效因此无法访问。行 <Greg, Lisbon> 在时间戳250将被删除,但由于时间戳243介于200和250,因此TX2可以读取它。同样,TX2还能读取行 <Susan, Bogota> 和 <Jane, Helsinki> 。
显式事务TX3开始于时间戳246,它先读取一行并根据读取到的值更新另一行。
DECLARE @City nvarchar(32);
BEGIN TRAN TX3
SELECT @City = City
FROM T1 WITH (REPEATABLEREAD)
WHERE Name = 'Jane';
UPDATE T1 WITH (REPEATABLEREAD)
SET City = @City
WHERE Name = 'Susan';
COMMIT TRAN -- commits at timestamp 255
TX3中的SELECT语句读取行 <Jane, Helsinki> ,该行在时间戳243是可访问的。接着TX3会将行 <Susan, Bogota> 更新为 <Susan, Helsinki> 。但如果TX3试图在TX1提交后进行提交,SQL Server会检查到行 <Jane, Helsinki> 已经被另外一个事务更新。由于违反了隔离级别REPEATABLE READ从而导致事务TX3提交失败并回滚。下一节将详细介绍验证功能。
验证
SQL Server在内存优化表的事务最后提交前会执行验证。由于数据变更无需锁参与,在相应的隔离级别上进行数据变更可能产生无效数据,因此通过验证防止产生无效数据。
下面的列表展示了不同隔离级别下可能出现的违规行为。在下一篇文章中会详细介绍隔离级别和并发控制,并介绍常见违规和提交依赖性。
在隔离级别SNAPSHOT下访问内存优化表,在提交(COMMIT)时可能出现以下错误:
- 如果当前事务插入行时使用的主键值与另一事务插入另一行时使用的相同,且该事务先于当前事务提交,则会出现错误41325(“由于在表x上的可重复读取验证失败当前事务未能提交”)并放弃该事务。
在隔离级别REPEATABLE READ下访问内存优化表,事务提交(COMMIT)时可能出现以下验证失败:
- 如果当前事务读取的行被其他事务更新并先于当前事务提交,则会出现错误41305(“由于在表x上的可重复读取验证失败当前事务未能提交”)并放弃该事务。
在隔离级别SERIALIZABLE下访问内存优化表,提交(COMMIT)时可能出现以下验证失败:
- 如果当前事务无法读取任何有效行或遇到SELECT,UPDATE或DELETE产生的幻影记录则会提交失败。事务需要在没有并发事务的情况下执行,所有操作在逻辑上都发生在单独序列点,如果违反该要求,则会出现错误41325且该事务会被放弃。
T-SQL支持
可以通过两种不同方式访问内存优化表:解析型Transact-SQL互操作或本地编译存储过程。
解析型T-SQL
互操作能力使内存优化表支持T-SQL几乎所有功能,但无法获得与本地编译存储过程相同的性能。在即席查询中互操作是合适的选择,向Hekaton迁移应用程序时或在在迁移性能关键的存储过程前也可以使用它。如果要同时访问内存优化表和基于磁盘的表,也要使用解析型T-SQL。
T-SQL通过互操作访问内存优化表不支持的功能有:
- TRUNCATE TABLE
- MERGE(以内存优化表作为目标)
- 动态游标和键集游标(这些会自动降级为静态游标)
- 跨数据库查询
- 跨数据库事务
- 链接服务器
- 锁提示:TABLOCK, XLOCK, PAGLOCK, NOLOCK等等
- 隔离级别提示:READUNCOMMITTED,READCOMMITTED 和 READCOMMITTEDLOCK
- CTP 1不支持内存优化表类型和表变量
本地编译存储过程中的T-SQL
本地编译存储过程是执行T-SQL及访问内存优化表最快的方式。但这种存储过程对Transact-SQL有诸多限制,而且对数据类型和排序规则的支持有限制。请参阅相应文档了解该功能对T-SQL语句、数据类型和操作的支持性。此外,本地编译存储过程无法访问基于磁盘的表。
由于数据库内部必须为每个表操作创建单独函数因此才产生这些限制,但后续版本会改进并扩大功能接口的覆盖范围。
不支持的功能
包括数据库镜像和复制等个别功能在Hekaton及含有内存优化数据表的数据库中不受支持,但AlwaysOn,日志传送,数据库备份和恢复等功能Hekaton可以完全支持,此外内存优化表还支持SQL Server Management Studio和SSIS。
了解功能支持情况,请查阅SQL Server Hekaton文档。
元数据
为了支持内存优化表和存储过程,现有的一些元数据对象已得到改进。
这些功能包括:
- OBJECTPROPERTY – 新增了TableIsMemoryOptimized属性
改进的系统视图包括:
- sys.data_spaces – 新增了type值FX,新增了type_desc值MEMORY_OPTIMIZED_DATA_FILEGROUP
-
sys.tables – 新增了3列:
- durability (0 或 1)
- durability_desc (SCHEMA_AND_DATA 和 SCHEMA_ONLY)
- is_memory_optimized (0 或 1)
- sys.table_types – 新增了is_memory_optimized列
- sys.indexes – 新增了type值7,新增了type_desc值NONCLUSTERED HASH
- sys.data_spaces – 新增了type值FX,新增了type_desc值MEMORY_OPTIMIZED_DATA_FILEGROUP
- sys.sql_modules 和 sys.all_sql_modules – 新增了uses_native_compilation列
此外,为了支持内存优化表还增加了一些新的元数据对象。
为了支持哈希索引CTP 1中增加了一个新的目录视图:sys.hash_indexes,该视图以sys.indexes为基础并且新增了一列,bucket_count列显示了相应索引中hash bucket的数量,除非删除并重建索引否则无法更改bucket_count。
以下是Hekaton新增的SQL Server动态管理视图,名称以sys.dm_db_xtp_* 开头的提供单一Hekaton数据库信息,名称以sys.dm_xtp_* 开头的提供实例信息。您可查阅文档获取这些对象的详细信息。后续发布的白皮书在讨论相关内容时会详细介绍相关的DMV,例如检查和检查点文件,事务及垃圾回收等。
- sys.dm_db_xtp_checkpoint
- sys.dm_db_xtp_checkpoint_files
- sys.dm_xtp_consumer_memory_usage
- sys.dm_db_xtp_gc_cycles_stats
- sys.dm_xtp_gc_stats
- sys.dm_xtp_memory_stats
- sys.dm_xtp_system_memory_consumers
- sys.dm_xtp_threads
- sys.dm_xtp_transaction_stats
- sys.dm_db_xtp_index_stats
- sys.dm_db_xtp_memory_consumers
- sys.dm_db_xtp_object_stats
- sys.dm_db_xtp_transactions
- sys.dm_db_xtp_table_memory_stats
使用AMR获得最佳实践
完成SQL Server Hekaton安装后,AMR(分析,迁移和报告)工具可以为您提供将哪些表和存储过程迁移至Hekaton的建议。
该工具通过数据采集器基于Management Data Warehouse并引入了新的报表类型,右键单击MDW数据库并选择“报表 | Management Data Warehouse”即可,您还能使用“事务性能分析概述(Transaction Performance Analysis Overview)”。
生成的报告包含了相关性能建议,告诉您哪个表可以在转换到内存优化表后获得最佳性能收益。同时根据当前正在使用的表报告不受支持的特性,描述需要多少工作量完成转换。例如:报告将指出在表定义中不受支持的数据类型及约束。
另一份报告会指出在内存优化表中将哪些存储过程转换为本地编译存储过程后能获取最佳收益。
实际上,Hekaton内存优化表同时支持解析型TSQL和本地编译存储过程,而且在基于磁盘的表上使用的查询也可以在Hekaton内存优化表中使用,因此向Hekaton的迁移可逐步迭代完成。根据Management Data Warehouse 报告给出的建议,您可以从最佳收益角度出发逐步完成向内存优化表的转换工作。当您见到收益就可继续转换其余表,但如果使用普通的Transact-SQL接口访问这些表,您的应用程序可能需要一些小的修改。
完成所有表的转换后,就可以考虑将存储过程重写并转换为本地编译存储过程,同样,您可以根据Management Data Warehouse 给出的报告从最佳收益的角度出发逐步完成该工作。
总结
开发代码名为Hekaton的SQL Server内存OLTP项目可以创建、使用并高效管理内存优化表,优化OLTP工作负载性能,提供了真正的多版本乐观并发控制,数据处理不再需要锁或闩锁。Hekaton中所有内存优化表至少要有一个索引,所有的访问都通过索引完成。在引用基于磁盘的表的事务中可以引用Hekaton内存优化表,只是有少数限制。本地编译存储过程无法访问基于磁盘的表,但能以最快速度访问内存优化表同时执行业务逻辑和计算。