深入浅出SQLite(三): SQLite锁知识及SQLITE_BUSY错误处理

SQLite锁知识及SQLITE_BUSY错误处理

目录

经验案例

SQLITE 死锁定位及解决方法

目的

​ SQLite是一款轻量级的数据库,用在终端服务器NVR和DVR上。如何有效使用避免死锁,以及当发生死锁时如何处理都具有非常重要的意义,本文将深入探讨如何处理关于sqlite死锁的知识,以及当发生死锁时如何解决死锁。

背景知识
  • SQLite中的锁主要为两类, 一类是回滚日志锁,一类是WAL模式下的锁。回滚日志下的锁本质是锁住数据库文件。 两种锁不是独立的,WAL模式下也会使用回滚日志的共享锁和独占锁。下面分别介绍这两种模式下的锁。
回滚日志锁

在这里插入图片描述

​ SQLite中的锁主要为两类, 一类是回滚日志锁,一类是WAL模式下的锁。回滚日志模式下,如上图所示,SQLite使用锁逐步上升机制, SQLITE的锁状态共有5种(未加锁(UNLOCKED)、共享(SHARED)、保留(RESERVED)、未决(PENDING)和排它(EXCLUSIVE)),上升到高级别需要低级别的锁。在SQLite2中,如果多个进程正在从数据库中读取数据,也就是说该数据库始终都有读操作发生,即在每一时刻该数据库都持有至少一把共享锁,这样将会导致没有任何进程可以执行写操作,因为在数据库持有读锁的时候是无法获取写锁的,我们将这种情形称为"写饥饿"。在SQLite3中,通过使用PENDING锁则有效的避免了"写饥饿"情形的发生。对于一个读事务会经过以下过程:UNLOCKED到PENDING;获取PENDING锁只是暂时的,获取PENDING锁是获取SHARED锁的第一步,因为若有其它事务已获取PENDING锁,则此事务不能再获取SHARED锁了。如果获取PENDING锁成功,则此事务可以继续获取SHARED锁,并将之间获取的PENDING释放。对于一个写事务会以下过程第一步和读事务一样,获取SHARED锁。获取RESERVED锁,一旦事务要进行写操作,首先就要获取此锁。获取EXCLUSIVE锁,实际上此时要先获取PENDING锁,以阻止其它事务继续获取SHARED锁(因为前面说过获取PENDING锁是获取SHARED锁的第一步),进而防止写饿死。获取PENDING锁后,才真去获取EXCLUSIVE锁;如果获取EXCLUSIVE锁成功,则事务就可以进行写磁盘操作了。

​ 回滚日志模式下,读与写之间不会产生死锁,但写与写之间仍旧可能发生死锁。 SQLite提供了三种不同的事务类型共用户选用: DEFERRED,IMMEDIATE,EXCLUSIVE, 三种的区别在于获取锁的起点不同。基本的使用准则是:如果你在使用的数据库没有其它的连接,用BEGIN就足够了。但是,如果有其它的连接也要对数据库进行写操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE开始事务。最好用BEGIN IMMEDIATE

WAL模式下的锁

