SQLite的并发控制机制是采用加锁的方式,实现非常简单,但也非常的巧妙,本节将对其进行一个详细的解剖。请仔细阅读下图,它可以帮助更好的理解下面的内容。

    RESERVED锁意味着进程将要对数据库进行写操作。某一时刻只能有一个RESERVED Lock,但是RESERVED锁和SHARED锁可以共存,而且可以对数据库加新的SHARED锁。 为什么要用RESERVED锁? 主要是出于并发性的考虑。由于SQLite只有库级排斥锁(EXCLUSIVE LOCK),如果写事务一开始就上EXCLUSIVE锁,然后再进行实际的数据更新,写磁盘操作,这会使得并发性大大降低。而SQLite一旦得到数据库的RESERVED锁,就可以对缓存中的数据进行修改,而与此同时,其它进程可以继续进行读操作。直到真正需要写磁盘时才对数据库加EXCLUSIVE锁。

    SQLite在pager层获取锁的函数如下:

    Windows下具体的实现如下:

    1. int rc = SQLITE_OK; /* Return code from subroutines */
    2. int res = 1; /* Result of a windows lock call */
    3. int newLocktype; /* Set id->locktype to this value before exiting */
    4. int gotPendingLock = 0;/* True if we acquired a PENDING lock this time */
    5. winFile *pFile = (winFile*)id;
    6. assert( pFile!=0 );
    7. TRACE5("LOCK %d %d was %d(%d)\n",
    8. pFile->h, locktype, pFile->locktype, pFile->sharedLockByte);
    9. /* If there is already a lock of this type or more restrictive on the
    10. ** OsFile, do nothing. Don't use the end_lock: exit path, as
    11. ** sqlite3OsEnterMutex() hasn't been called yet.
    12. */
    13. //当前的锁>=locktype,则返回
    14. if( pFile->locktype>=locktype ){
    15. return SQLITE_OK;
    16. }
    17. /* Make sure the locking sequence is correct
    18. */
    19. assert( pFile->locktype!=NO_LOCK || locktype==SHARED_LOCK );
    20. assert( locktype!=PENDING_LOCK );
    21. assert( locktype!=RESERVED_LOCK || pFile->locktype==SHARED_LOCK );
    22. /* Lock the PENDING_LOCK byte if we need to acquire a PENDING lock or
    23. ** a SHARED lock. If we are acquiring a SHARED lock, the acquisition of
    24. ** the PENDING_LOCK byte is temporary.
    25. */
    26. newLocktype = pFile->locktype;
    27. /*两种情况: (1)如果当前文件处于无锁状态(获取读锁---读事务
    28. **和写事务在最初阶段都要经历的阶段),
    29. **(2)处于RESERVED_LOCK,且请求的锁为EXCLUSIVE_LOCK(写事务)
    30. **则对执行加PENDING_LOCK
    31. */
    32. /////////////////////(1)///////////////////
    33. if( pFile->locktype==NO_LOCK
    34. || (locktype==EXCLUSIVE_LOCK && pFile->locktype==RESERVED_LOCK)
    35. ){
    36. int cnt = 3;
    37. //加pending锁
    38. while( cnt-->0 && (res = LockFile(pFile->h, PENDING_BYTE, 0, 1, 0))==0 ){
    39. /* Try 3 times to get the pending lock. The pending lock might be
    40. ** held by another reader process who will release it momentarily.
    41. */
    42. Sleep(1);
    43. }
    44. //设置为gotPendingLock为1,使和在后面要释放PENDING锁
    45. }
    46. /* Acquire a shared lock
    47. */
    48. /*获取shared lock
    49. **此时,事务应该持有PENDING锁,而PENDING锁作为事务从UNLOCKED到
    50. **SHARED_LOCKED的一个过渡,所以事务由PENDING->SHARED
    51. **此时,实际上锁处于两个状态:PENDING和SHARED,
    52. **直到后面释放PENDING锁后,才真正处于SHARED状态
    53. */
    54. ////////////////(2)/////////////////////////////////////
    55. if( locktype==SHARED_LOCK && res ){
    56. assert( pFile->locktype==NO_LOCK );
    57. res = getReadLock(pFile);
    58. if( res ){
    59. newLocktype = SHARED_LOCK;
    60. }
    61. }
    62. /* Acquire a RESERVED lock
    63. */
    64. /*获取RESERVED
    65. **此时事务持有SHARED_LOCK,变化过程为SHARED->RESERVED。
    66. **RESERVED锁的作用就是为了提高系统的并发性能
    67. */
    68. ////////////////////////(3)/////////////////////////////////
    69. if( locktype==RESERVED_LOCK && res ){
    70. assert( pFile->locktype==SHARED_LOCK );
    71. //加RESERVED锁
    72. res = LockFile(pFile->h, RESERVED_BYTE, 0, 1, 0);
    73. if( res ){
    74. newLocktype = RESERVED_LOCK;
    75. }
    76. }
    77. /* Acquire a PENDING lock
    78. */
    79. /*获取PENDING锁
    80. **此时事务持有RESERVED_LOCK,且事务申请EXCLUSIVE_LOCK
    81. **变化过程为:RESERVED->PENDING。
    82. **PENDING状态只是唯一的作用就是防止写饿死.
    83. **读事务不会执行该代码,但是写事务会执行该代码,
    84. **执行该代码后gotPendingLock设为0,后面就不会释放PENDING锁。
    85. */
    86. //////////////////////////////(4)////////////////////////////////
    87. if( locktype==EXCLUSIVE_LOCK && res ){
    88. //这里没有实际的加锁操作,只是把锁的状态改为PENDING状态
    89. //设置了gotPendingLock,后面就不会释放PENDING锁了,
    90. gotPendingLock = 0;
    91. }
    92. /* Acquire an EXCLUSIVE lock
    93. */
    94. /*获取EXCLUSIVE锁
    95. **当一个事务执行该代码时,它应该满足以下条件:
    96. **(1)锁的状态为:PENDING (2)是一个写事务
    97. **变化过程:PENDING->EXCLUSIVE
    98. */
    99. /////////////////////////(5)///////////////////////////////////////////
    100. if( locktype==EXCLUSIVE_LOCK && res ){
    101. assert( pFile->locktype>=SHARED_LOCK );
    102. res = unlockReadLock(pFile);
    103. TRACE2("unreadlock = %d\n", res);
    104. res = LockFile(pFile->h, SHARED_FIRST, 0, SHARED_SIZE, 0);
    105. if( res ){
    106. newLocktype = EXCLUSIVE_LOCK;
    107. }else{
    108. TRACE2("error-code = %d\n", GetLastError());
    109. }
    110. }
    111. /* If we are holding a PENDING lock that ought to be released, then
    112. ** release it now.
    113. */
    114. /*此时事务在第2步中获得PENDING锁,它将申请SHARED_LOCK(第3步,和图形相对照),
    115. **而在之前它已经获取了PENDING锁,
    116. **所以在这里它需要释放PENDING锁,此时锁的变化为:PENDING->SHARED
    117. */
    118. //////////////////////////(6)/////////////////////////////////////
    119. if( gotPendingLock && locktype==SHARED_LOCK ){
    120. UnlockFile(pFile->h, PENDING_BYTE, 0, 1, 0);
    121. }
    122. /* Update the state of the lock has held in the file descriptor then
    123. ** return the appropriate result code.
    124. */
    125. if( res ){
    126. rc = SQLITE_OK;
    127. }else{
    128. TRACE4("LOCK FAILED %d trying for %d but got %d\n", pFile->h,
    129. locktype, newLocktype);
    130. rc = SQLITE_BUSY;
    131. }
    132. //在这里设置文件锁的状态
    133. pFile->locktype = newLocktype;
    134. return rc;
    135. }

    (I)对于一个读事务会的完整经过: 语句序列:(1)——>(2)——>(6) 相应的状态真正的变化过程为:UNLOCKED→PENDING(1)→PENDING、SHARED(2)→SHARED(6)→UNLOCKED (II)对于一个写事务完整经过: 第一阶段: 语句序列:(1)——>(2)——>(6) 状态变化:UNLOCKED→PENDING(1)→PENDING、SHARED(2)→SHARED(6)。此时事务获得SHARED LOCK。 第二个阶段: 语句序列:(3) 此时事务获得RESERVED LOCK。 第三个阶段: 事务执行修改操作。 第四个阶段: 语句序列:(1)——>(4)——>(5) 状态变化为: RESERVED→ RESERVED 、PENDING(1)→PENDING(4)→EXCLUSIVE(5)。此时事务获得排斥锁,就可以进行写磁盘操作了。

    注:在上面的过程中,由于(1)的执行,使得某些时刻SQLite处于两种状态,但它持续的时间很短,从某种程度上来说可以忽略,但是为了把问题说清楚,在这里描述了这一微妙而巧妙的过程。

    既然SQLite采取了这种机制,所以应用程序得处理SQLITE_BUSY 错误,先来看一个会产生SQLITE_BUSY错误的例子: document/2015-09-15/55f7c87be55f2 所以应用程序应该尽量避免产生死锁,那么应用程序如何做可以避免死锁的产生呢? 答案就是为你的程序选择正确合适的事务类型。 SQLite有三种不同的事务类型,这不同于锁的状态。事务可以从DEFERRED,IMMEDIATE或者EXCLUSIVE,一个事务的类型在BEGIN命令中指定: BEGIN [ DEFERRED | IMMEDIATE | EXCLUSIVE ] TRANSACTION; 一个deferred事务不获取任何锁,直到它需要锁的时候,而且BEGIN语句本身也不会做什么事情——它开始于UNLOCK状态;默认情况下是这样的。如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁,当对数据库进行第一次读操作时,它会获取SHARED LOCK;同样,当进行第一次写操作时,它会获取RESERVED LOCK。 由BEGIN开始的Immediate事务会试着获取RESERVED LOCK。如果成功,BEGIN IMMEDIATE保证没有别的连接可以写数据库。但是,别的连接可以对数据库进行读操作,但是RESERVED LOCK会阻止其它的连接BEGIN IMMEDIATE或者BEGIN EXCLUSIVE命令,SQLite会返回SQLITE_BUSY错误。这时你就可以对数据库进行修改操作,但是你不能提交,当你COMMIT时,会返回SQLITE_BUSY错误,这意味着还有其它的读事务没有完成,得等它们执行完后才能提交事务。 Exclusive事务会试着获取对数据库的EXCLUSIVE锁。这与IMMEDIATE类似,但是一旦成功,EXCLUSIVE事务保证没有其它的连接,所以就可对数据库进行读写操作了。 上面那个例子的问题在于两个连接最终都想写数据库,但是他们都没有放弃各自原来的锁,最终,shared 锁导致了问题的出现。如果两个连接都以BEGIN IMMEDIATE开始事务,那么死锁就不会发生。在这种情况下,在同一时刻只能有一个连接进入BEGIN IMMEDIATE,其它的连接就得等待。BEGIN IMMEDIATE和BEGIN EXCLUSIVE通常被写事务使用。就像同步机制一样,它防止了死锁的产生。 基本的准则是:如果你在使用的数据库没有其它的连接,用BEGIN就足够了。但是,如果你使用的数据库在其它的连接也要对数据库进行写操作,就得使用BEGIN IMMEDIATE或BEGIN EXCLUSIVE开始你的事务。