赞
踩
Postgres 锁模式
Locks:
保护数据库里其他事务的逻辑内容
已持有的持久化事务
需要回滚的变化内容
Lathes(闩锁):
保护数据库其他线程 内部数据结构的关键区
已持有的持久化操作
不需要回滚的变化内容
在PostgreSQL中有三种级别的锁,他们的关系如下:
- |上层 RegularLock
- |
- | LWLock
- |
- |底层 SpinLock
那么按照顺序,我们先来讨论下PostgreSQL的最底层的SpinLock。
作为PostgreSQL的最底层的锁,SpinLock比较简单,它的特点是封锁时间很短,没有等待队列和死锁检测机制,在事务结束时不能自动释放。因此,SpinLock一般不单独使用,而是作为其他锁(LWLock)的底层实现。
作为最底层锁,它的实现是和操作系统和硬件环境相关的。为此,PostgreSQL实现了两个SpinLock:
与机器相关的实现,利用TAS指令集实现(定义在s_lock.h和s_lock.c中);
与机器无关,利用PostgreSQL定义的信号量PGSemaphore实现(定义在spin.c中)。
很显然,依赖机器实现的SpinLock一定比不依赖机器实现的SpinLock要快。因此,如果PostgreSQL运行的机器上如果支持TAS指令集,那么自然会采用第一种实现,否则只能使用第二种实现了。
关于SpinLock的动作,可以看下面这张图:
我们,知道与机器相关的实现利用了TAS指令集。那么什么是TAS呢?
TAS是 Test and Set的缩写。是一个原子操作。它修改内存的值,并返回原来的值。当一个进程P1对一个内存位置做TAS操作,不允许其它进程P2对此内存位置再做TAS操作。P2必须等P1操作完成后,再做TAS操作。因此,该操作被用来实现进程互斥。
有了这个概念,我们来看源代码。
代码在:
- src/include/storage/s_lock.h
- src/backend/storage/lmgr/s_lock.c
虽然说了对于SpinLock有两个底层实现,但是在上层调用时,我们是使用统一的接口的,接口在src/backend/storage/lmgr/s_lock.c中:
- /*
- * s_lock(lock) - platform-independent portion of waiting for a spinlock.
- */
- int
- s_lock(volatile slock_t *lock, const char *file, int line, const char *func)
- {
- ...
-
- while (TAS_SPIN(lock)) //调用点
- {
-
- ...
-
- }
可以发现这个TAS_SPIN(lock)是一个宏,
#define TAS_SPIN(lock) TAS(lock)
当使用基于TAS指令集的锁时,有:
#define TAS(lock) tas(lock)
对机器的TAS的使用在函数tas()中。
- static __inline__ int
- tas(volatile slock_t *lock)
- {
- register slock_t _res = 1;
-
- /*
- * Use a non-locking test before asserting the bus lock. Note that the
- * extra test appears to be a small loss on some x86 platforms and a small
- * win on others; it's by no means clear that we should keep it.
- *
- * When this was last tested, we didn't have separate TAS() and TAS_SPIN()
- * macros. Nowadays it probably would be better to do a non-locking test
- * in TAS_SPIN() but not in TAS(), like on x86_64, but no-one's done the
- * testing to verify that. Without some empirical evidence, better to
- * leave it alone.
- */
- __asm__ __volatile__(
- " cmpb $0,%1 \n"
- " jne 1f \n"
- " lock \n"
- " xchgb %0,%1 \n"
- "1: \n"
- : "+q"(_res), "+m"(*lock)
- : /* no inputs */
- : "memory", "cc");
- return (int) _res;
- }

可以看到这段在C语言中的内嵌汇编代码即是调用了机器的TAS指令。假设lock原来的值为“0”,当P1去做申请lock时,能获取得到锁。而此时P2再去申请锁时,必须spin,因为此时lock的值已经被P1修改为“1”了。
用TAS来实现spin lock,此处要注意volatile的使用。volatile表示这个变量是易失的,所以会编译器会每次都去内存中取原始值,而不是直接拿寄存器中的值。
这避免了在多线程编程中,由于多个线程更新同一个变更,内存中和寄存器中值的不同步而导致变量的值错乱的问题。另外,也会影响编译器的优化行为。
具体汇编代码的解析,可以查看相关资料。
在使用时,PostgreSQL不直接调用tas()函数,而是通过:
int s_lock(volatile slock_t *lock, const char *file, int line, const char *func);
来申请spin lock。返回值是等待的时间。
如果机器上没有TAS指令集,那么PostgreSQL利用PGSemaphores来实现SpinLock。
PGSemaphore是使用OS底层的semaphore来实现的,PG对其做了封装,提供了PG系统内部统一的semaphore操作接口。PG的用PGSemaphore结构体表示PG自身的semaphore信号,并将相关操作封装在sembuf中,传递给底层OS。
实现代码在:
src/backend/storage/lmgr/spin.c
我们知道这个TAS_SPIN(lock)是SpinLock的抽象定义:
#define TAS_SPIN(lock) TAS(lock)
在不使用TAS的场合,有:
#define TAS(lock) tas_sema(lock)
即调用tas_sema(lock)函数实现SpinLock:
- int
- tas_sema(volatile slock_t *lock)
- {
- /* Note that TAS macros return 0 if *success* */
- return !PGSemaphoreTryLock(&SpinlockSemaArray[*lock]);
- }
-
对于信号量,PostgreSQL分别针对POSIX 信号量、SYSTEM V信号量和windows信号量进行了不同的实现,实现代码分别在:
- src/backend/port/posix_sema.c
- src/backend/port/sysv_sema.c
- src/backend/port/win32_sema.c
我们这里以SYSTEM V信号量为例进行讲解。
PGSemaphoreTryLock的定义为:
- bool
- PGSemaphoreTryLock(PGSemaphore sema)
- {
- int errStatus;
- struct sembuf sops; //重要!!!
-
- sops.sem_op = -1; /* decrement */
- sops.sem_flg = IPC_NOWAIT; /* but don't block */
- sops.sem_num = sema->semNum;
-
- /*
- * Note: if errStatus is -1 and errno == EINTR then it means we returned
- * from the operation prematurely because we were sent a signal. So we
- * try and lock the semaphore again.
- */
- do
- {
- errStatus = semop(sema->semId, &sops, 1);
- } while (errStatus < 0 && errno == EINTR);
-
- ...
-

即调用了PGSemaphores来实现SpinLock。
而PGSemaphores的定义为:
- typedef struct PGSemaphoreData
- {
- int semId; /* semaphore set identifier */
- int semNum; /* semaphore number within set */
- } PGSemaphoreData;
在利用system V信号量时,我们有:
- struct sembuf
- {
- unsigned short int sem_num; /* semaphore number */
- short int sem_op; /* semaphore operation */
- short int sem_flg; /* operation flag */
- };
PGSemaphoreTryLock中的while循环里就是执行了semop操作。
而这些操作是OS自带的操作(在<sys/sem.h>头文件中):
extern int semop(int __semid, struct sembuf *opsptr, size_t nops);
很明显,此处PostgreSQL封装了OS底层的system V 的semaphore,然后利用OS底层的系统函数来操作。
剩下两种信号量大抵如此,此处不多言。
SpinLock是分两种情况来分别实现的。这是它们的不同,在Spinlock之上有一些共通的操作要说明下。对于SpinLock的获取,并不是每次都成功,当尝试获取时发现一个对象已经被lock时,当前线程不会阻塞在改锁上,而是先spin(自旋)一定的次数之后再sleep一定的时间后尝试再次获取。对于每次spin之后的sleep时间,PostgreSQL使用了自适应算法,来决定spin的次数和每次spin后,sleep的时间。
下面两个变量要注意下:
spins_per_delay
该变量表示spin多少次后,开始sleep。默认为100,最大值为1000,最小值为10。
spins_per_delay的值基本上不变;但是cur_delay的值为当前值1倍和2倍之间变动。因此,spin delay次数越多,sleep时间会越长。
还有一个变量:
cur_delay
当前sleep的时间,最大值为1000,最小值为1。单位为ms。
锁模式定义
/* NoLock is not a lock mode, but a flag value meaning "don't get a lock" */
#define NoLock 0#define AccessShareLock 1 /* SELECT */
#define RowShareLock 2 /* SELECT FOR UPDATE/FOR SHARE */
#define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */
#define ShareUpdateExclusiveLock 4 /* VACUUM (non-FULL),ANALYZE, CREATE
* INDEX CONCURRENTLY */
#define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */
#define ShareRowExclusiveLock 6 /* like EXCLUSIVE MODE, but allows ROW
* SHARE */
#define ExclusiveLock 7 /* blocks ROW SHARE/SELECT...FOR
* UPDATE */
#define AccessExclusiveLock 8 /* ALTER TABLE, DROP TABLE, VACUUM
* FULL, and unqualifie
Postgresql中的表锁
Postgresql的表锁共有8个,而这8个锁又可以分成四个小类:普通锁、访问锁、意向锁和严格锁, 可以从 pg_locks表中查看锁的状态 。
1.1 普通共享锁SHARE和普通排他锁EXCLUSIVE
SHARE
SHARE是共享锁,也就是读锁,当它加到表上之后,整个表只允许读,不允许改,如果我们为一个表创建索引(不带CONCURRENTLY)时,会创建这种锁。它与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE冲突。
- EXCLUSIVE
任何的Postgresql的命令都不会加EXCLUSIVE锁,不过在一些操作时,可能会在某些系统表加上此类锁。
EXCLUSIVE锁和除了ACCESS SHARE外所有锁冲突,而ACCESS SHARE锁在执行select语句会加该锁,所以一旦表t1被加了EXCLUSIVE锁,对于另一个事务的效果也是只能select,不能修改表。
1.2 访问共享锁ACCESS SHARE和访问排他锁ACCESS EXCLUSIVE
Access锁是Postgresql特有的一种锁,主要是针对Postgresql多版本更新数据的方式而创建的锁。所谓的多版本更新数据,意思是Postgresql在更改某一行数据时,不是在该行直接修改数据,而是另外复制了一个新行,修改都在新行上进行。Access锁有两种:ACCESS SHARE和ACCESS EXCLUSIVE,下面分别进行介绍。ACCESS SHARE
select语句会在对应的表上加上ACCESS SHARE类型的锁,通常情况下,任何只读取表而不修改表的查询都会请求这种锁模式。加上了该锁之后,表明即使在修改数据的情况下也允许读数据。ACCESS SHARE锁只和ACCESS EXCLUSIVE锁冲突。
ACCESS EXCLUSIVE
ACCESS EXCLUSIVE模式与所有的模式的锁冲突,包括SHARE结尾和EXCLUSIVE结尾的锁,也就是说当一个表加上ACCESS EXCLUSIVE锁之后,该表会阻塞其它事务的任何操作。会对表加ACCESS EXCLUSIVE的操作有ALTER TABLE、DROP TABLE、TRUNCATE、REINDEX、CLUSTER、VACUUM FULL等等。
1.3 意向共享锁ROW SHARE和意向排他锁ROW EXCLUSIVE
意向锁是我们要修改表中某一行的数据时,需要先在表上加的一种锁,表示即将要在表的部分行上加上共享锁或者排它锁。也就是说我们在为一个数据表的某些数据行加行锁时,实际上在该表上至少加了两种锁,一种是意向表锁,一种才是行锁。ROW SHARE
使用select from update 或者使用selet for share会加上此锁,它和EXCLUSIVE以及ACCESS EXCLUSIVE冲突。
- ROW EXCLUSIVE
该锁会在UPDATE、DELETE、INSERT命令执行时在相关表上自动创建,它和其它三种EXCLUSIVE的锁和SHARE锁冲突。
1.4 SHARE UPDATE EXCLUSIVE和SHARE ROW EXCLUSIVE
SHARE UPDATE EXCLUSIVE和SHARE ROW EXCLUSIVE是针对意向锁不会发生冲突的特点,为了应对可能出现的更加严格的锁需求提出的。SHARE ROW EXCLUSIVE
SHARE ROW EXCLUSIVE与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE\EXCLUSIVE、ACCESS EXCLUSIVE冲突,目前任何的Postgresql命令都不会创建该锁,在这里我们就不再赘述。SHARE UPDATE EXCLUSIVE
VACUUM(不带FULL选项)、ANALYZE、CREATE INDEX CONCURRENTLY命令会创建该锁。它与SHARE UPDATE EXCLUSIVE、SHARE 、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE锁模式冲突。
Postgresql中的行锁
相对于表锁来说,行锁就是加到某一行上的锁。它的模式比较简单,只有共享和排它两种类型。但是在Postgresql中,实际上由于使用多版本的方式更新数据,实际上当update时创建的排它行锁,也不会影响对该行的读。
死锁
当两个以上的事务,比如事务A等待事务B中的资源,而事务B又在等待事务A中的资源,双方互相等待对方持有的资源,而又不释放自己的资源,就会导致死锁。
互斥条件形成的死锁
锁升级形成的死锁
(1)Postgresql中的锁分为表锁和行锁,其中表锁有8种,行锁有2种。
(2)select语句会加ACCESS SHARE锁,会和ALTER\DROP\TRUNCATE等操作使用的ACCESS EXCLUSIVE锁冲突
(3)CREATE INDEX(不使用CONCURRENTLY)操作会使用SHARE锁,不会和SELECT操作冲突,但是会和UPDATE\DELETE\INSERT操作使用的ROW EXCLUSIVE冲突。
(4)CREATE INDEX CONCURRENTLY 操作会使用SHARE UPDATE EXCLUSIVE锁,不会和SELECT以及UPDATE\DELETE\INSERT操作冲突。
(5)SELECT FROR UPDATE|SHARE会在对应的行加上行排它或者行共享,但是由于Postgresql采用多版本更新的模式,对行加上行排它时,实际上该行照样可以被读取。
(6)形成死锁的主要原因和解决方法是:
事务之间互相等待对方锁定的表。解决方法是申请锁定表时不同事务之间的顺序也要一致。
事务之间对锁定了同一个表,但是某个先获取了锁的事务进行了锁升级,造成了死锁。解决方法是同一个事务种要使用不同级别的锁,要先申请最高级别的锁。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。