SQlite数据库的C编程接口(七) 数据库锁定(Database Locking) by斜风细雨QQ:253786989 2012-02-09
对于《Using SQLite》的这一节内容,理解的不是很清楚。有时间要仔细看看SQLite的文档:http://www.sqlite.org/lockingv3.html(File Locking And Concurrency In SQLite Version 3)
SQLite使用一些不同的锁来保护数据库,以允许多个数据库连接同时访问一个相同的数据库文件,而不会出现数据库损坏。不管是在“自动提交事务(autocommit transaction)”模式,还是“显示事务(explicit transaction)”模式,这些锁都工作良好。
SQLite锁系统(locking system)涉及几个不同层次的锁,用来减少竞争、避免死锁等等。以使SQlite允许多个数据库连接并行读取同一个数据库文件,不过任何写操作都需要完整的,整个数据库文件的独占访问。
大部分时间锁系统(locking system)工作良好,允许不同的应用程序之间方便和安全的分享同一个数据库文件。如果编码得当,大部分写操作仅仅需要几分之一秒。然而,如果多个数据库连接试图在同一时间访问同一个数据库文件,那这些操作迟早会相遇。通常情况下,如果一个数据库操作需要一个暂时无法得到的锁,那么SQLite会返回SQLITE_BUSY,或者在更极端的情况下,返回SQLITE_IOERR(或者扩展码SQLITE_IOERR_BLOCKED)。函数sqlite3_prepare_xxx、sqlite3_step、sqlite3_reset、sqlite3_finalize会返回SQLITE_BUSY。函数sqlite3_backup_step、sqlite3_blob_open也会返回SQLITE_BUSY,因为在这两个函数的内部都是通过调用sqlite3_prepare_xxx、sqlite3_step函数来完成工作的。如果调用sqlite3_close函数时,所连接的数据库存在没有销毁(unfinalize)的语句,则该函数也会返回SQLITE_BUSY。
如果想访问某个锁,就需要等待其所有者完成并释放对它的使用,通常不会等待太长时间。等待(waiting)状态可以由应用程序处理,比如接收到SQLITE_BUSY应答,则再次尝试处理该语句。或者也可以由一个忙处理程序(busy handler)来处理。
忙处理程序(Busy handlers)
busy handler(忙处理程序)是一个回调函数,当SQLite library无法获取一个锁时调用。在busy handler中,可以继续尝试获取锁的操作,或者放弃并返回SQLITE_BUSY错误码。
SQLite有一个内置的基于定时器的busy handler,可以给这个busy handler设置一个以毫秒为单位的超时时间。在超时时间范围内,busy handler将继续重复尝试获取锁的操作。
int sqlite3_busy_timeout(sqlite3*, int ms);
用来设置内置busy handler的超时时间,以毫秒为单位。如果给ms参数传递0或者负值,则内置的busy handler被清除。
程序员也可以自己写busy handler,然后通过aqlite3_busy_handler函数进行设置。
int sqlite3_busy_handler(sqlite3*, int(*)(void*,int), void*);
给指定的数据库连接设置自定义的busy handler,把自己写的busy handler函数传递给该函数的第2个参数。如果给第2个参数传递NULL,则移除自定义的busy handler。第3个参数是传递给busy handler回调函数的用户数据指针。
自定义的busy handler回调函数原型如下:
int user_defined_busy_handler_callback( void *udp, int incr )
第1个参数是通过sqlite3_busy_handler函数传递过来的用户数据指针。第2个参数是一个计数器,每次busy handler被调用时该计数器的计数递增。如果该回调函数返回0,则SQLite将放弃获取锁的操作,并返回SQLITE_BUSY。如果该函数返回一个非0值,则SQLite将继续尝试获取锁的操作。
需要注意的是:一个数据库连接仅仅可以拥有一个busy handler,不能同时设置自定义的busy handler并配置内置的基于定时器的busy handler。每次设置其中一种busy handler都将删除另一个busy handler。
死锁(Deadlocks)
为一个数据库连接设置busy handler不能够解决所有问题。有的时候在多个数据库连接之间会出现死锁现象。比如两个数据库连接各自持有一些锁,但是它们现在都在等待对方持有的锁被释放,这样就会造成死锁现象。唯一的解决办法,就是其中一个数据库连接放弃获取锁的操作,并释放自己所持有的锁。
如果SQLite检测到了一个潜在的死锁情况,它将跳过busy handler,并将有一个数据库连接立刻返回SQLITE_BUSY。这样做是为了鼓励应用程序释放他们自己的锁,以打破死锁现象。
避免SQLITE_BUSY(Avoiding SQLITE_BUSY)
如果我们在开发某个项目时需要充分考虑数据库的并发性能,最简单的方法是使用sqlite3_busy_timeout设置内置的busy handler,并且超时时间在250-2000毫秒范围内进行调整,这样可以减少SQLITE_BUSY的产生。
如果想完全避免SQLITE_BUSY,唯一的办法就是确保对于同一个数据库某一个时刻仅仅存在一个数据库连接。这需要设置PRAGMA locking-mode为EXCLUSIVE(独占的)。
另外,应用程序可以使用独占模式(EXCLUSIVE)的事务,这样对于SQLITE_BUSY返回值的处理就更容易些。可以通过BEGIN EXCLUSIVE TRANSACTION命令开启一个独占式的事务,如果成功则在事务的执行过程中不会返回SQLITE_BUSY。不过BEGIN命令本身可能会执行失败并返回SQLITE_BUSY,但这种情况处理起来很容易,应用程序可以通过sqlite3_reset函数重置BEGIN语句,然后重试。BEGIN EXCLUSIVE的缺点是只有目前没有其他的数据库连接(包括只读事务)正在访问该数据库,它才会执行成功。而且一旦独占式的事务开始执行,它同样会锁住数据库,使其他的数据库连接(包括只读事务)无法访问该数据库。
为了允许更多的并发访问,还有一种类型的事务——IMMEDIATE TRANSACTION(即时事务)。可以通过BEGIN IMMEDIATE TRANSACTION命令启动一个即时事务,如果成功则在事务执行的过程中通常只有执行到COMMIT语句时才会返回SQLITE_BUSY。不管是事务中的哪些命令(包括COMMIT),一旦遇到了SQLITE_BUSY,应用程序可以简单的重置语句然后等待并重试。BEGIN IMMEDIATE语句本身也有可能会遭遇SQLITE_BUSY,这时应用程序也是可以简单的重置BEGIN语句然后重试。与EXCLUSIVE TRANSACTION不同的是,如果存在其它的数据库连接正在读取(非写)数据库,这时候IMMEDIATE事务是可以启动的。一旦IMMEDIATE事务成功启动,则不允许其它数据库连接进行写入操作,但只读的数据库连接仍然可以访问数据库,除非IMMEDIATE事务正在强制修改数据库文件(通常是事务正在执行COMMIT操作)。使用IMMEDIATE事务不会发生死锁现象,所有的SQLITE_BUSY可以通过重试操作进行处理。
避免死锁(Avoiding deadlocks)
避免死锁的原则比较简单,不过遵循这些原则会使应用程序变得复杂一些。
首先,sqlite3_prepare_xxx、sqlite3_backup_step、sqlite3_blob_open函数调用不会产生死锁现象。任何时候如果这些函数返回SQLITE_BUSY,简单的等待一下然后重试即可。
如果在一个(deferred)事务中,sqlite3_step、sqlite3_reset、sqlite3_finalize函数返回SQLITE_BUSY,则应用程序必须回退然后重试。如果这些语句不在一个显示的(explicit)事务中,prepared语句可以简单的重置然后重新执行。如果这些语句在一个显示的(explicit)事务中,那么整个事务就必须回退,然后从头开始执行。致使回退的整个原因就是其他一些数据库连接需要修改数据库。另外需要注意的是,如果应用程序完成了一些读取操作并准备进行写操作,那么在新的事务中最好重新读取这些信息以确定这些数据仍然有效。
不管你做什么,不要忽视SQLITE_BUSY。它可能很少发生,但如果处理不当它就可能成为大麻烦的源头。
当“繁忙”转换为“被阻塞”(When BUSY becomes BLOCKED)
当一个连接需要修改数据库中的数据,数据库就要被加锁使其对于其他连接处于只读状态。事实上,事务中对于数据库的某个修改并没有马上写入数据库文件,而是保存在数据库的页缓存中。因为如果将这个修改直接写入数据库,那么这个修改对于其他的读连接就可见了,这就打破了事务的隔离性原则。
当所有需要的修改操作全部做完,事务开始准备提交。这时数据库文件会被进一步锁定,不允许新的只读事务启动。允许已经存在的读操作(reader)完成,并释放他们持有的数据库锁。当所有的读操作完成,写操作(writer)就要独占式的访问数据库,最终将页缓存中的修改刷新到数据库中。
这一过程允许写事务正在执行的同时,只读事务依然可以继续运行。当写事务确实在提交数据时,读事务被锁定。然而,一个关键的假设是在这个过程中,所有修改被放入页缓存中,直到事务提交的时候才被写入数据库。如果缓存(cache)中装满了(包含处于悬挂状态的修改的)数据页,那么写事务没有其它选择,只能给数据库加一个独占锁,并且在提交(commit)阶段之前刷新缓存。该事务仍然可以在任何时候回滚,但是写入操作必须获得独占锁的即时访问,以刷新缓存。
如果这个锁不是立即可用,写入操作就会被强制终止整个事务。写入事务将回退,然后返回扩展码SQLITE_IOERR_BLOCKED。因为事务是自动回退的,所以应用程序没有更多选择,只能重新启动事务。
为了避免这种情况,最好是用显示的BEGIN EXCLUSIVE语句启动一个可以修改多行数据的大事务。BEGIN EXCLUSIVE也许会失败,并返回SQLITE_BUSY,但是应用程序可以简单的重新尝试直到成功。一旦一个独占式的事务成功启动,它就可以完整的访问数据库,消除SQLITE_IOERR_BLOCKED的出现,甚至事务在提交之前就造成了缓冲区溢出(增加数据库缓存会有所帮助)。
SQlite数据库的C编程接口(七) 数据库锁定(Database Locking) by斜风细雨QQ:253786989 2012-02-09