场景演示

在回答这个问题前,我们先看一段代码。

import sqlite3  
import os  
  
DB = "test.db"  
  
def setup_db():  
    if os.path.exists(DB):  
        os.remove(DB)  
    conn = sqlite3.connect(DB)  
    # conn.execute("PRAGMA journal_mode=WAL;") # 显式开启 WAL    
    conn.execute("CREATE TABLE data (id INTEGER);")  
    conn.executemany("INSERT INTO data VALUES (?)", [(i,) for i in range(3)])  
    conn.commit()  
    conn.close()  
  
def long_writer():  
    conn = sqlite3.connect(DB, timeout=10)  
    cur = conn.cursor()  
    conn.execute("BEGIN EXCLUSIVE")  # 显式开启独占事务,确保持有写锁  
    cur.execute("INSERT INTO data VALUES (?)", (999,))  
    reader()  
    conn.commit()  
    conn.close()  
  
def reader():  
    conn = sqlite3.connect(DB, timeout=10)  
    cur = conn.cursor()  
    cur.execute("SELECT COUNT(*) FROM data")  
    conn.close()  
  
print("=" * 50)  
setup_db()  
long_writer()  
  
print("n 测试完成")

这段代码的操作内容很简单,就是:

  • 建立一个 sqlite 数据库
  • 连接一个连接,写入一条数据
  • 在写数据的过程中,建立一个新的连接读取数据

可能你会觉得正常不会有人这么玩的,谁会在一个数据库连接中还建立第二个连接呢? 个人理解,正常开发确实不会这样玩;但是在高度模块化开发的过程中,则完全有可能。甚至于读操作可能是开发者A开发,写操作可能是开发者B开发,因而存在上述场景。

而上述调用,使用 sqlite 数据库,你会看到下述报错:

==================================================
Traceback (most recent call last):
  File "D:CodeProjectotherPythonProjectsqlite2_writer_demo.py", line 33, in <module>
    long_writer()
  File "D:CodeProjectotherPythonProjectsqlite2_writer_demo.py", line 21, in long_writer
    reader()
  File "D:CodeProjectotherPythonProjectsqlite2_writer_demo.py", line 28, in reader
    cur.execute("SELECT COUNT(*) FROM data")
sqlite3.OperationalError: database is locked

问题分析

用过类似 MySQLPostgreSQL 之类的数据库的同学可能会有疑问:为啥会报错呢?

究其原因:sqlite 使用的是粗粒度的文件锁,而不是 MySQL 之类的行锁/表锁,而正常模式下不支持并发操作的,存在并发操作时,会报错database is locked

像上述代码中,一个写连接已经占用一个线程了,如果再建立一个连接,那么相当于建立两个连接,也就是并发操作,自然就锁表了。

解决方式

既然不支持多连接,那就合并方法呗,把代码中的 reader() 方法内容直接拷贝到 long_writer()

确实如此,上述方式不失为一种方法。当然还有另一种方式,也就是今天要介绍的 Wal 模式。

具体操作呢,就是将上述代码中注释的那一行取消注释即可。

conn.execute("PRAGMA journal_mode=WAL;") # 显式开启 WAL  

就这么简单,然后运行不报错,也写入成功。

Wal 模式

看到这里,你肯定会有疑问:前面不是说 sqlite 只支持文件锁嘛,而 sqlite 的数据库文件就一个,是怎么做到单个连接操作这个文件的时候,另外还能建立连接并发操作这个文件而不报错加锁问题呢?

开启 Wal 模式,简单理解:其实相当于读锁和写锁分离,写入过程中加锁,是不会影响读取的。具体实现上:

  • 写操作不再直接修改主数据库文件,而是写入独立的 WAL 日志文件,仅需获取轻量级的写锁(不阻塞读);
  • 读操作从主数据库文件读取数据(而非 WAL 日志),仅需获取共享锁,与写操作的轻量级锁互不排斥,完全无阻塞,实现真正的读写并行

懂了吧,也就是说只要开启 Wal 模式,对于读多写少的场景,再也不会因为少量写操作导致单次请求,读操作被阻塞,响应过慢了。

延申思考

Wal 模式是不是完全解决了 sqlite 的并发问题呢?

并发操作无外乎读读操作、读写操作、写写操作,你可以在上述代码中模仿写方法里面再嵌套写方法、读方法嵌套读方法实践试试~ 这里就不解答了,给你留个实践的机会呢!

上述代码中,为啥要加 conn.execute("BEGIN EXCLUSIVE") 呢?

实际上,不加的话,你会发现:非 Wal 模式也不会报错了。这是因为 sqlite 默认自动提交会立即释放锁,也就是写操作完成自动提交的瞬间会释放锁,从而不会阻塞读。但是阻塞本身在非 Wal 模式是存在的,只有在耗时写操作或高频读操作时,这种阻塞才会很明显。BEGIN EXCLUSIVE 会显式开启独占事务,以便模拟这种感知。


好啦,今天的分享就到这里了,感谢阅读,我是唐叔,欢迎三连哦~

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com