二、数据都在内存里面,那宕机或者断电了,数据不是没有了吗?
答案:不是。
内存数据库通过两种方式保证数据的持久性:事务日志和chcekpoint。
(1)事务日志
内存数据库的"写日志"和"写数据"在一个事务中进行,在事务执行期间,SQL会先"写数据"然后在才"写日志",这点与传统数据库不同,在传统数据库中,不管是在内存中还是磁盘中,"写数据"总是在"写日志"之后,也就是通常所说的WAL(Write-Ahead Transaction Log)。但是,在事务提交时,内存数据库和传统数据库在"写日志"上没有什么区别:日志会先于数据写入到磁盘中。
因此,即使服务器发生了宕机或者断电,下次数据库重启时会按照已经保存在磁盘中事务日志将业务redo(重做),所以不要担心数据会丢失。
另外,需要补充的是,内存数据库只会对持久性表将已提交的事物日志保存到磁盘中。这样做的好处可以减少写磁盘的次数。内存数据库支持频繁、快速的增、删、改等操作,这个强度远远高于传统数据库,数据库需要为每笔操作写日志,这样就会产生大量磁盘IO,写日志操作将有可能成为性能瓶颈,不记录未提交的事务日志就减少写日志的数量,从而可以提高数据库的性能。
有同学会想,不记录未提交事务的日志会不会导致数据不一致呢?
肯定不会,因为日志在写入磁盘前不可能发生先把"脏数据"写入到磁盘的现象(下面介绍checkpoint的时候会介绍原因)。
(2)CheckPoint
在内存数据库中,CheckPoint的主要目的就是将内存中的"数据"写入到磁盘中,从而在数据库崩溃或者重启时减少数据恢复的时间。不需要数据库逐条读取所有的日志来恢复数据。默认情况下Checkpoint是周期性进行的,当日志至上次checkpoint后增加了512M时会触发新一轮CheckPoint。
在传统数据库这种,Checkpoint可以将未提交的数据flush到磁盘的mdf文件中,这个现象在内存数据库中不会发生,因为内存数据库只将已提交事务的日志,而在写日志(到磁盘)之前不可能将数据先写到磁盘中,因此可以保证写到磁盘中的数据一定是已提交事务的数据。
三、数据在内存是怎么存放的,还是按照页的方式吗,一行的大小有限制吗?
答案:不是按照页的方式,一行的限制大小为8060Bytes。
内存优化表是基于行版本存储的,同一行在内存中会有多个版本,可以将内存优化表的存储结构看作是该表中 所有行的多个行版本的集合。
内存优化表中的行跟传统数据库的行结构是不一样的,下图描述了内存优化表中一行的数据结构:
在内存优化表中,一行有两个大部分组成:Row header和Row body,
Row header记录这个行的有效期(开始时间戳和结束时间戳)和索引指针
Row body记录了一行的实际数据。
在内存优化表中,行版本的数量是由针对该行的操作次数决定的,比如:每更新一次,就会新产生一行,增加一个行版本,新行有新的开始时间戳,新行产生后,原来的数据行会自动填充结束时间戳,意味这行已经过期。
备注:上图实际上只有3行,第1行有3个行版本,第2行有2个行版本,第3行有4个行版本。
既然同一行在内存中存在这么多的行版本,那数据库在访问时是怎么控制的呢?
在传统数据库中,表中每一行都是唯一的,一个事务如想找到一行,通过文件号、页号、槽位就可以了。
在内存数据库中,每一行有多个行版本,一个事务不可能对将每个行版本都操作一遍,实际上,一个事物只能操作同一行的一个行版本,至于它能对哪个行版本进行操作,取决于事务执行时间是否在这行的两个时间戳之间。除此之外的其他行版本对该事务而言是不可见的。
由于一行可能存在多个行版本,大家可能会提出这样一个疑问:每行都有这么多行版本,一张上百万行的表,内存哪够呀。不用担心,前文介绍过了,每个行实际上是有时间戳的,对于已经打上结束时间戳且没有活动事务访问的行,SQL Server会通过garbage collection机制回收它占用的内存,从而节省内存。所以不要担心内存不够。
四、内存数据库号称无锁式设计,那如果发生了并发冲突怎么办,SQL是如何处理冲突的呢?
答案:内存数据库用行版本来处理冲突。
锁的一个重要作用就是避免多个进程同时修改数据,从而造成数据不一致。常见的冲突现象包括读写互锁和写写互锁。那内存数据库是如何通过行版本来解决这两种锁定现象的呢?
(1)读写互锁
在内存数据库中,所有对内存优化表的事务隔离都是基于快照的,准确的说是基于行的快照。从上文行的 结构可以知道,每行的行头包括开始时间戳和结束时间戳的,一个事务能不能访问到这行关键在于事务的启动时间是不是在这行的两个时间戳内。
如果某个事务正在修改一行(快照),但还未提交到内存优化表中,也就是说"新行"还没有结束时间戳,对"读事务"而言,它读还是是原来行(快照),因此不会存在脏读的现象。
(2)写写互锁
两个事务同时更新一行时,就会发生写写互锁。
内存数据库冲突发生的概率比传统数据库小很多,但如果实在遇到了冲突,只能调整应用程序,在应用程序中加入"重试逻辑"(等待一会,然后再重新发起事务)来解决。
或许有同学觉得这种方式好像也没有什么大的性能改变。其实不然,举个例子,在传统数据库中一个锁可能将整个表都管住了,在表锁期间只能等待这个事务做完才能执行其他事务,而实际上这个事务可能只是修改了小部分行,因为表锁的存在,其他行那些不需要被这个事务操作的行。但内存数据库中写写冲突总是发生在行级别的,这个粒度小多了,影响没这么大。