如果对SQLite事务的概念完全陌生,建议先阅读以下这篇文章来熟悉相关基础概念。
SQLite的原子提交原理
https://blog.youkuaiyun.com/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表示不允许。
每个读事务只需Shared锁,而写事务需要先加Shared锁来读取要修改的数据页,然后再依次升级到Reserved锁和Exclusive锁。Exclusive锁只能在Reserved锁的基础上升级。但是由于系统宕机或断电后第一次连接,需要通过回滚日志恢复数据库中断事务的初始状态,此时可以由Shared锁跳过Reserved锁直接升级到Exclusive锁,锁的状态变化如下图
关于死锁的问题,考虑一个事务为Pending锁状态,等待另外一个拥有Shared锁释放事务,但是这个事物正好需要升级为Reserved锁,需要等待另一个事务释放Pending锁,所以这2个事务都占着各自的锁等待对方释放锁,出现死锁。
SQLite使用非阻塞模式的锁来防止死锁的形成,即或者申请到了锁或者返回一个SQLITE_BUSY的error code,这样就不会在那里死等而出现死锁,但是从理论上来说,有及其微小的概率,一个事务一直申请不到锁。
2.Linux中锁的实现
SQLite通过Linux中的记录锁来实现文件的加锁,该锁的实现遵循POSIX标准。锁分为读锁和写锁两种,读锁可以被多个进程同时拥有,而写锁是排他锁,一个文件最多只有一个写锁,该锁为建议锁,建议锁不由内核强制执行,需要进程之间相互遵守约定,也就是说当前进程发现文件已经加锁了,还要强制访问文件,内核不会阻拦。
记录锁有一个非常好的特性,可以对一个文件的任何部分加锁,SQLite正是利用了这一特性来实现事务锁的四种类型。
这里要注意加锁的位置并不在文件本身,而在数据库文件起始地址0x40000000(1GB)的位置后的512字节,称为lock-byte page,一般数据库文件并没有这么大,所以一般情况下实际文件中并没有该页。如果加锁的地址小于数据库的大小,由于在Windows平台下不能对加锁页读写,所以SQLite不会对该页存储数据,从而这页是浪费的,如果文件数据库的大小小于加锁的地址,则不存在浪费。
lock-byte page的内存地址分布如下图:
Pending byte用来存放Pending锁,加的是写锁,占1字节;Reserved byte用来存储Reserved锁,加的是写锁占1字节;Shared first用来存放Shared锁,加的是读锁,或者Exclusive锁,加的是写锁,占Shared size(510)字节大小。为什么Shared size是510字节的长度而不是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_start、lock.l_len和lock.l_type即可。
在Linux中,每个进程只能拥有一把锁,由内核维护。如果在进程中出现多线程时,锁需要SQLite自身维护,将在下篇文章分析。
3.参考资料
《SQLite Database System Design and Implementation》p99-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
————————————————
版权声明:本文为优快云博主「偏飞123」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/pfysw/article/details/80100236