问题sql
咱们在生产环境中使用SQLite时中发现建表报“table xxx already exists”错误,但DB文件中并无该表。后面才发现这个是SQLite在实现过程当中的一个bug,而这个bug与数据字典的一致性相关,下面这篇文章主要讨论SQLite的缓存机制,以及缓存一致性实现的策略,但愿对你们了解SQLite缓存机制有必定的帮助。数据库
缓存缓存
SQLite中缓存主要包括两方面,数据字典缓存和数据页缓存。SQLite自己是一个文件数据库,全部的数据都在一个DB文件中,文件以块(page)的形式存放,默认状况下每一个page是1024个字节。为了不每次访问都产生磁盘IO,针对数据块在SQLite内部实现了一层缓存
pagecache,pagecache的做用就是缓存页数据。在SQLite内部,除了用户数据,还有一部份内容是元数据信息,包括表,视图,索引和触发器等,这部分元数据信息在数据库领域通常称为数据字典,这部分信息也存在DB文件中。因为每次执行语句时,都须要数据字典进行语义分析和执行计划优化(表是否存在,列是否存在,是否有索引可用,是否存在触发器等),若是每次获取这些信息时,都须要从DB文件中获取,则很是影响性能。你可能会说,不是已经有pagecache了吗?对的,数据字典的内容也缓存在pagecahce中,可是,要知道page中的数据都是二进制的,须要对内容进行解析产生结构化数据才能使用。为此,为了不分析语句时,频繁解析获取数据字典,将解析好的数据进行缓存,以供屡次使用,提升效率。cookie
数据页缓存一致性
咱们这里讨论的数据页缓存对应MySQL的概念就是BufferPool,固然其它数据库Oracle,SQLServer都有相似的概念。
传统PC上面的数据库,都是在数据库服务启动时,根据参数设定值一次性分配特定大小的BufferPool。而SQLite采用懒分配策略,即“用多少则分配多少”,pagecache默认大小是2000个page,2000个page能够认为是一个缓存的上限。一次性分配的好处是,内存在物理是连续的,不容易产生内存碎片;而懒分配则更节约内存,因为SQLite通常用于端设备,采用懒分配方式可能更经济实惠。SQLite的缓存分配策略采用LRU,保留最近访问的page,淘汰最老的page。
SQLite中每一个数据库链接对应一个DB句柄,应用经过DB句柄来操做数据库,而pagecache实际上就做为一个成员挂在DB句柄中,所以每一个DB句柄都有本身独立的缓存,这点与传统的PC数据库不一样(好比MySQL中,全部链接共享BufferPool)。既然每一个DB句柄有独立的缓存,那么缓存之间如何同步?好比有Connection1和Connection2两个链接,Connection1首先从文件中读取了page_A并加入到了缓存;随后Connection2也从文件中读取Page_A,并进行了更新;那么当Connection1再次读取page_A时,Connection1如何知道本身缓存的page_A已经不是最新了,须要从新到DB文件中读取?
SQLite为了处理这个问题,在DB的文件控制头中存放的DB的版本信息,开始执行SQL时会读取DB的版本信息并缓存,如何发现本次的版本信息与以前的不一样,则确认DB文件已经被修改,清理自身的缓存。每次事务提交时,都会调用pager_write_changecounter进行更新,具体位置在第一页的第24个字节,占4个字节。函数
数据字典缓存一致性
咱们这里讨论的数据字典对应MySQL的概念就是information_schema的系统表,字典缓存就是对系统表信息的结构化信息存储。在SQLite中字典信息采用Hash表存储,包括(tblHash,idxHash,trigHash和fkeyHash等)判断一个对象是否存在的依据是Hash表中对象是否存在。openDatabase函数经过调用sqlite3Init对数据字典进行初始化,并设置标记。与数据页缓存同样,字典缓存也是每一个DB句柄有单独的一份数据,一样的,SQLite文件头中一样存放了数据字典的版本信息,具体位置在第一页的第40个字节,占4个字节。进行DDL操做时(CREATE,DROP,ALTER等),会调用sqlite3ChangeCookie更新字典版本号(Schema cookie)。在Prepare阶段分析语句时,若发现对象不存在,会触发一次Schema cookie检查,若是数据字典不是最新,则会调用sqlite3SchemaClear进行清理,并从新加载数据字典。另外,SQLite的数据字典表很是简单,主要在sqlite_master表中,每一个对象都是一行记录,记录中包含了表定义,加载字典时,实际就是将表定义语句分析一遍,经过调用sqlite3EndTable将对象加入Hash表,很是方便。性能
小结
能够看到,不管数据页缓存也好,数据字典缓存也好,SQLite都是采用一个版本号来控制版本信息,很是简单实用,但缺点是粒度很是大。若是DB写很是频繁,那么每次读基本都会致使物理IO,可能修改的是A表,访问B表也须要将缓存清空。这里也能够解释为何页缓存是“懒加载”模式,这样清空缓存的代价也相对较小。对于数据字典缓存,粒度一样很粗,每修改一个表,视图,触发器等对象,都会触发数据字典版本更新。固然SQLite不会傻傻的每次执行SQL时都去判断本身的版本是否最新,只是在访问对象时,对象不存在的状况才去检查版本,这样在必定程度上减小了加载的次数,但这样也带来了问题,下面回到问题自己。优化
回到问题
前面咱们抛出了一个SQLite的bug,这里来细说前因后果。假设有两个DB句柄,分别称为A和B。执行以下序列: A:create table t(id int); B:DROP table if exists t; A: create table t(id int); 第二次A建表时会报“table t already exists”错误,而实际上表已经不存在了。这主要缘由就是第3步A建表时发现表存在并无触发去判断数据字典是否最新的逻辑,致使误报。复现该问题时要注意关闭sharecache,由于在sharecache模式下,全部的DB句柄共享一个缓存区。其实问题很简单,但猜想复现问题仍是花了一点精力。spa