SQL Server 内存 OLTP 中的事务是非常直接的。虽然我们并不会讨论一些可能的优化,但是它的基础设计模式是相当容易理解的,并且能够在其他的项目中重用。
SQL Server 内存 OLTP 中的事务依赖于一个类似时间戳的结构,被称为事务 ID。一个事务使用两个时间戳,一个用于操作的开始,另一个在提交事务的时候为它分配值。多个事务能够共享同一个开始值。
同样的,在内存中每一行记录的每一个版本都有一个开始和一个结束事务 ID。基本的规则是,一个事务仅能够读取满足 RowVersion.StartingId <= Transaction.StartingId < RowVersion.EndingId 条件的数据。
对于 DELETE 操作,行版本的结束 ID 首先会被设置成事务的开始 ID。然后会设置一个标记表明一个事务正在处理中。
UPDATE 操作的开始和 DELETE 操作相似,它会将之前的行版本设置为结束事务 ID。然后会创建一个新的行版本,它的开始事务 ID 等于事务的开始 ID。结束 ID 首先会被设置为无穷大,然后再次设置一个活动事务标记。旧的行版本还会获得一个指向新的行版本的指针。
INSERT 操作和 UPDATE 操作完全相同,但是不需要删除之前的行版本。
提交和验证
在提交阶段,首先会为当前事务分派一个唯一的事务 ID。然后会开始一个验证流程,该流程对受影响的记录进行隔离错误检查。错误的类型依赖于请求的事务隔离级别。内存优化表仅支持三个级别,快照(Snapshot)、可重复读(Repeatable Read)和序列化(Serializable)。
快照
和正常的表一样,如果在另一个事务试图插入新行的时候向内存优化表中插入数据也会失败。但是它失败的方式有一点不同。通常情况下,一个事务必须等待其他事务完成,之后失败的事务仅会看到重复行,但再也不会尝试插入操作。
但是在这里我们会看到两个事务都插入了它们自己的行。之后它们将会读回数据以便查看自己是否赢得了竞赛。如果没有,那么就会产生 41325 错误,对应的消息是“对表 [表名] 的可重复读验证失败,当前事务无法提交”。
可重复的读事务
对于可重复读隔离级别,MSDN 上有这样一个警告:“需要注意的一件重要的事情是,因为可重复读隔离级别是利用其他事务的阻塞来实现的,所以使用该隔离级别极大地增加了事务执行过程中所持有的锁的数量”。
因为内存优化表没有锁,所以针对它们的可重复读非常特别。它并不会阻塞其他的事务,而是重新读取事务最后的行。如果这些行有任何一行发生了变化,这个事务就会被中止。这种情况对应的错误码是 41305,对应的消息是“对表 [表名] 的可重复读验证失败,当前事务无法提交”。
序列化事务
和可重复读相似,序列化事务并没有使用传统的方式,通过锁避免让其他的事务对正在检查的数据产生干扰。相反的,它会进行检查以便清楚自己是否能够成功读取任意有效行或者是否遇到了不真实的行。这两种情况中的任何一种都会导致事务再次被中止。
提交处理
如果验证成功,那么每一个受影响的行版本的结束事务 ID 都将会被设置为事务的结束 ID。同样的,新的行版本(例如插入和更新所产生的)的开始 ID 会被设置为事务的结束 ID。活动标记会被清除,索引会被更新到指向新记录。
垃圾收集器
应该注意的是,并不需要从索引中移除指向旧的行版本的指针。也不需要立即将旧版本删除。
内存优化表要求使用引用计数垃圾收集器。详细信息现在还不得而知,但是基于设计的剩余部分我们能够预测出它的行为。GC 需要从索引处启动,检查哪些索引指向了过时的行。如果发现了这样的行,它就会减少引用计数并且更新索引使其指向最近的行版本。如果计数器变成了 0,那么行版本就会被删除。
关于垃圾收集器最棘手的部分是如何知道首先要查看哪些行。人们会推测对每一个索引的所有行进行简单的迭代,但这种方式的成本会相当高。
设计注意事项
在使用内存 OLTP 的时候,开发者需要对自己的访问模式有非常清晰的认识。如果在编写代码的时候没有避免重叠事务,那么就会导致隔离级别违规,产生比使用传统表更多的事务中止问题。
如果想要了解更多的信息,可以阅读 Kalen Delaney 的 SQL Server In-Memory OLTP Internals Overview for CTP1 。
评论