​ 在WAL日志模式中也会用到回滚日志的共享锁和独占锁,WAL模式下,所有连接始终都持有回滚日志的共享锁,在退出WAL日志模式时,会先获取回滚日志的独占锁,如果获取失败,说明还有其他连接在使用数据库,那么就不能退出。只有当前一个连接在使用数据库时才能退出,退出时会把WAL日志文件和WAL-index文件删除。下面来分析每一种WAL锁的应用:

  • WAL_WRITE_LOCK
    • 这个锁为独占锁,每一个写事务开始时都需要该锁,同一个时间只能有一个写事务持有该锁,写事务和写事务不能并发进行。
    • 在事务异常中断,WAL-index文件头部遭破坏后恢复这段时间也会持有该锁,因为写事务提交时也会修改WAL-index文件。
  • WAL_CKPT_LOCK
    • 在将WAL日志更新到数据库文件时会使用该锁,同一时间只能有一个连接进行checkpoint操作,所以该锁为独占锁。在wal_checkpoint模式为PASSIVE时,WAL同步数据的内容不超过每个线程的read-mark记录的帧,后面的帧不会同步,读事务只读WAL日志中在read-mark之前的帧,后面的帧在数据库中读取,所以同步数据库时并不会影响到读事务。写事务是向WAL文件追加内容,也不影响checkpoint操作。
    • checkpoint操作必须持有WAL_READ_LOCK(0)独占锁,因为持有WAL_READ_LOCK(0)共享锁的读事务只在数据库中读取数据。
    • 如果wal_checkpoint模式为其他模式时,需要截断WAL日志,或者把写事务的起始地址恢复到WAL文件开头,此时需要持有WAL_WRITE_LOCK,防止其他连接开始写事务。同时也要持有WAL_READ_LOCK(i)锁,此时读事务已经不能在WAL日志中读取数据。
  • WAL_RECOVER_LOCK
    • 在恢复WAL-index时会加该锁,该锁是独占锁。同时在恢复期间,所有的锁都会加上,此时不能进行任何读写操作。但是如果恢复前发现有其他进程在进行checkpoint操作来同步数据库时,并不会等待其完成,而是直接同步进行。
    • 另外有一点需要注意的是,检查点操作和恢复操作肯定不能同时进行,否则可能把未提交的页同步到数据库中,那为什么当其他线程持有WAL_CKPT_LOCK锁时,还要继续执行恢复操作?个人的理解是如果检查点操作在同步数据库时处在关键操作的地方,必然会占有读锁或写锁的独占锁,此时恢复操作肯定不能加锁成功,而如果恢复操作加锁成功,那么检查点操作并不是在关键地方,在恢复操作完成之前,下一次检查点操作可以依然正常获取WAL_CKPT_LOCK锁,但是其他写锁和读锁都获取失败,所以不可能对数据库进行同步。
    • 另外别的进程可以通过获取WAL_RECOVER_LOCK的共享锁是否成功,来判断是否有进程在进行恢复操作。
  • WAL_READ_LOCK(0)
    • 0表示只从数据库读取页
    • 读事务申请的共享锁,和WAL_WRITE_LOCK不冲突,读写可以完全并发进行,互不影响。但是不能和数据库同步操作和WAL-index文件恢复并发进行。
  • WAL_READ_LOCK(i) i=1~4
    • 读事务在开始的时候,记录WAL文件的最后一个有效帧(read mark。即ReadMark[i]的值),并在整个事务中忽略之后添加的新内容。
    • 如果不在WAL日志中,则从数据库中读取数据,则直接去数据库读取,并修改ReadMark[i]的值,这时需要获取WAL_READ_LOCK(i)的独占锁,以确保此时没有相关的读事务。
    • 如果在,则直接读就可以了。
    • 为什么去数据库读取时需要修改ReadMark[i]? 因为mxFrame在写事务结束时是会改变的,检查点操作时不知道读事务的最大帧,所以需要记录在ReadMark[i]里面。举个例子:假如读事务A开始的时候,WAL文件中有5个帧(Frame),且都不包含数据库第3页的内容。A在开始的时候记下read mark,表示只能看到WAL文件的前5帧,之后的不管,然后搜索wal-index,发现前5帧里都没有第3页,于是从数据库文件中去读取第3页的内容。如果这时,有一个写事务B,修改了第3页的内容,这时WAL文件的第六帧就是第3页的新内容,如果这时候,回填程序超过了A的read mark,把第六帧(第三页)也同步到了数据库,那么读写可能冲突,导致读到错误的数据。本质上就是,让读数据库和写数据库操作(check point操作)不冲突,即读写不冲突。
SQLITE_BUSY错误处理
  • 在多路并发处理时,由于当前数据库文件被其它另一个连接使用,导致当前连接无法获得数据库文件的控制权,就会返回SQLITE_BUSY错误。 这里需要与SQLITE_LOKCED错误进行区分,后者是同一个数据库连接的问题,而前者是不同数据库连接并发情况下的问题。本节将介绍几种常见的导致SQLITE_BUSY的情况,以及其它情况下如何通过自定义方式来处理SQLITE_BUSY问题。
