一、SQLite的多线程特性解析

SQLite作为一个轻量级数据库,它的多线程支持经常被开发者误解。很多人以为它像MySQL那样天然支持高并发,实际上SQLite采用的是"单写入器/多读取器"模型。这意味着同一时刻只能有一个线程执行写操作,但多个线程可以同时读取数据。

举个例子,假设我们有个C#程序要操作SQLite:

// 错误示例:多个线程共享同一个连接
using (var connection = new SQLiteConnection("Data Source=test.db"))
{
    // 线程1执行写入
    new Thread(() => {
        connection.Execute("INSERT INTO users VALUES ('张三')");
    }).Start();
    
    // 线程2同时执行查询
    new Thread(() => {
        var users = connection.Query("SELECT * FROM users");
    }).Start();
}

这种写法会导致锁竞争,因为SQLite的连接不是线程安全的。正确的做法是每个线程使用独立的连接:

// 正确示例:每个线程使用独立连接
new Thread(() => {
    using (var conn = new SQLiteConnection("Data Source=test.db"))
    {
        conn.Execute("INSERT INTO users VALUES ('李四')");
    }
}).Start();

new Thread(() => {
    using (var conn = new SQLiteConnection("Data Source=test.db"))
    {
        var users = conn.Query("SELECT * FROM users");
    }
}).Start();

二、事务处理的正确姿势

事务是SQLite多线程编程的核心。SQLite默认使用自动提交模式,这会导致频繁的锁升级。来看一个Java示例:

// 错误示例:自动提交模式下的频繁操作
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:test.db")) {
    // 每个executeUpdate都会触发独立事务
    for (int i = 0; i < 100; i++) {
        conn.createStatement().executeUpdate(
            "INSERT INTO logs VALUES ('操作" + i + "')");
    }
}

这会产生100次锁竞争!改进方案是显式使用事务:

// 正确示例:批量操作使用显式事务
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:test.db")) {
    conn.setAutoCommit(false); // 关闭自动提交
    Statement stmt = conn.createStatement();
    
    try {
        for (int i = 0; i < 100; i++) {
            stmt.executeUpdate("INSERT INTO logs VALUES ('操作" + i + "')");
        }
        conn.commit(); // 统一提交
    } catch (SQLException e) {
        conn.rollback(); // 出错回滚
    }
}

三、WAL模式:提升并发性能的利器

SQLite的WAL(Write-Ahead Logging)模式可以显著提升多线程性能。我们来看Python中的配置示例:

import sqlite3

# 启用WAL模式
conn = sqlite3.connect('test.db')
conn.execute('PRAGMA journal_mode=WAL')  # 启用WAL
conn.execute('PRAGMA synchronous=NORMAL')  # 适当降低同步要求

# 多线程读取示例
def read_data():
    local_conn = sqlite3.connect('test.db')
    cursor = local_conn.cursor()
    cursor.execute("SELECT * FROM products")
    return cursor.fetchall()

# 多线程写入示例
def write_data(product):
    local_conn = sqlite3.connect('test.db')
    try:
        local_conn.execute("INSERT INTO products VALUES (?)", (product,))
        local_conn.commit()
    except:
        local_conn.rollback()
    finally:
        local_conn.close()

WAL模式下,读取操作不会阻塞写入,写入操作也不会阻塞读取,大大提升了并发能力。

四、连接池与超时设置

在高并发场景下,连接管理和超时设置至关重要。Node.js中的实现示例:

const sqlite3 = require('sqlite3').verbose();
const { open } = require('sqlite');

// 创建连接池
async function getConnection() {
    return open({
        filename: 'test.db',
        driver: sqlite3.Database,
        mode: sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE,
        busyTimeout: 5000 // 设置5秒超时
    });
}

// 使用示例
async function queryUser(id) {
    const db = await getConnection();
    try {
        return await db.get('SELECT * FROM users WHERE id = ?', [id]);
    } finally {
        await db.close(); // 释放连接
    }
}

五、实战:多线程日志系统设计

让我们用C++实现一个线程安全的日志系统:

#include <sqlite3.h>
#include <mutex>
#include <thread>

class ThreadSafeLogger {
private:
    sqlite3* db;
    std::mutex write_mutex;
    
public:
    ThreadSafeLogger(const char* db_path) {
        sqlite3_open(db_path, &db);
        sqlite3_exec(db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL);
        sqlite3_exec(db, "CREATE TABLE IF NOT EXISTS logs (time TEXT, msg TEXT)", 
                     NULL, NULL, NULL);
    }
    
    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(write_mutex); // 写操作加锁
        
        sqlite3_stmt* stmt;
        const char* sql = "INSERT INTO logs VALUES(datetime('now'), ?)";
        
        if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
            sqlite3_bind_text(stmt, 1, message.c_str(), -1, SQLITE_TRANSIENT);
            sqlite3_step(stmt);
            sqlite3_finalize(stmt);
        }
    }
    
    ~ThreadSafeLogger() {
        sqlite3_close(db);
    }
};

// 使用示例
void worker(ThreadSafeLogger& logger, int id) {
    for (int i = 0; i < 100; i++) {
        logger.log("线程" + std::to_string(id) + "日志" + std::to_string(i));
    }
}

int main() {
    ThreadSafeLogger logger("logs.db");
    std::thread t1(worker, std::ref(logger), 1);
    std::thread t2(worker, std::ref(logger), 2);
    
    t1.join();
    t2.join();
    return 0;
}

六、性能优化技巧

  1. 批量插入优化:使用BEGIN...COMMIT事务包裹批量操作
  2. 索引优化:为频繁查询的列创建适当索引
  3. 页面大小调整:PRAGMA page_size=4096(根据场景调整)
  4. 缓存设置:PRAGMA cache_size=-2000(设置为2MB)

Go语言中的优化示例:

func bulkInsert(db *sql.DB, items []Item) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    
    stmt, err := tx.Prepare("INSERT INTO items VALUES(?, ?, ?)")
    if err != nil {
        tx.Rollback()
        return err
    }
    
    defer stmt.Close()
    
    for _, item := range items {
        _, err = stmt.Exec(item.ID, item.Name, item.Price)
        if err != nil {
            tx.Rollback()
            return err
        }
    }
    
    return tx.Commit()
}

七、常见陷阱与解决方案

  1. 数据库被锁:设置busy_timeout或实现重试机制
  2. 磁盘I/O瓶颈:考虑使用RAM磁盘或SSD
  3. 连接泄漏:确保每个连接都被正确关闭
  4. 死锁问题:统一操作顺序,减少事务持有时间

Python中的重试机制实现:

def safe_execute(conn, sql, max_retries=3):
    for attempt in range(max_retries):
        try:
            return conn.execute(sql)
        except sqlite3.OperationalError as e:
            if 'database is locked' not in str(e):
                raise
            if attempt == max_retries - 1:
                raise
            time.sleep(0.1 * (attempt + 1))

八、总结与最佳实践

  1. 连接管理:每个线程使用独立连接
  2. 事务控制:合理使用显式事务
  3. WAL模式:大多数场景下推荐启用
  4. 超时设置:配置适当的busy_timeout
  5. 错误处理:实现健壮的重试机制

记住,SQLite适合中小规模并发场景。如果需要处理数百以上的并发写入,可能需要考虑其他数据库解决方案。