当前位置:   article > 正文

Postgres中的锁_postgres 锁

postgres 锁

Postgres 锁模式
Locks:
保护数据库里其他事务的逻辑内容
已持有的持久化事务
需要回滚的变化内容

Lathes(闩锁):
保护数据库其他线程 内部数据结构的关键区
已持有的持久化操作
不需要回滚的变化内容 

【 自旋锁 SpinLock 】

PostgreSQL中有三种级别的锁,他们的关系如下:

  1. |上层 RegularLock
  2. |
  3. | LWLock
  4. |
  5. |底层 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操作。因此,该操作被用来实现进程互斥。

有了这个概念,我们来看源代码。

代码在:

  1. src/include/storage/s_lock.h
  2. src/backend/storage/lmgr/s_lock.c

虽然说了对于SpinLock有两个底层实现,但是在上层调用时,我们是使用统一的接口的,接口在src/backend/storage/lmgr/s_lock.c中:

  1. /*
  2. * s_lock(lock) - platform-independent portion of waiting for a spinlock.
  3. */
  4. int
  5. s_lock(volatile slock_t *lock, const char *file, int line, const char *func)
  6. {
  7. ...
  8. while (TAS_SPIN(lock)) //调用点
  9. {
  10. ...
  11. }

可以发现这个TAS_SPIN(lock)是一个宏,

#define TAS_SPIN(lock)	TAS(lock)

当使用基于TAS指令集的锁时,有:

#define TAS(lock) tas(lock)

对机器的TAS的使用在函数tas()中。

  1. static __inline__ int
  2. tas(volatile slock_t *lock)
  3. {
  4. register slock_t _res = 1;
  5. /*
  6. * Use a non-locking test before asserting the bus lock. Note that the
  7. * extra test appears to be a small loss on some x86 platforms and a small
  8. * win on others; it's by no means clear that we should keep it.
  9. *
  10. * When this was last tested, we didn't have separate TAS() and TAS_SPIN()
  11. * macros. Nowadays it probably would be better to do a non-locking test
  12. * in TAS_SPIN() but not in TAS(), like on x86_64, but no-one's done the
  13. * testing to verify that. Without some empirical evidence, better to
  14. * leave it alone.
  15. */
  16. __asm__ __volatile__(
  17. " cmpb $0,%1 \n"
  18. " jne 1f \n"
  19. " lock \n"
  20. " xchgb %0,%1 \n"
  21. "1: \n"
  22. : "+q"(_res), "+m"(*lock)
  23. : /* no inputs */
  24. : "memory", "cc");
  25. return (int) _res;
  26. }

可以看到这段在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:

  1. int
  2. tas_sema(volatile slock_t *lock)
  3. {
  4. /* Note that TAS macros return 0 if *success* */
  5. return !PGSemaphoreTryLock(&SpinlockSemaArray[*lock]);
  6. }

对于信号量,PostgreSQL分别针对POSIX 信号量、SYSTEM V信号量和windows信号量进行了不同的实现,实现代码分别在:

  1. src/backend/port/posix_sema.c
  2. src/backend/port/sysv_sema.c
  3. src/backend/port/win32_sema.c

我们这里以SYSTEM V信号量为例进行讲解。

PGSemaphoreTryLock的定义为:

  1. bool
  2. PGSemaphoreTryLock(PGSemaphore sema)
  3. {
  4. int errStatus;
  5. struct sembuf sops; //重要!!!
  6. sops.sem_op = -1; /* decrement */
  7. sops.sem_flg = IPC_NOWAIT; /* but don't block */
  8. sops.sem_num = sema->semNum;
  9. /*
  10. * Note: if errStatus is -1 and errno == EINTR then it means we returned
  11. * from the operation prematurely because we were sent a signal. So we
  12. * try and lock the semaphore again.
  13. */
  14. do
  15. {
  16. errStatus = semop(sema->semId, &sops, 1);
  17. } while (errStatus < 0 && errno == EINTR);
  18. ...

即调用了PGSemaphores来实现SpinLock。

而PGSemaphores的定义为:

  1. typedef struct PGSemaphoreData
  2. {
  3. int semId; /* semaphore set identifier */
  4. int semNum; /* semaphore number within set */
  5. } PGSemaphoreData;

在利用system V信号量时,我们有:

  1. struct sembuf
  2. {
  3. unsigned short int sem_num; /* semaphore number */
  4. short int sem_op; /* semaphore operation */
  5. short int sem_flg; /* operation flag */
  6. };

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。

【Postgres中的锁原理】

锁模式定义

/* 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)形成死锁的主要原因和解决方法是:

事务之间互相等待对方锁定的表。解决方法是申请锁定表时不同事务之间的顺序也要一致。
事务之间对锁定了同一个表,但是某个先获取了锁的事务进行了锁升级,造成了死锁。解决方法是同一个事务种要使用不同级别的锁,要先申请最高级别的锁。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/511759
推荐阅读
相关标签
  

闽ICP备14008679号