由一个bug引发的SQLite缓存一致性探索
问题
我们在生产环境中使用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中的数据都是二进制的,需要对内容进行解析产生结构化数据才能使用。为此,为了避免分析语句时,频繁解析获取数据字典,将解析好的数据进行缓存,以供多次使用,提高效率。
数据页缓存一致性
我们这里讨论的数据页缓存对应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句柄共享一个缓存区。其实问题很简单,但猜测复现问题还是花了一点精力。
由一个bug引发的SQLite缓存一致性探索的更多相关文章
- z-index失效原因分析——由一个bug引发的对层叠上下文和z-index属性的深度思考
新年刚开工就被一个bug虐得整个人都不好了,特地记录下. (一)bug描述 在一个fixed-data-table(一个React组件)制作的表格中,需要给表头的字段提示的特效,所以做了一个提示层,但 ...
- 修复 ThinkPHP3.2.3 抛出异常模块的一个BUG,关闭字段缓存功能
使用 ThinkPHP3.2.3 遇到一个奇怪的问题,正式环境上报错,提示 “页面错误!请稍后再试~” 为了查看到底出啥错误,哪里出错,于是在入口文件中加了一段代码,开启调试: defined('AP ...
- MyBatis 学习记录7 一个Bug引发的思考
主题 这次学习MyBatis的主题我想记录一个使用起来可能会遇到,但是没有经验的话很不好解决的BUG,在特定情况下很容易发生. 异常 java.lang.IllegalArgumentExceptio ...
- .net remoting和wcf自托管——一个bug引发的警示
一.解决问题,需要深入,并从细节入手,多从代码找原因,不能认为代码是死的,不会出错: 之前代码都运行良好,突然某一天,在我电脑上出问题了.出了问题,那就应该找出原因.其实这个问题,本身并不难,好歹给你 ...
- MySQL 5.6的一个bug引发的故障
突然收到告警,提示mysql宕机了,该服务器是从库.于是尝试登录服务器看看能否登录,发现可以登录,查看mysql进程也存在,尝试登录提示 ERROR (HY000): Too many connect ...
- Hexo next博客的pjax一个Bug引发的关于pjax用法的小技巧-----pjax后图片点击放大的js失效
文章目录 广告: 背景 发现 解决 get技能 广告: 本人博客地址:https://mmmmmm.me 源码:https://github.com/dataiyangu/dataiyangu.git ...
- CPU指令重排序与MESI缓存一致性
一.重排序场景 class ResortDemo { int a = 0; boolean flag = false; public void writer() { a = 1; //1 flag = ...
- sqlite在Android上的一个bug:SQLiteCantOpenDatabaseException when nativeExecuteForCursorWindow
更多内容在这里查看 https://ahangchen.gitbooks.io/windy-afternoon/content/ ::-/com.company.product W/System.er ...
- 一个小BUG引发的思考。(论开发与测试之间的那点事)
标题不是“一个馒头引发的血案”. 言归正传:今天上午测试的时候,发现了一个BUG,如图: 一个用肉眼就能发现的BUG.原因当然是因为开发同事没有自测试,流入到了测试人员这里了. 无非是开发同事不严谨造 ...
随机推荐
- [Intel Edison开发板] 03、Edison开发IDE入门及跑官方提供的DEMO
一.启动Eclipse爱迪生开发板IDE eclipse开发环境在iss-iot-win_03-14-16中,但是一定每次都是点bat脚本启动,否则就会少东西(windows->preferen ...
- linux笔记
1 动态查看日志 tail -f filename tail -1000f filename 2 解压当前目录内容为xxx.zip zip -r xxx.zip ./* 3 查看内存使用情况 fre ...
- 苹果台式一体机笔记本安装win双系统攻略教程
步骤 序:win系统下载 :http://www.itellyou.cn 选择要安装的系统进行下载,本文以win7为例 进入苹果系统,左上角——前往——实用工具——BootCamp 助理 点击继续 ...
- 使用自定义 classloader 的正确姿势
详细的原理就不多说了,网上一大把, 但是, 看了很多很多, 即使看了jdk 源码, 说了罗里吧嗦, 还是不很明白: 到底如何正确自定义ClassLoader, 需要注意什么 ExtClassLoade ...
- PHP 面向对象编程和设计模式 (2/5) - 静态变量、属性和方法及延迟绑定
PHP高级程序设计 学习笔记 2014.06.10 Static(静态)关键字用来定义静态方法和属性,static 也可用于定义静态变量以及后期静态绑定. 1.静态变量 static variable ...
- 数组去重 JS
我说的数组去重是这样的: var arr = ['f', 'a', 'b', 'd', 'e', 'g'] ; var str='f'; 去除arr中的str 最简单的是遍历arr与str做比较, ...
- Handler系列之原理分析
上一节我们讲解了Handler的基本使用方法,也是平时大家用到的最多的使用方式.那么本节让我们来学习一下Handler的工作原理吧!!! 我们知道Android中我们只能在ui线程(主线程)更新ui信 ...
- JavaScript 函数节流和函数去抖应用场景辨析
概述 也是好久没更新 源码解读,看着房价蹭蹭暴涨,心里也是五味杂陈,对未来充满恐惧和迷茫 ...(敢问一句你们上岸了吗) 言归正传,今天要介绍的是 underscore 中两个重要的方法,函数节流和函 ...
- 【NLP】Tika 文本预处理:抽取各种格式文件内容
Tika常见格式文件抽取内容并做预处理 作者 白宁超 2016年3月30日18:57:08 摘要:本文主要针对自然语言处理(NLP)过程中,重要基础部分抽取文本内容的预处理.首先我们要意识到预处理的重 ...
- DotLiquid模板引擎简介
DotLiquid是一个在.Net Framework上运行的模板引擎,采用Ruby的Liquid语法,这个语法广泛的用在Ruby on rails和Django等网页框架中. DotLiquid相比 ...