SQLite3源码学习(24) Pager模块之事务锁的实现1

   如果对SQLite事务的概念完全陌生,建议先阅读以下这篇文章来熟悉相关基础概念。

SQLite的原子提交原理

https://blog.csdn.net/javensun/article/details/8515690

 

SQLite支持多路并发的事务处理,这就需要一种机制来隔离一个事务相对于其他事务的影响。为了保证事务的隔离性,那么在并发的环境下,对于写数据库的操作就要串行化。而事务锁就是用来解决这个问题的,使得多个进程同时读写一个数据库时保持独立,不会相互影响。

事务锁主要借助于操作系统的文件锁机制来实现的,锁是存放在内存中的,并不存放在数据库文件中,所以操作系统崩溃或硬件断电后锁将全部丢失,重新连接数据库时事务会回滚到初始状态。本文主要讨论Linux下文件锁的实现机制,在Windows平台下的实现也基本类似。

1.事务锁的类型

        SQLite的锁实现非常简单,只支持数据库文件层次的加锁,不支持行、页、表等层次的加锁。SQLite的锁是粗粒度的锁,它允许同一个数据库的多个连接同时读数据,但每次只允许一个连接写数据。

SQLite的锁有以下几种类型:

1.Shared

该锁是用来读数据的,每个事务在读取数据库时需要申请Shared锁,多个Shared锁可以同时存在。

拥有该锁可以向数据库文件读数据,但是不允许写数据。

2.Reserved

该锁用在写事务开始时,备份日志文件、修改数据页到把修改刷新到本地文件这段时间,告诉其他事务现在已经开始准备写入数据库了。Reserved锁可以和多个Shared锁共存,但每次只能有一个Reserved锁。

3.Exclusive

该锁允许事务向数据库写入文件,每个文件只能有一个Exclusive锁,且不能和其他锁共存。

4.Pending

这个锁是Reserved锁到Exclusive锁的过渡阶段,不允许事务申请该锁,由Exclusive锁内部拥有。该锁不允许其他事务申请新的Shared锁,等待已经已经拥有Shared锁的事务全部释放后,再升级为Exclusive锁。这样做是为了防止其他连接不断读数据库,导致写饿死。

下面这个表格列出了,当一个事务已经拥有一种锁的类型后,另一个事务再申请锁是否被允许,Y表示允许,N表示不允许。

clip_image002

每个读事务只需Shared锁,而写事务需要先加Shared锁来读取要修改的数据页,然后再依次升级到Reserved锁和Exclusive锁。Exclusive锁只能在Reserved锁的基础上升级。但是由于系统宕机或断电后第一次连接,需要通过回滚日志恢复数据库中断事务的初始状态,此时可以由Shared锁跳过Reserved锁直接升级到Exclusive锁,锁的状态变化如下图

clip_image004

 

 关于死锁的问题,考虑一个事务为Pending锁状态,等待另外一个拥有Shared锁释放事务,但是这个事物正好需要升级为Reserved锁,需要等待另一个事务释放Pending锁,所以这2个事务都占着各自的锁等待对方释放锁,出现死锁。

SQLite使用非阻塞模式的锁来防止死锁的形成,即或者申请到了锁或者返回一个SQLITE_BUSYerror code,这样就不会在那里死等而出现死锁,但是从理论上来说,有及其微小的概率,一个事务一直申请不到锁。

2.Linux中锁的实现

   SQLite通过Linux中的记录锁来实现文件的加锁,该锁的实现遵循POSIX标准。锁分为读锁和写锁两种,读锁可以被多个进程同时拥有,而写锁是排他锁,一个文件最多只有一个写锁,该锁为建议锁,建议锁不由内核强制执行,需要进程之间相互遵守约定,也就是说当前进程发现文件已经加锁了,还要强制访问文件,内核不会阻拦。

记录锁有一个非常好的特性,可以对一个文件的任何部分加锁,SQLite正是利用了这一特性来实现事务锁的四种类型。

这里要注意加锁的位置并不在文件本身,而在数据库文件起始地址0x40000000(1GB)的位置后的512字节,称为lock-byte page,一般数据库文件并没有这么大,所以一般情况下实际文件中并没有该页。如果加锁的地址小于数据库的大小,由于在Windows平台下不能对加锁页读写,所以SQLite不会对该页存储数据,从而这页是浪费的,如果文件数据库的大小小于加锁的地址,则不存在浪费。

lock-byte page的内存地址分布如下图:

clip_image006

Pending byte用来存放Pending锁,加的是写锁,占1字节;Reserved byte用来存储Reserved锁,加的是写锁占1字节;Shared first用来存放Shared,加的是读锁,或者Exclusive锁,加的是写锁,占Shared size(510)字节大小。为什么Shared size510字节的长度而不是1字节呢?这是因为在Windows NT之前的版本加锁的区域不能重叠,这样如果有多个进程,就无法同时获得共享锁,所以取Shared first+Shared size范围内的一个随机数加锁,如果2个进程取得的随机数相同,那么并发性将受到限制。更高的Windows版本则没有这个限制。

