sqlite事务模型、性能优化tips、常见误区

0,前言

本文主要介绍sqlite的事务模型,以及基于事务模型的一些性能优化tips,包括事务封装、WAL+读写分离、分库分表、page size优化等。并基于手淘sqlite的使用现状总结了部分常见问题及误区,主要集中在多线程的设置、多线程下性能优化的误区等。本文先提出如下几个问题(做者在进行统一存储的关系存储框架优化过程当中一直困惑的问题,同时也是客户端开发者常常搞错的问题)并在正文中进行解答:html

  • 1,sqlite的多进程安全是怎么实现的?性能如何?
  • 2,sqlite的数据库链接是什么?
  • 3,言sqlite必提的读写分离,具体指什么?能不能提高数据读写的性能?为何
  • 4,sqlite提供的WAL特性解决了什么问题?
  • 5,sqlite的多线程设置是为了解决什么问题?与读写分离有什么关系?
  • 6,什么状况下数据库会发生死锁?
  • 7,有哪些性能优化的方案?

1,sqlite主要数据结构

在深刻了解sqlite以前,最好先对sqlite的主要数据结构有个概要的理解,sqlite是一个很是完备的关系数据库系统,由不少部分组成(parser,tokenize,virtual machine等等),同时sqlite的事务模型相对简化,是入门学习关系数据库方法论的一个不错的选择;下文对事务模型的分析也基于这些核心数据结构。下面这张图比较准确的描述了sqlite的几个核心数据结构:linux

1.1 Connection

connection经过sqlite3_open函数打开,表明一个独立的事务环境(这里及下文提到的事务,包括显式声明的事务,也包括隐式的事务,即每条独立的sql语句)sql

1.2 B-Tree

B-Tree负责请求pager从disk读取数据,而后把页面(page)加载到页面缓冲区(page cache)数据库

1.3 Pager

Pager负责读写数据库,管理内存缓存和页面(即下文提到的page caches),以及管理事务,锁和崩溃恢复windows

2,sqlite事务模型及锁

2.1 sqlite多进程安全及Linux & windows文件锁

  • 关于建议锁(advisory lock)和强制锁(mandatory lock)缓存

    • 建议锁并不禁内核强制实行,若是有进程不检查目标文件是否已经由别的进程加了锁就往其中写入数据,内核也不会加以阻拦。所以,建议锁并不能阻止进程对文件的访问,而是须要进程事先对锁的状态作一个约定,并根据锁的当前状态和相互关系来肯定其余进程是否能对文件执行指定的操做
    • 强制锁是由内核强制采用的文件锁——因为内核对每一个read()和write()操做都会检查相应的锁,会下降系统性能
  • 典型的建议锁安全

    • 锁文件;锁文件是最简单的对文件加锁的方法,每一个须要加锁的数据文件都有一个锁文件(lock file)。但这种方式存在比较大的问题是没法强制保护须要加锁的文件,而且当加锁进程非正常退出以后,会形成其余进程的死锁
    • 记录锁;System V和BSD4.3引入了记录锁,相应的系统调用为lockf()和flock()。而POSIX对于记录锁提供了另一种机制,其系统调用为fcntl()。记录锁和锁文件有两个很重要的区别:1)记录锁能够对文件的任何一部分加锁,这对DBMS有极大的帮助,2)记录锁的另外一个优势就是它由进程持有,而不是文件系统持有,当进程结束时,全部的锁也随之释放。对于一个进程自己而言,多个锁毫不会冲突。(Windows中的锁都是强制锁,具体不是很熟,只知道在因为windows上文锁的限制,sqlite多进程下的并发性会受影响)

2.1.1 结论

sqlite的文件锁在linux/posix上基于记录锁实现,也就是说sqlite在文件锁上会有如下几个特色:性能优化

  • 多进程使用安全,且不会由于进程异常退出引起死锁
  • 单进程使用性能几乎不会受损,多进程使用的性能损耗会受必定的影响

2.2 事务模型(Without WAL)

sqlite对每一个链接设计了五钟锁的状态(UNLOCKED, PENDING, SHARED, RESERVED, EXCLUSIVE), sqlite的事务模型中经过锁的状态保证读写事务(包括显式的事务和隐式的事务)的一致性和读写安全。sqlite官方提供的事务生命周期以下图所示,我在这里稍微加了一些我的的理解:数据结构