常见的导致SQLTE_BUSY的情况
  1. 回滚日志模式下,写与写操作可能导致死锁,死锁必然导致SQLITE_BUSY,考虑如下情况:
    连接1BEGIN (UNLOCKED)
    连接1SELECT ... (SHARED)
    连接1INSERT ... (RESERVED)
    连接2BEGIN (UNLOCKED)
    连接2SELECT ... (SHARED)
    连接1COMMIT (PENDING,尝试获取EXCLUSIVE锁,但还有SHARED锁未释放,返回SQLITE_BUSY)
    连接2INSERT ... (尝试获取RESERVED锁,但已有PENDING锁未释放,返回SQLITE_BUSY)

现在2个连接都在等待对方释放锁,于是就死锁了, 这种死锁可以通过使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE开始事务,从而避免掉死锁.

  1. WAL模式下,根据上节知识知道,读与写不会相互阻塞,而写与写之间通过独占锁实现互斥,不会发生死锁。所以不存在死锁导致的SQLITE_BUSY问题。但存在由其它情况导致的SQLITE_BUSY问题。包括如下三点
    1. 如果另一个数据库连接是在EXCLUSIVE模式下打开,则当前连接对数据库的所有查询操作都将返回SQLITE_BUSY
    2. 当最后一个数据库连接准备关闭时,根据上节的知识,需要获取回滚日志的独占锁并清理文件,这时如果有另一个连接试图打开或查询数据库文件,那就有可能导致SQLITE_BUSY
    3. 当一个数据库连接在修复数据库时,如果有另一个连接要查询时,那该操作会导致SQLIT_BUSY
自定义SQLITE_BUSY处理方式
  • 当某个事务陷入死锁或者无法获取控制权时,sqlite尝试一定次数,几次之后依然无法获取锁的话,就返回错误信息,并把如何处理错误的权利交给用户程序。重试几次相隔多久是可以交由用户控制的,也可以改为沉睡唤醒机制,就是微信优化用的方法
  • 可以采用sqlite3的API函数sqlite3_busy_handler() 或sqlite3_busy_timeout()来处理SQLITE_BUSY问题,int sqlite3_busy_handler(sqlite3 *, int (*)(void *, int), void *)不注册此函数时默认回调函数为NULL,清除busy handle,申请不到锁直接返回;函数可以定义一个回调函数,当出现数据库忙时sqlite会调用该函数进行延时并返回非0会重试本次操作,回调函数的第二个参数会被传递为此次因BUSY忙事件而调用该函数的次数, 因此你完全可以自行控制多少次后(也就是延时多少后)才真正返回BUSY;回调函数返回非0,数据库会重试当前操作,返回0则当前操作返回SQLITE_BUSY。
  • sqlite3_busy_timeout()的可以设置时间,不注册此函数时默认超时等待为0,当ms<=0时,清除busy handle,申请不到锁直接返回;定义一个毫秒数,当未到达该毫秒数时,sqlite会sleep并重试当前操作,如果超过ms毫秒,仍然申请不到需要的锁,当前操作返回SQLITE_BUSY。只有到至少到特定时间后才会返回SQLITE_BUSY. 这个函数实际上注册了一个默认的sqlite3_busy_handler(sqliteDefaultBusyCallback),而这个回调函数在你的编译环境下可能使得第二个ms参数必需要大于1000且是他的整数倍才有意义,由于此默认callback函数延时较大,建议写回调函数然后用slite3_busy_handler注册, 这样就可以自己用自己的延时函数或方法进行处理了。这里需要注意的一点是,如果需要自定义自己的函数,则应该在sqlite_busy_timeout之后去调用sqlite_busy_handler设置。
总结
  • 本文主要介绍了SQLite锁相关知识,并据此介绍几种常见的导致SQLITE_BUSY的情况,以及如何自定义SQLITE_BUSY处理方式。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值