SQLite is an in-process library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine.

SQLite is an embedded SQL database engine. Unlike most other SQL databases, SQLite does not have a separate server process. SQLite reads and writes directly to ordinary disk files. A complete SQL database with multiple tables, indices, triggers, and views, is contained in a single disk file.

SQLite,正如其名(SQL + light),是一个比较轻量级的支持SQL查询语言的数据库,它的数据就存储在文件系统的文件中(或者单纯的在内存中),因此创建数据库连接时(这种情况更像是初始化),只需要指定目标文件即可,同时也因为这种设计,它并没有像一些大型数据库那样提供一个TCP server来供远程调用(serverless),更不用说分布式了。因此,只有存储数据库文件的那台机器能够创建数据库连接,其他机器想要连接上该数据库只有进行文件的mount才有可能(当然并不建议这么做,同时操作同一个文件会有风险)。

SQLite is a compact library. With all features enabled, the library size can be less than 500KiB, depending on the target platform and compiler optimization settings.

SQLite非常适合用于嵌入式设备等存储和性能要求比较低的系统(比如手机)。

因为SQLite是基于文件来操作的,文件的写入肯定是要加锁的,虽然SQLite对并发性做了一系列优化,但这始终会是一个瓶颈,所以不建议把SQLite当做中大型服务端的数据库来使用。

Programming Language Binding for SQLite

SQLite安装完毕后可以直接通过它提供的命令行来对数据库进行操作,并且官方也提供了C++的API。除了官方提供的C++的绑定外,在其他语言上面我们可以使用一些第三方实现的库,比如JavaScript中的node-sqlite3。因为SQLite是serverless的,所以各个语言的绑定其实就是实现了在SQLite提供的规则下对数据库文件进行操作(很多其实都不用亲自实现,可以和官方提供的C++库进行混合编程)。

通过这些语言层面的绑定的库,我们可以在不同的编程语言内实现对SQLite进行操作,当然各种操作使用的还是SQL语言。比如:

var sqlite3 = require('sqlite3').verbose();

const databaseUrl = 'sqlite:database.sqlite';
var db = new sqlite3.Database(databaseUrl);

db.serialize(function() {
  db.run("CREATE TABLE lorem (info TEXT)");

  var stmt = db.prepare("INSERT INTO lorem VALUES (?)");
  for (var i = 0; i < 10; i++) {
      stmt.run("Ipsum " + i);
  }
  stmt.finalize();

  db.each("SELECT rowid AS id, info FROM lorem", function(err, row) {
      console.log(row.id + ": " + row.info);
  });
});

db.close();

其中创建一个表依然是将CREATE TABLE的SQL符串传递到数据库执行SQL的API中,而不是使用类似db.createTable这样一个函数。这其实带来了一些问题,比如一不小心代码中有SQL注入的风险。

ORM

没有什么计算机问题是添加一个中间层解决不了的,如果有,那就再添加一层。

ORM (Object Relational Mapping) 就是在上面的数据库API上面添加了一层,从而屏蔽了直接写SQL容易出现的一些问题。

同样以Node.js为例,一个比较著名的ORM库叫做sequelize,它对好几种数据库都进行了统一的封装。使用它来实现和上面一样的操作:

const Sequelize = require('sequelize');

const databaseUrl = 'sqlite:database.sqlite';
const sequelize = new Sequelize(databaseUrl);
const Lorem = sequelize.define('lorem', {
    info: {
        type: Sequelize.STRING
    },
});

async function createLoremTable() {
    // force: true will drop the table if it already exists
    await Lorem.sync({force: true});
    for (let i = 0; i < 10; ++i) {
        await Lorem.create({
            info: `Ipsum ${i}`,
        });
    }
}

async function main() {
    await createLoremTable();
    const info = await Lorem.findAll();
    info.forEach((item, index) => {
        console.log(index + ":" + item.get('info'));
    });
}

main().catch(error => console.log(error));

可以看到,sequelize中:

  • 所有SQL查询都被封装成了函数;
  • 所有数据库操作都是异步的;
  • 所有数据库操作都支持Promise,以及async/await。

Concurrency

开头也说过并发性会是SQLite的一个瓶颈,但是不是因此它就完全不适合作为服务端的数据库呢?