相关代码如下,首先定义lock-byte page的偏移地址

# define PENDING_BYTE     (0x40000000)
#define RESERVED_BYTE     (PENDING_BYTE+1)
#define SHARED_FIRST      (PENDING_BYTE+2)
#define SHARED_SIZE       510

Linux平台下通过fcntl()接口加锁,并封装成osFcntl()

static struct unix_syscall {
  const char *zName;            /* Name of the system call */
  sqlite3_syscall_ptr pCurrent; /* Current value of the system call */
  sqlite3_syscall_ptr pDefault; /* Default value */
} aSyscall[] = {
……
  { "fcntl",        (sqlite3_syscall_ptr)fcntl,      0  },
#define osFcntl     ((int(*)(int,int,...))aSyscall[7].pCurrent)
……
}  

传入fcntl()的第三个参数结构体如下,该结构体用来决定加锁的类型,

struct flock
  {
    short int l_type;	/* Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK.	*/
    short int l_whence;	/* Where `l_start' is relative to (like `lseek').  */
#ifndef __USE_FILE_OFFSET64
    __off_t l_start;	/* Offset where the lock begins.  */
    __off_t l_len;	/* Size of the locked area; zero means until EOF.  */
#else
    __off64_t l_start;	/* Offset where the lock begins.  */
    __off64_t l_len;	/* Size of the locked area; zero means until EOF.  */
#endif
    __pid_t l_pid;	/* Process holding the lock.  */
  };

Shared锁之前首先要获取Pending锁,如果获取失败,说明已经有其他进程开始写数据库文件,返回SQLITE_BUSY

接下来获取Shared锁,如果其他进程已经拥有Exclusive锁,由于Exclusive锁是排他锁,那么加锁失败,返回SQLITE_BUSY

不管是否加锁成功,都要释放之前获取的Pending锁。

static int unixFileLock(unixFile *pFile, struct flock *pLock){
  int rc;
  // pInode在下一篇文章分析
  unixInodeInfo *pInode = pFile->pInode;
  assert( unixMutexHeld() );
  assert( pInode!=0 );
  if( (pFile->ctrlFlags & (UNIXFILE_EXCL|UNIXFILE_RDONLY))==UNIXFILE_EXCL ){
  ……
  }else{
    rc = osFcntl(pFile->h, F_SETLK, pLock);
  }
  return rc;
}
/*第一个参数为文件连接句柄,第二个参数为加锁类型*/
static int unixLock(sqlite3_file *id, int eFileLock){
struct flock lock;
……
  /* A PENDING lock is needed before acquiring a SHARED lock and before
  ** acquiring an EXCLUSIVE lock.  For the SHARED lock, the PENDING will
  ** be released.
  */
  lock.l_len = 1L;
  lock.l_whence = SEEK_SET;
  if( eFileLock==SHARED_LOCK 
      || (eFileLock==EXCLUSIVE_LOCK && pFile->eFileLock<PENDING_LOCK)
  ){
    lock.l_type = (eFileLock==SHARED_LOCK?F_RDLCK:F_WRLCK);
    lock.l_start = PENDING_BYTE;
    if( unixFileLock(pFile, &lock) ){
      //返回SQLITE_BUSY
      tErrno = errno;
      rc = sqliteErrorFromPosixError(tErrno, SQLITE_IOERR_LOCK);
      if( rc!=SQLITE_BUSY ){
        storeLastErrno(pFile, tErrno);
      }
      goto end_lock;
    }
  }
  if( eFileLock==SHARED_LOCK ){
    assert( pInode->nShared==0 );
    assert( pInode->eFileLock==0 );
    assert( rc==SQLITE_OK );

    /* Now get the read-lock */
    lock.l_start = SHARED_FIRST;
    lock.l_len = SHARED_SIZE;
    if( unixFileLock(pFile, &lock) ){
      tErrno = errno;
      rc = sqliteErrorFromPosixError(tErrno, SQLITE_IOERR_LOCK);
    }

    /* Drop the temporary PENDING lock */
    //释放PENDING lock
    lock.l_start = PENDING_BYTE;
    lock.l_len = 1L;
    lock.l_type = F_UNLCK;
    if( unixFileLock(pFile, &lock) && rc==SQLITE_OK ){
      /* This could happen with a network mount */
      tErrno = errno;
      rc = SQLITE_IOERR_UNLOCK; 
    }
}

其他锁也是类似,只要设置对应的lock.l_startlock.l_lenlock.l_type即可。

Linux中,每个进程只能拥有一把锁,由内核维护。如果在进程中出现多线程时,锁需要SQLite自身维护,将在下篇文章分析。

3.参考资料

SQLite Database System Design and Implementationp99-p107

Sqlite学习笔记()&&SQLite封锁机制

http://www.cnblogs.com/cchust/p/4761814.html

SQLite 锁机制学习总结锁状态转换及锁机制实现代码分析

https://my.oschina.net/u/587236/blog/129022

SQLite入门与分析()---再谈SQLite的锁

http://www.cnblogs.com/hustcat/archive/2009/03/10/1408208.html

 

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值