这里有几点须要注意:多线程

  • UNLOCKED、PENDING、SHARED、RESERVED状态是非独占的,也就是说同一个链接中多个线程并发只读不会被阻塞。
  • 写操做的数据修改会先写入page cache,内容包括journal日志、b-tree的修改等;正是因为page cache的存在,不少耗时的“重”操做均可以不干扰其余链接和当前链接的读操做,真正意义上保证了sqlite能够同时处理一个写链接和多个读链接。
  • 链接由RESERVED状态进入EXCLUSIVE状态,须要等待读线程释放SHARED锁,也即写操做会被读操做阻塞
  • 链接由RESERVED状态进入EXCLUSIVE状态后(显式或隐式的调用commit),数据库进入独占状态,其余任何链接都没法由UNLOCK状态进入SHARED状态;也即写操做会阻塞全部链接的读操做(不包括已经进入SHARED状态的操做),直到page caches写入数据库文件(成功或失败)。
  • 数据库独占状态越久,其余操做的等待时间越久,即SQLITE_BUSY产生的一个缘由

2.2.1 结论

  • 对于常规的事务模型(without WAL),读写(链接)分离,不一样链接或同一个链接上的读和写操做仍互相阻塞,对性能提高没有明显帮助
  • 写事务在拿到reserve锁以前在page cache里的操做不会影响其余链接的读写,因此使用事务进行批量数据的更新操做有很是大的性能优点
  • 事务模型存在死锁的场景,以下图所示:

2.3 WAL对事务模型的影响

按照官方文档,WAL的原理以下:

对数据库修改是是写入到WAL文件里的,这些写是能够并发的(WAL文件锁)。因此并不会阻塞其语句读原始的数据库文件。当WAL文件到达必定的量级时(CheckPoint),自动把WAL文件的内容写入到数据库文件中。当一个链接尝试读数据库的时候,首先记录下来当前WAL文件的末尾 end mark,而后,先尝试在WAL文件里查找对应的Page,经过WAL-Index来对查找加速(放在共享内存里,.shm文件),若是找不到再查找数据库文件。

这里结合源码,有下面几个理解:

  • 数据的写操做写入WAL的过程再也不须要SHARED锁、EXCLUSIVE锁,而是须要WAL文件锁
  • 数据的写操做不会被读操做阻塞(写操做再也不须要SHARED锁)
  • 数据的读操做不会被写操做阻塞(写操做再也不须要独占数据库)
  • WAL文件写入数据库文件的过程,依然会被读操做阻塞,也会阻塞读操做
  • WAL文件的大小设置很关键,过大的WAL文件,会让查找操做从B-Tree查找退化成线性查找(WAL中page连续存储);但大的WAL文件对写操做较友好。

2.3.1 结论

  • 只有开了WAL,再使用读写(链接)分离才能有较大的性能提高
  • WAL本质上是将部分随机写操做(数据库文件和journal日志)变成了串行写WAL文件,并进行了锁分离
  • WAL文件的大小设置很关键,过大的WAL文件,会让查找操做从B-Tree查找退化成线性查找(WAL中page连续存储);但大的WAL文件对写操做较友好

2.4 多线程设置

  • 多线程是sqlite使用过程当中比较容易误解的一个概念,带来的问题要么是产生各类线程安全问题,要么是没法充分发掘sqlite的性能,这里结合代码咱们简单分析一下并给出几个重要结论。
  • 线程安全设置主要在设置bCoreMutex和bFullMutex,启用bFullMutex以后数据库链接和prepared statement都已加锁(社区各类文档都到此为止);但仍是感受不够清晰:这两个锁会对咱们使用sqlite有怎样的影响?best practice又是什么?
// 多线程的设置的实现:设置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
  • 若是FullMutex打开,则每一个数据库链接会初始化一个互斥量成员(db->mutex),也就是社区各类文档上所说的“bFullMutex是对链接的线程保护”
if( isThreadsafe ){    // bFullMutex = 1
    db->mutex = sqlite3MutexAlloc(SQLITE_MUTEX_RECURSIVE);    // 每一个数据库链接会初始化一个成员锁
    if( db->mutex==0 ){
      sqlite3_free(db);
      db = 0;
      goto opendb_out;
    }
  }
  • 若是CoreMutex打开,则会设置全局的锁控制函数
