本文主要介绍sqlite的事务模型,以及基于事务模型的一些性能优化tips,包括事务封装、WAL+读写分离、分库分表、page size优化等。并基于手淘sqlite的使用现状总结了部分常见问题及误区,主要集中在多线程的设置、多线程下性能优化的误区等。本文先提出如下几个问题(做者在进行统一存储的关系存储框架优化过程当中一直困惑的问题,同时也是客户端开发者常常搞错的问题)并在正文中进行解答:html
在深刻了解sqlite以前,最好先对sqlite的主要数据结构有个概要的理解,sqlite是一个很是完备的关系数据库系统,由不少部分组成(parser,tokenize,virtual machine等等),同时sqlite的事务模型相对简化,是入门学习关系数据库方法论的一个不错的选择;下文对事务模型的分析也基于这些核心数据结构。下面这张图比较准确的描述了sqlite的几个核心数据结构:linux
connection经过sqlite3_open函数打开,表明一个独立的事务环境(这里及下文提到的事务,包括显式声明的事务,也包括隐式的事务,即每条独立的sql语句)sql
B-Tree负责请求pager从disk读取数据,而后把页面(page)加载到页面缓冲区(page cache)数据库
Pager负责读写数据库,管理内存缓存和页面(即下文提到的page caches),以及管理事务,锁和崩溃恢复windows
关于建议锁(advisory lock)和强制锁(mandatory lock)缓存
典型的建议锁安全
sqlite的文件锁在linux/posix上基于记录锁实现,也就是说sqlite在文件锁上会有如下几个特色:性能优化
sqlite对每一个链接设计了五钟锁的状态(UNLOCKED, PENDING, SHARED, RESERVED, EXCLUSIVE), sqlite的事务模型中经过锁的状态保证读写事务(包括显式的事务和隐式的事务)的一致性和读写安全。sqlite官方提供的事务生命周期以下图所示,我在这里稍微加了一些我的的理解:数据结构
这里有几点须要注意:多线程
按照官方文档,WAL的原理以下:
对数据库修改是是写入到WAL文件里的,这些写是能够并发的(WAL文件锁)。因此并不会阻塞其语句读原始的数据库文件。当WAL文件到达必定的量级时(CheckPoint),自动把WAL文件的内容写入到数据库文件中。当一个链接尝试读数据库的时候,首先记录下来当前WAL文件的末尾 end mark,而后,先尝试在WAL文件里查找对应的Page,经过WAL-Index来对查找加速(放在共享内存里,.shm文件),若是找不到再查找数据库文件。
这里结合源码,有下面几个理解:
// 多线程的设置的实现:设置bCoreMutex和bFullMutex #if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 /* IMP: R-54466-46756 */ case SQLITE_CONFIG_SINGLETHREAD: { /* EVIDENCE-OF: R-02748-19096 This option sets the threading mode to ** Single-thread. */ sqlite3GlobalConfig.bCoreMutex = 0; /* Disable mutex on core */ sqlite3GlobalConfig.bFullMutex = 0; /* Disable mutex on connections */ break; } #endif #if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 /* IMP: R-20520-54086 */ case SQLITE_CONFIG_MULTITHREAD: { /* EVIDENCE-OF: R-14374-42468 This option sets the threading mode to ** Multi-thread. */ sqlite3GlobalConfig.bCoreMutex = 1; /* Enable mutex on core */ sqlite3GlobalConfig.bFullMutex = 0; /* Disable mutex on connections */ break; } #endif #if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE>0 /* IMP: R-59593-21810 */ case SQLITE_CONFIG_SERIALIZED: { /* EVIDENCE-OF: R-41220-51800 This option sets the threading mode to ** Serialized. */ sqlite3GlobalConfig.bCoreMutex = 1; /* Enable mutex on core */ sqlite3GlobalConfig.bFullMutex = 1; /* Enable mutex on connections */ break; } #endif
if( isThreadsafe ){ // bFullMutex = 1 db->mutex = sqlite3MutexAlloc(SQLITE_MUTEX_RECURSIVE); // 每一个数据库链接会初始化一个成员锁 if( db->mutex==0 ){ sqlite3_free(db); db = 0; goto opendb_out; } }
/* If the xMutexAlloc method has not been set, then the user did not ** install a mutex implementation via sqlite3_config() prior to ** sqlite3_initialize() being called. This block copies pointers to ** the default implementation into the sqlite3GlobalConfig structure. */ sqlite3_mutex_methods const *pFrom; sqlite3_mutex_methods *pTo = &sqlite3GlobalConfig.mutex; if( sqlite3GlobalConfig.bCoreMutex ){ pFrom = sqlite3DefaultMutex(); }else{ pFrom = sqlite3NoopMutex(); } pTo->xMutexInit = pFrom->xMutexInit; pTo->xMutexEnd = pFrom->xMutexEnd; pTo->xMutexFree = pFrom->xMutexFree; pTo->xMutexEnter = pFrom->xMutexEnter; pTo->xMutexTry = pFrom->xMutexTry; pTo->xMutexLeave = pFrom->xMutexLeave; pTo->xMutexHeld = pFrom->xMutexHeld; pTo->xMutexNotheld = pFrom->xMutexNotheld; sqlite3MemoryBarrier(); pTo->xMutexAlloc = pFrom->xMutexAlloc;
sqlite3_mutex_methods const *sqlite3NoopMutex(void){ static const sqlite3_mutex_methods sMutex = { noopMutexInit, noopMutexEnd, noopMutexAlloc, noopMutexFree, noopMutexEnter, noopMutexTry, noopMutexLeave, 0, 0, }; return &sMutex; } // CoreMutext未打开的话,对应使用的锁函数均为空实现 static int noopMutexInit(void){ return SQLITE_OK; } static int noopMutexEnd(void){ return SQLITE_OK; } static sqlite3_mutex *noopMutexAlloc(int id){ UNUSED_PARAMETER(id); return (sqlite3_mutex*)8; } static void noopMutexFree(sqlite3_mutex *p){ UNUSED_PARAMETER(p); return; } static void noopMutexEnter(sqlite3_mutex *p){ UNUSED_PARAMETER(p); return; } static int noopMutexTry(sqlite3_mutex *p){ UNUSED_PARAMETER(p); return SQLITE_OK; } static void noopMutexLeave(sqlite3_mutex *p){ UNUSED_PARAMETER(p); return; }
粗略看了一下,经过db->mutex(sqlite3_mutex_enter(db->mutex);)保护的逻辑块和函数主要以下列表:
sqlite3_db_status、sqlite3_finalize、sqlite3_reset、sqlite3_step、sqlite3_exec、 sqlite3_preppare_v二、column_name、blob操做、sqlite3Close、sqlite3_errmsg...
基本覆盖了全部的读、写、DDL、DML,也包括prepared statement操做;也就是说,在未打开FullMutex的状况下,在一个链接上的全部DB操做必须严格串行执行,包括只读操做。
sqlite3中的mutex操做函数,除了用于操做db->mutex这个成员以外,还主要用于如下逻辑块(主要是影响数据库全部链接的逻辑):
shm操做(index for wal)、内存池操做、内存缓存操做等
由#2.2的分析可知,写操做会在RESERVED状态下将数据更改、b-tree的更改、日志等写入page cache,并最终flush到数据库文件中;使用事务的话,只须要一次对DB文件的flush操做,同时也不会对其余链接的读写操做阻塞;对比如下两种数据写入方式(这里以统一存储提供的API为例),实测耗时有十几倍的差距(固然对于频繁的读操做,使用事务能够减事务状态的切换,也会有一点点性能提高):
// batch insert in transaction with 1000000 records // AliDBExecResult* execResult = NULL; _database->InTransaction([&]() -> bool { // in transaction auto statement = _database->PrepareStatement("INSERT INTO table VALUES(?, ?)"); for (auto record : records) { // bind 1000000 records // bind record ... ... statement->AddBatch(); } auto result = statement->ExecuteUpdate(); return result->is_success_; }); // batch insert with 1000000 records, no transaction // auto statement = _database->PrepareStatement("INSERT INTO table VALUES(?, ?)"); for (auto record : records) { // bind 1000000 records // bind record ... ... statement->ExecuteUpdate(); }
启用WAL以后,数据库大部分写操做变成了串行写(对WAL文件的串行操做),对写入性能提高有很是大的帮助;同时读写操做能够互相彻底不阻塞(如#2.3所述)。上述两点比较好的解释了启用WAL带来的提高;同时推荐一个写链接 + 多个读链接的模型,以下图所示:
全部的写操做、显式事务操做都使用同一个链接,且全部的写操做、显式事务操做都串行执行
// two transactions: void Transaction_1() { connection_->Exec("BEGIN"); connection_->Exec("insert into table(value) values('xxxx')"); connection_->Exec("COMMIT"); } void Transaction_2() { connection_->Exec("BEGIN"); connection_->Exec("insert into table(value) values('xxxx')"); connection_->Exec("COMMIT"); } // code fragment 1: concurrent transaction thread1.RunBlock([]() -> void { for (int i=0; i< 100000; i++) { Transaction_1(); } }); thread2.RunBlock([]() -> void { for (int i=0; i< 100000; i++) { Transaction_2(); } }); thread1.Join(); thread2.join(); // code fragment 2: serial transaction for (int i=0; i< 100000; i++) { Transaction_1(); } for (int i=0; i< 100000; i++) { Transaction_2(); }
如#2.3提到,过大的WAL文件,会让查找操做从B-Tree查找退化成线性查找(WAL中page连续存储);但大的WAL文件对写操做较友好。对于大记录的写入操做,较大的wal size会有效提升写入效率,同时不会影响查询效率
分库分表能够有效提升数据操做的并发度;但同时过多的表会影响数据库文件的加载速度。如今数据库方向的不少研究包括Auto sharding, paxos consensus, 存储和计算的分离等;Auto
application-awared optimization,Auto hardware-awared optimization,machine
learning based optimization也是不错的方向。
包括WAL checkpoint策略、WAL size优化、page size优化等,均须要根据具体的业务场景设置。
按照sqlite文档,sqlite线程安全模式有如下三种:
SQLITE_CONFIG_SINGLETHREAD(单线程模式)
SQLITE_CONFIG_MULTITHREAD(多线程模式)
SQLITE_CONFIG_SERIALIZED(串行模式)
产生这个误区主的主要缘由是官方文档里的最后一句话:
SQLite will be safe to use in a multi-threaded environment as long as no two threads attempt to use the same database connection at the same time.
但你们每每忽略了前面的一句话:
it disables mutexing on database connection and prepared statement objects
即对于单个链接的读、写操做,包括建立出来的prepared statement操做,都没有线程安全的保护。也即在多线程模式下,对单个链接的操做,仍须要在业务层进行锁保护。
关于这一点,#2.4给出了具体的解释;多线程模式下(SQLITE_CONFIG_MULTITHREAD)对prepared statement、connection的操做都不是线程安全的
这个问题比较笼统;即便在串行模式下,全部的数据库操做仍需遵循事务模型;而事务模型已经将数据库操做的锁进行了很是细粒度的分离,串行模式的锁也是在上层保证了事务模型的完整性
多线程模式下,仍须要业务上层进行锁保护,串行模式则是在sqlite内部进行了锁保护;认为多线程模式性能好的兄弟哪来的自信认为业务层的锁实现比sqlite内部锁实现性能更高?
本文为云栖社区原创内容,未经容许不得转载。