实践中,在我执行上述示例代码时,同时用Webstorm的DB Navigator打开了要Drop的那张表就发生了下面的错误:

Executing (default): DROP TABLE IF EXISTS `lorems`;
Executing (default): DROP TABLE IF EXISTS `lorems`;
Executing (default): DROP TABLE IF EXISTS `lorems`;
Executing (default): DROP TABLE IF EXISTS `lorems`;
Executing (default): DROP TABLE IF EXISTS `lorems`;
{ SequelizeTimeoutError: SQLITE_BUSY: database is locked
    at Query.formatError (/Users/CYu/Code/Javascript/javascript-demo/node_modules/sequelize/lib/dialects/sqlite/query.js:415:16)
    at Statement.afterExecute (/Users/CYu/Code/Javascript/javascript-demo/node_modules/sequelize/lib/dialects/sqlite/query.js:119:32)
    at Statement.replacement (/Users/CYu/Code/Javascript/javascript-demo/node_modules/sqlite3/lib/trace.js:19:31)
    at Statement.replacement (/Users/CYu/Code/Javascript/javascript-demo/node_modules/sqlite3/lib/trace.js:19:31)
  name: 'SequelizeTimeoutError',
  parent: 
   { Error: SQLITE_BUSY: database is locked
     errno: 5,
     code: 'SQLITE_BUSY',
     sql: 'DROP TABLE IF EXISTS `lorems`;' },
  original: 
   { Error: SQLITE_BUSY: database is locked
     errno: 5,
     code: 'SQLITE_BUSY',
     sql: 'DROP TABLE IF EXISTS `lorems`;' },
  sql: 'DROP TABLE IF EXISTS `lorems`;' }

可以看到,在经过多次尝试后操作仍然失败了,原因是数据库加锁了。但这并不完全表明SQLite的并发能力就很差。因为我用DB Navigator打开表之后是可以直接修改表单内容的,因此SQLite给数据库一直上着锁。而如果只是要对数据库内容进行查询,即读操作是不会被加锁的。并且通常写操作的耗时会非常短(毫秒级),(对一个中小型站点而言)发生同时去写的可能性也不大,况且sequelize在ORM层还增加了retry的机制,确保了你的操作不会被丢失。

最后,引用一个SQLite官方文档中的回答:

If your application has a need for a lot of concurrency, then you should consider using a client/server database. But experience suggests that most applications need much less concurrency than their designers imagine.

Write-Ahead Logging

When a process wants to change a database file (and it is not in WAL mode), it first records the original unchanged database content in a rollback journal. The rollback journal is an ordinary disk file that is always located in the same directory or folder as the database file and has the same name as the database file with the addition of a -journal suffix. The rollback journal also records the initial size of the database so that if the database file grows it can be truncated back to its original size on a rollback.

即默认模式下,SQLite每次写操作之前会先备份,用于出现crash时回滚。(对一个成熟的数据库而言,灾备还是必须的,再次感到,SQLite麻雀虽小五脏俱全。)

The original content is preserved in the database file and the changes are appended into a separate WAL file. A COMMIT occurs when a special record indicating a commit is appended to the WAL. Thus a COMMIT can happen without ever writing to the original database, which allows readers to continue operating from the original unaltered database while changes are simultaneously being committed into the WAL. Multiple transactions can be appended to the end of a single WAL file.

WAL模式下,操作和默认模式正好相反,每次写操作是在自己先备份好的那个数据库上进行,因此并不影响对写之前内容的读操作(甚至多个写操作都可能并行来做,只不过最后需要对多个修改过的备份做一个merge,类似Git版本控制时merge多个人对同一个文件的操作,只要没有conflict都好办)。

所以,当业务中需要频繁地进行写操作时,则建议将SQLite改为WAL模式。而如果大部分时间是读操作,则使用默认模式即可。

要修改数据库SQLite写操作的的模式,比如用上面提到的node-sqlite3可以在连接数据库后执行下面的SQL命令:

db.run('PRAGMA journal_mode = WAL;');

sequelize目前似乎还不支持PRAGMA指令的操作(见https://github.com/sequelize/sequelize/issues/5245)。

References

  1. Does SQLite lock the database file on reads?
  2. SQLite数据库是中小站点CMS的最佳选择
  3. Write-Ahead Logging