/* 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;
  • 而CoreMutext未打开的话,sqlite3NoopMutex()的实现以下(CoreMutext未打开的话,对应使用的锁函数均为空实现):
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; }
  • FullMutex保护了什么?

粗略看了一下,经过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操做必须严格串行执行,包括只读操做。

  • CoreMutex保护了什么?

sqlite3中的mutex操做函数,除了用于操做db->mutex这个成员以外,还主要用于如下逻辑块(主要是影响数据库全部链接的逻辑):

shm操做(index for wal)、内存池操做、内存缓存操做等

2.4.1 结论

  • 多线程设置是决定DDL、DML、WAL(包括SHM)操做是否线程安全的设置
  • 多线程设置与读写(链接)分离没有任何关系,并非实现读写(链接)分离的必要条件(不少人对这一点有误解)

3,性能优化tips

3.1 合理使用事务

由#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();
}

3.2 启用WAL + 读写(链接)分离

启用WAL以后,数据库大部分写操做变成了串行写(对WAL文件的串行操做),对写入性能提高有很是大的帮助;同时读写操做能够互相彻底不阻塞(如#2.3所述)。上述两点比较好的解释了启用WAL带来的提高;同时推荐一个写链接 + 多个读链接的模型,以下图所示:

3.2.1 读写链接分离的细节

  • 读操做使用不一样的链接并发执行,能够彻底避免因为显式事务、写操做之间的锁竞争带来的死锁
  • 全部的写操做、显式事务操做都使用同一个链接,且全部的写操做、显式事务操做都串行执行

    • 能够彻底避免因为显式事务、写操做之间的锁竞争带来的死锁,如#2.2.1提到的死锁的例子
    • 并发写并不能有效的提升写入效率,参考以下伪代码,哪段执行更快?
// 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();
}

3.3 针对具体业务场景,设置合适的WAL SIZE

如#2.3提到,过大的WAL文件,会让查找操做从B-Tree查找退化成线性查找(WAL中page连续存储);但大的WAL文件对写操做较友好。对于大记录的写入操做,较大的wal size会有效提升写入效率,同时不会影响查询效率

3.4 针对业务场景分库分表

分库分表能够有效提升数据操做的并发度;但同时过多的表会影响数据库文件的加载速度。如今数据库方向的不少研究包括Auto sharding,  paxos consensus, 存储和计算的分离等;Auto
application-awared optimization,Auto hardware-awared optimization,machine
learning based optimization也是不错的方向。

3.5 其余

包括WAL checkpoint策略、WAL size优化、page size优化等,均须要根据具体的业务场景设置。

4,常见问题 & 误区

4.1 线程安全设置及误区

  • sqlites configuration options: https://sqlite.org/c3ref/c_config_getmalloc.html
  • 按照sqlite文档,sqlite线程安全模式有如下三种:

    • SQLITE_CONFIG_SINGLETHREAD(单线程模式)

      • This option sets the threading mode to Single-thread. In other words, it disables all mutexing and puts SQLite into a mode where it can only be used by a single thread.
    • SQLITE_CONFIG_MULTITHREAD(多线程模式)

      • This option sets the threading mode to Multi-thread. In other words, it disables mutexing on database connection and prepared statement objects. The application is responsible for serializing access to database connections and prepared statements. But other mutexes are enabled so that 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.
    • SQLITE_CONFIG_SERIALIZED(串行模式)

      • This option sets the threading mode to Serialized. In other words, this option enables all mutexes including the recursive mutexes on database connection and prepared statement objects. In this mode (which is the default when SQLite is compiled with SQLITE_THREADSAFE=1) the SQLite library will itself serialize access to database connections and prepared statements so that the application is free to use the same database connection or the same prepared statement in different threads at the same time.

4.1.1 误区一:多线程模式是线程安全的

产生这个误区主的主要缘由是官方文档里的最后一句话:

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操做,都没有线程安全的保护。也即在多线程模式下,对单个链接的操做,仍须要在业务层进行锁保护。

4.1.2 误区二:多线程模式下,并发读操做是安全的

关于这一点,#2.4给出了具体的解释;多线程模式下(SQLITE_CONFIG_MULTITHREAD)对prepared statement、connection的操做都不是线程安全的

4.1.3 误区三:串行模式下,全部数据库操做都是串行执行

这个问题比较笼统;即便在串行模式下,全部的数据库操做仍需遵循事务模型;而事务模型已经将数据库操做的锁进行了很是细粒度的分离,串行模式的锁也是在上层保证了事务模型的完整性

4.1.4 误区四:多线程模式性能最好,串行模式性能差

多线程模式下,仍须要业务上层进行锁保护,串行模式则是在sqlite内部进行了锁保护;认为多线程模式性能好的兄弟哪来的自信认为业务层的锁实现比sqlite内部锁实现性能更高?

5,参考文献

  1. sqlite configuration reference, https://sqlite.org/c3ref/c_config_getmalloc.html
  2. 深刻理解sqlite, https://www.kancloud.cn/kangdandan/sqlite


本文做者:hamsongliu

阅读原文

本文为云栖社区原创内容,未经容许不得转载。

相关文章
相关标签/搜索