一、前言
在并发控制的过程中,事务隔离起着重要作用。事务隔离是控制如何和何时进行更改以及何时必须对彼此、用户和系统可见的属性。
KingabseES 通过多版本并发控制架构实现隔离。多版本并发控制是一种允许多个会话同时访问同一记录的技术,即,当会话 A 正在更新一条记录时,会话 B 仍然可以访问该记录。
但是以下情况,应该怎么办:
- 如果会话 A 和会话 B 都想同时更新相同的记录。
- 如果在会话 A 访问表时,在会话 B 中试图 truncate 表。
- 如果会话 A 正在更新表,而会话 B 试图 vacuum 表。
这里出现了锁定的概念。
二、锁机制
锁机制在 KingbaseES 里非常重要 (对于其他的 RDBMS 也是如此),特别是对于数据库应用开发人员,高并发应用的开发人员必须熟悉。大部分数据异常情况,跟死锁或者数据不一致有关系,基本上都是由于对锁机制不太了解导致的。
锁定类型取决于执行的命令类型。 KingabseES 支持三种锁定机制:
- 表级锁 ( Table-Level Locks )
- 行级锁 ( Row-Level Locks )
- 咨询锁 ( Advisory Locks )
表级和行级的锁可以是显式的也可以是隐式的,咨询性锁一般是显式的。显式的锁由显式的用户请求(通过特殊的查询)获取,隐式的锁是通过标准的 SQL 命令来获取。
- 隐式锁 意味着当事务结束时锁会默认关闭。
- 显式锁 一旦获得,可能会一直保持到显式释放。我们可以使用该WITH LOCK 语句显式获取锁。
除了表级和行级的锁,还有页级共享/排除锁,用于控制对共享缓存池里表页的访问。在一行数据被读取或者更新后,这些锁会立即被释放。应用程序开发者通常不需要关注页级的锁。
三、表级锁 ( Table-Level Locks )
表级锁通过内置 SQL 命令获取(隐式);此外,它们可以通过 LOCK 命令显式获取。表级锁包括:
- 访问共享(ACCESS SHARE) - SELECT 、COPY 命令可在查询中引用的表上获得该锁。一般规则是所有的查询中只有读表才获取此锁。
- 行共享(ROW SHARE) - SELECT FOR UPDATE 和 SELECT FOR SHARE 命令可在目标表上获得该锁(以及查询中所有引用的表的访问共享锁)。
- 行独占(ROW EXCLUSIVE) - UPDATE、INSERT 和 DELETE 命令在目标表上获得该锁(以及查询中所有引用的表的访问共享锁)。 一般规则是所有修改表的查询获得该锁。
- 共享更新独占(SHARE UPDATE EXCLUSIVE) - VACUUM(不含FULL),ANALYZE,CREATE INDEX CONCURRENTLY,和一些 ALTER TABLE 的命令获得该锁。
- 共享(SHARE) - CREATE INDEX(无 CONCURRENTLY) 命令在查询中引用的表上获得该锁。
- 共享行独占(SHARE ROW EXCLUSIVE) - CREATE COLLATION, CREATE TRIGGER和多种形式 ALTER TABLE,获取此锁。
- 独占(EXCLUSIVE) - REFRESH MATERIALIZED VIEW CONCURRENTLY,获取此锁。
- 访问独占(ACCESS EXCLUSIVE) - ALTER TABLE,DROP TABLE,TRUNCATE,REINDEX,CLUSTER 和 VACUUM FULL 命令在查询中引用的表上获得该锁。此锁模式是 LOCK 命令的默认模式。
最基本的是 Share 和 Exclusive 这两种锁,它们分别是读、写锁的意思。Share,即读锁,表的内容就不被修改了;可以为多个事务加上此锁,只要任意一个事务不释放这个读锁,则其他事务就不能修改这个表。Exclusive,相当于加了写锁,这时别的进程不能写也不能读这条数据。
但后来数据库又加上了多版本的功能,修改一条语句的同时,允许了读数据。为了处理这种情况,又增加了两种锁Access Share和Access Excusive,锁中的关键字 Access 是与多版本相关的有了该功能。其实,有了该功能后,如果修改一行数据,实际并没有改原先那行数据,而是复制了一个新行,修改都在新行上,事务不提交,其他人是看不到修改的这条数据的。由于旧行数据没有变化,在修改过程中,读数据的人仍然可以读到旧的数据。
表级锁加锁对象是表,这使得加锁范围太大,导致并发并不高,于是人们提出了行级锁的概念,但行级锁与表级锁之间会产生冲突,这时需要一种机制来描述行级锁与表级锁之间的关系,有了意向锁的概念,这时又加了两种锁,即意向共享锁(Row Share) 和意向排他锁(Row Exclusive),由于意向锁之间不会产生冲突,因为他们是“有意”做,还没真做;而且意向排它锁相互之间也不会产生冲突,于是又需要更严格一些的锁,这样就产生了 Share Update Exclusive , Share Row Exclusive 可以看成 Share 与 Row Exclusive 。
1、访问共享(ACCESS SHARE)
访问共享锁是由只从表中读取但不修改它的查询获取的。通常,这是一个选择查询。
隐式锁定示例:
-
从会话 1 中选择获取访问共享锁定的内容
(SESSION 1)# begin ;
BEGIN
(SESSION 1)# select *, pg_sleep(300) from acl ;
... -
尝试从会话 2 中截断表
(SESSION 2)# begin;
BEGIN
(SESSION 2)# truncate table acl;
..
会话 1 获取 AccessShareLock ,以获取记录。
会话 2 想要 truncate 表,必须使用 AccessExclusiveLock 模式锁,但由于锁冲突,正在等待获取。
ACCESS SHARE LOCK与ACCESS EXCLUSIVE锁定模式冲突。
这时查看锁的信息,可以得到了两个锁。看到 AccessShareLock 的 granted 值是 true ,而 AccessExclusiveLock 的 granted 值是 false。
kingbase=# \! ksql -c "select pid, virtualxid vxid, locktype lock_type, mode lock_mode, granted, relation::regclass relname from pg_locks WHERE relation = 'acl'::regclass; "
pid | vxid | lock_type | lock_mode | granted | relname
------+------+-----------+---------------------+---------+---------
7244 | 6/9 | relation | AccessShareLock | t | acl
7364 | 3/12 | relation | AccessExclusiveLock | f | acl
(2 rows)
查找锁和锁的PID
kingbase=# \! ksql -c "SELECT locked.pid AS locked_pid, locker.pid AS locker_pid, locked_act.usename AS locked_user, locker_act.usename AS locker_user, locked.transactionid, relname FROM pg_locks locked LEFT OUTER JOIN pg_class ON (locked.relation = pg_class.oid), pg_locks locker, pg_stat_activity locked_act, pg_stat_activity locker_act WHERE locker.granted = true AND locked.granted = false AND locked.pid = locked_act.pid AND locker.pid = locker_act.pid AND locked.relation = locker.relation;"
locked_pid | locker_pid | locked_user | locker_user | virtualtransaction | relname
------------+------------+-------------+-------------+--------------------+---------------
7364 | 7244 | kingbase | kingbase | 3/12 | acl
(1 row)
显式锁定示例:
kingbase=# begin;
BEGIN
kingbase=# LOCK TABLE emp IN ACCESS SHARE MODE;
LOCK TABLE
kingbase=# \! ksql -c "select pid, virtualxid vxid, locktype lock_type, mode lock_mode, granted, relation::regclass relname from pg_locks WHERE relation = 'emp'::regclass; "
pid | vxid | lock_type | lock_mode | granted | relname
------+------+-----------+-----------------+---------+------------------------------------
6020 | 4/8 | relation | AccessShareLock | t | emp
(1 rows)
kingbase=#
2、行共享(ROW SHARE)
SELECT FOR UPDATE 和 SELECT FOR SHARE 命令在目标表上获取此模式的锁。
EXCLUSIVE 与 *ACCESS EXCLUSIVE *锁定模式冲突 。
-
从会话 1 中选择获取访问共享锁定的内容
(SESSION 1)# begin ;
BEGIN
(SESSION 1)# select *, pg_sleep(300) from acl for update ;
... -
从会话 2 查看锁和锁的PID信息
kingbase=# ! ksql -c "select pid, virtualxid vxid, locktype lock_type, mode lock_mode, granted, relation::regclass relname from pg_locks WHERE relation = 'acl'::regclass; "
pid | vxid | lock_type | lock_mode | granted | relname ------+------+-----------+---------------------+---------+--------- 7691 | | relation | RowShareLock | t | acl (1 rows)
3、行独占( ROW EXCLUSIVE )
命令 UPDATE、 DELETE和 INSERT在目标表上获取此锁定模式。
SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE等锁模式,与 ACCESS EXCLUSIVE 锁模式冲突 。
隐式锁定示例:
例子
(SESSION 1)#begin;
BEGIN
(SESSION 1)#select * from acl;
id | sno | name | sal | dept
----+-----+------+-----+-------
1 | 1 | A | 200 | IT
2 | 2 | B | 200 | IT
3 | 3 | C | 300 | SALES
(3 rows)
(SESSION 1)#insert into acl values(4,4,'D',400,'IT');
INSERT 0 1
(SESSION 1)#
现在,这个会话 1 获得了一个行独占锁。
从会话 2 开始,尝试更改表。
(SESSION 2)#alter table acl drop dept;
会话 2 将等待会话 1 释放锁,因为 alter table drop column 需要 ACCESS EXCLUSIVE 锁,这与 ROW EXCLUSIVE 锁冲突。
kingbase=# \! psql -c "select pid, virtualxid vxid, locktype lock_type, mode lock_mode, granted, relation::regclass relname from pg_locks WHERE relation = 'acl'::regclass; "
pid | vxid | lock_type | lock_mode | granted | relname
------+--------+-----------+---------------------+---------+----------+---------
8038 | 4/1364 | relation | AccessShareLock | t | acl
8038 | 4/1364 | relation | RowExclusiveLock | t | acl
8058 | 3/14 | relation | AccessExclusiveLock | f | acl
(3 rows)
执行 pg_stat_activity 以找到 PID,
kingbase=# select pid, wait_event_type, wait_event, query from pg_stat_activity;
-[ RECORD 3 ]---+---------------------------------------------------------------
pid | 8058
wait_event_type | Lock
wait_event | relation
query | alter table acl drop dept;
现在,根据 PID 查看 pg_locks 信息
kingbase=# select locktype, relation, virtualxid, transactionid, mode, granted from pg_locks where pid='8058';
locktype | relation | virtualxid | transactionid | mode | granted
---------------+----------+------------+---------------+---------------------+---------
virtualxid | | 3/14 | | ExclusiveLock | t
transactionid | | | 671 | ExclusiveLock | t
relation | 32803 | | | AccessExclusiveLock | f
(3 rows)
显式锁定示例:
(SESSION 1)#begin;
BEGIN
(SESSION 1)#lock table acl IN ROW EXCLUSIVE MODE;
LOCK TABLE
(SESSION 1)#\! psql -c "select pid, virtualxid vxid, locktype lock_type, mode lock_mode, granted, relation::regclass relname from pg_locks WHERE relation = 'acl'::regclass; "
pid | vxid | lock_type | lock_mode | granted | relname
------+--------+-----------+------------------+---------+---------
8509 | 4/1394 | relation | RowExclusiveLock | t | acl
(1 row)
4、共享更新独占(SHARE UPDATE EXCLUSIVE)
由 VACUUM(非 FULL)、ANALYZE、CREATE INDEX CONCURRENTLY、CREATE STATISTICS 和 ALTER TABLE VALIDATE 以及其他少数几个 ALTER TABLE 命令获取共享更新独占锁。
与 SHARE UPDATE EXCLUSIVE、SHARE、SHARE ROW EXCLUSIVE、EXCLUSIVE 和 ACCESS EXCLUSIVE 锁模式冲突。此模式保护表,避免并发模式更改和 VACUUM 运行。
隐式锁定示例:
例子
(SESSION 1)#vacuum pgbench_accounts;
..
..
获得的锁是
kingbase=# SELECT locktype,transactionid,virtualtransaction,mode FROM pg_locks ;
locktype | transactionid | virtualtransaction | mode
------------+---------------+--------------------+--------------------------
relation | | 3/180 | RowExclusiveLock
virtualxid | | 3/180 | ExclusiveLock
relation | | 5/379 | AccessShareLock
virtualxid | | 5/379 | ExclusiveLock
relation | | 3/180 | ShareUpdateExclusiveLock
(5 rows)
可以使用 lock_timeout 来避免等待锁。
s
lock_timeout 将不会在指定时间内获得访问权,如果发生超时则等待锁的会话断开。
SET lock_timeout TO '2s';
显式锁定示例:
(SESSION 1)#begin;
BEGIN
(SESSION 1)#LOCK TABLE acl IN SHARE UPDATE EXCLUSIVE MODE ;
LOCK TABLE
(SESSION 1)#
锁的信息是
kingbase# \! psql --c "select pid, virtualxid vxid, locktype lock_type, mode lock_mode, granted, relation::regclass relname from pg_locks WHERE relation = 'acl'::regclass; "
pid | vxid | lock_type | lock_mode | granted | relname
------+-------+-----------+--------------------------+---------+---------
8719 | 3/185 | relation | ShareUpdateExclusiveLock | t | acl
(1 row)
5、共享(SHARE)
由 CREATE INDEX (无 CONCURRENTLY)获得共享锁。
CREATE INDEX 的非并发版本使用 ShareLock 防止表更新,例如 DROP TABLE 或 INSERT 或 DELETE。
ROW EXCLUSIVE与, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE, 和 ACCESS EXCLUSIVE lock 模式冲突 。此模式保护表免受并发数据更改的影响。
隐式锁定示例:
(SESSION 1)#create index abalance_ind on pgbench_accounts(balance);
...
...
锁的信息是
kingbase=# SELECT locktype,transactionid,virtualtransaction,mode FROM pg_locks ;
locktype | transactionid | virtualtransaction | mode
---------------+---------------+--------------------+---------------------
virtualxid | | 3/196 | ExclusiveLock
relation | | 5/382 | AccessShareLock
virtualxid | | 5/382 | ExclusiveLock
relation | | 3/196 | ShareLock
transactionid | 737 | 3/196 | ExclusiveLock
relation | | 3/196 | AccessExclusiveLock
(6 rows)
显式锁定示例:
(SESSION 1)#begin;
BEGIN
(SESSION 1)#SELECT * FROM acl FOR SHARE;
id | sno | name | sal | dept
----+-----+------+-----+-------
1 | 1 | A | 200 | IT
2 | 2 | B | 200 | IT
3 | 3 | C | 300 | SALES
4 | 4 | D | 400 | IT
(4 rows)
(SESSION 1)#
kingbase=# \! psql -f implicit.sql
pid | vxid | lock_type | lock_mode | granted | xid_lock | relname
------+-------+-----------+--------------+---------+----------+---------
8719 | 3/197 | relation | RowShareLock | t | | acl
(1 row)
6、共享行独占(SHARE ROW EXCLUSIVE)
由 CREATE COLLATION, CREATE TRIGGER和多种ALTER TABLE 获得共享行独占锁。
ROW EXCLUSIVE与, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, 和 ACCESS EXCLUSIVE lock 模式冲突 。此模式可保护表免受并发数据更改的影响,并且是自排斥的,因此一次只能有一个会话保持它。
显式锁定示例:
(SESSION 1)#BEGIN ;
BEGIN
(SESSION 1)#LOCK TABLE acl IN SHARE ROW EXCLUSIVE MODE;
LOCK TABLE
(SESSION 1)#
kingbase=# \! psql -f implicit.sql
pid | vxid | lock_type | lock_mode | granted | xid_lock | relname
------+-------+-----------+-----------------------+---------+----------+---------
8719 | 3/198 | relation | ShareRowExclusiveLock | t | | acl
(1 row)
7、独占(EXCLUSIVE)
由 REFRESH MATERIALIZED VIEW CONCURRENTLY 获得独占锁。
ROW SHARE与, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, 和 ACCESS EXCLUSIVE lock 模式冲突 。这种模式只允许并发 ACCESS SHARE 锁,即只从表中读取,可以与持有这种锁模式的事务并行进行。
隐式锁定示例:
kingbase=# REFRESH MATERIALIZED VIEW CONCURRENTLY pgbench_accounts_mv WITH DATA;
..
..
kingbase=# SELECT locktype,transactionid,virtualtransaction,mode FROM pg_locks ;
locktype | transactionid | virtualtransaction | mode
---------------+---------------+--------------------+---------------------
relation | | 4/18 | AccessShareLock
relation | | 4/18 | AccessShareLock
virtualxid | | 4/18 | ExclusiveLock
relation | | 3/10 | AccessShareLock
virtualxid | | 3/10 | ExclusiveLock
relation | | 4/18 | AccessExclusiveLock
transactionid | 747 | 4/18 | ExclusiveLock
relation | | 4/18 | AccessShareLock
relation | | 4/18 | ExclusiveLock
(9 rows)
显式锁定示例:
kingbase=# begin;
BEGIN
kingbase=# LOCK TABLE acl IN EXCLUSIVE MODE;
LOCK TABLE
kingbase=#
kingbase=# \! psql -f implicit.sql
pid | vxid | lock_type | lock_mode | granted | xid_lock | relname
------+------+-----------+---------------+---------+----------+---------
2611 | 4/20 | relation | ExclusiveLock | t | | acl
(1 row)
8、访问独占(ACCESS EXCLUSIVE)
由 DROP TABLE, TRUNCATE, REINDEX, CLUSTER, VACUUM FULL, 和 REFRESH MATERIALIZED VIEW (不带 CONCURRENTLY)命令获取访问独占锁。也有很多形式, ALTER TABLE 在这个级别获取锁。这也是 LOCK TABLE 未明确指定模式的语句的默认锁定模式
与所有模式的锁冲突(ACCESS SHARE, ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, 和 ACCESS EXCLUSIVE)。这种模式保证持有者是唯一以任何方式访问表的事务。
隐式锁定示例:
kingbase=# vacuum full pgbench_accounts;
..
..
kingbase=# SELECT locktype,transactionid,virtualtransaction,mode FROM pg_locks ;
locktype | transactionid | virtualtransaction | mode
---------------+---------------+--------------------+---------------------
virtualxid | | 4/22 | ExclusiveLock
relation | | 3/12 | AccessShareLock
virtualxid | | 3/12 | ExclusiveLock
relation | | 4/22 | AccessExclusiveLock
transactionid | 749 | 4/22 | ExclusiveLock
relation | | 4/22 | AccessExclusiveLock
(6 rows)
显式锁定示例:
kingbase=# begin;
BEGIN
kingbase=# LOCK TABLE acl IN ACCESS EXCLUSIVE MODE;
LOCK TABLE
kingbase=# \! psql -f implicit.sql
pid | vxid | lock_type | lock_mode | granted | xid_lock | relname
------+------+-----------+---------------------+---------+----------+---------
2611 | 4/19 | relation | AccessExclusiveLock | t | | acl
(1 row)
9、表级锁的冲突
下面的表格,描述了锁定模式的冲突:
请求的锁模式 | 当前的锁模式 | |||||||
ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE | |
ACCESS SHARE | X | |||||||
ROW SHARE | X | X | ||||||
ROW EXCLUSIVE | X | X | X | X | ||||
SHARE UPDATE EXCLUSIVE | X | X | X | X | X | |||
SHARE | X | X | X | X | X | |||
SHARE ROW EXCLUSIVE | X | X | X | X | X | X | ||
EXCLUSIVE | X | X | X | X | X | X | X | |
ACCESS EXCLUSIVE | X | X | X | X | X | X | X | X |
表中“X”表示这两种表冲突,也就是不同的进程不能同时持有这两种锁。
可以得出的结论是
-
两个事务不能同时在同一张表上持有冲突模式的锁。
例如,如果存在正在进行的访问共享锁,则另一个会话无法获得独占访问。 -
许多事务可以同时持有非冲突锁定模式。
例如,根据上图,行共享锁与行独占锁不冲突,因此它们可以由多个事务/会话一次持有。 -
一些锁定模式是自冲突的
例如,一个ACCESS EXCLUSIVE 锁一次不能被多个事务持有 -
有些锁定模式,不会自冲突
例如,一个ACCESS SHARE锁可以由多个事务持有。
四、行级锁 ( Row-Level Locks )
一个事务可能会在相同的行上保持冲突的锁,甚至是在不同的子事务中。但是除此之外,两个事务永远不可能在相同的行上持有冲突的锁。行级锁不影响数据查询,它们只阻塞对同一行的写入者和加锁者。
KingbaseES 中的行级锁分为四种
- 更新(FOR UPDATE)
- 无密钥更新(FOR NO KEY UPDATE)
- 分享(FOR SHARE)
- 关键分享(FOR KEY SHARE)
1、更新(FOR UPDATE)
在某些情况下,您的数据库必须获得锁才能正确处理数据。
例如,
select * from my_table where my_condition;
--执行一些其他语句
update my_table set my_column = my_column where my_condition;
在这里,如果我的要求是使用 my_condition 获取数据,并执行一些其他语句,并针对相同的条件更新表。
在这里,因为 select 是一个访问共享锁(正如在上面了解到的), 所以在会话处理选择时,其他会话可能会修改相同的记录。
为了避免这种情况,我们使用 select for update 子句。
select ... for update 将选择某些行但也会锁定它们。这允许在当前事务中再次更新它们然后提交,就好像它们已经被当前事务更新,而另一个事务无法以任何方式修改这些行。
select * from my_table where my_condition;
select * from my_table where my_condition for update;
由于数据行被 my_condition 锁定,没有其他事务可以以任何方式修改它们,因此,事务隔离级别在这里没有区别。
select ... for update 阻止其他会话的以下操作
- UPDATE
- DELETE
- SELECT FOR UPDATE
- SELECT FOR NO KEY UPDATE
- SELECT FOR SHARE
- SELECT FOR KEY SHARE
2、无键更新(FOR NO KEY UPDATE)
select for no key updates 的行为类似于 select for update LOCKEY 子句,但它不会阻止 select for key SHARE。
如果正在对行执行处理,但不想阻止创建子记录,这是理想的选择。
3、共享(FOR SHARE)
行为类似于 FOR NO KEY UPDATE ,只是它在每个检索到的行上获取共享锁,而不是独占锁。一个共享锁会阻塞其他事务在这些行上执行 UPDATE、DELETE、SELECT FOR UPDATE 或者 SELECT FOR NO KEY UPDATE,但是它不会阻止它们执行 *SELECT FOR SHARE *或者 SELECT FOR KEY SHARE。
select ... for update nowait 该语句帮助我们了解资源被其他会话阻塞。
Select For Update 对外键的影响
kingbase=# \d emp
Table "public.emp"
Column | Type | Collation | Nullable | Default
--------+------------------------+-----------+----------+---------
id | integer | | not null |
sno | integer | | |
name | character varying(255) | | |
sal | character varying(100) | | |
dept | character varying(10) | | |
Indexes:
"emp_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"emp_fkey" FOREIGN KEY (dept) REFERENCES dept(name)
kingbase=# \d dept
Table "public.dept"
Column | Type | Collation | Nullable | Default
-------------+-----------------------+-----------+----------+---------
name | character varying(10) | | not null |
address | character varying(10) | | |
dept_status | character varying(10) | | |
Indexes:
"dept_pkey" PRIMARY KEY, btree (name)
Referenced by:
TABLE "emp" CONSTRAINT "emp_fkey" FOREIGN KEY (dept) REFERENCES dept(name)
kingbase=#
如果处理不当, Select for update 会对所有子表或外键表产生不良影响。例如,使用select for update从emp表中选择数据时,dept也将被锁定。这是必要的,否则就有可能打破外键约束。
在会话 1 中
(SESSION 1)#begin;
BEGIN
(SESSION 1)#select * from emp for update;
id | sno | name | sal | dept
----+-----+------+-----+-------
1 | 1 | A | 200 | IT
2 | 2 | B | 200 | IT
3 | 3 | C | 300 | SALES
(3 rows)
(SESSION 1)#
在会话 2 中
(SESSION 2)#begin;
BEGIN
(SESSION 2)#update dept set name='NIT' where name='IT';
...
...
锁定查询输出
kingabse=# \! psql -c "select pid, virtualxid vxid, locktype lock_type, mode lock_mode, granted, relation::regclass relname from pg_locks ; "
pid | vxid | lock_type | lock_mode | granted | relname
------+-------+-----------+------------------+---------+-----------
2700 | 4/31 | relation | AccessShareLock | t | emp_pkey
2700 | 4/31 | relation | RowShareLock | t | emp
3021 | 3/641 | relation | AccessShareLock | t | dept_pkey
3021 | 3/641 | relation | AccessShareLock | t | emp_pkey
3021 | 3/641 | relation | RowExclusiveLock | t | dept
3021 | 3/641 | relation | RowExclusiveLock | t | dept_pkey
3021 | 3/641 | relation | RowShareLock | t | dept
3021 | 3/641 | relation | RowShareLock | t | emp
3021 | 3/641 | tuple | AccessShareLock | t | emp
(9 rows)
我们可以通过 For Share Lock 来避免这种情况
会话 1 与共享锁定
(SESSION 1)#begin;
BEGIN
(SESSION 1)#select * from emp for share;
id | sno | name | sal | dept
----+-----+------+-----+-------
1 | 1 | A | 200 | IT
2 | 2 | B | 200 | IT
3 | 3 | C | 300 | SALES
(3 rows)
(SESSION 1)#
具有相同查询的会话 2
(SESSION 2)#update dept set name='NIT' where name='IT';
ERROR: update or delete on table "dept" violates foreign key constraint "emp_fkey" on table "emp"
DETAIL: Key (name)=(IT) is still referenced from table "emp"
(SESSION 2)#
锁定查询结果
kingabse=# \! psql -c "select pid, virtualxid vxid, locktype lock_type, mode lock_mode, granted, relation::regclass relname from pg_locks ; "
pid | vxid | lock_type | lock_mode | granted | xid_lock | relname
------+------+-----------+-----------------+---------+----------+----------
2700 | 4/32 | relation | AccessShareLock | t | | emp_pkey
2700 | 4/32 | relation | RowShareLock | t | | emp
(2 rows)
4、键共享(FOR KEY SHARE)
其行为类似于FOR SHARE,只是锁较弱:SELECT FOR UPDATE被阻止,而不是SELECT FOR NO KEY UPDATE。键共享锁会阻塞其他事务执行修改键值的DELETE或者UPDATE,但不会阻塞其他UPDATE,也不会阻止SELECT FOR NO KEY UPDATE、SELECT FOR SHARE或者SELECT FOR KEY SHARE。
5、行级锁冲突
要求的锁模式 | 当前的锁模式 | |||
FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE | |
FOR KEY SHARE | X | |||
FOR SHARE | X | X | ||
FOR NO KEY UPDATE | X | X | X | |
FOR UPDATE | X | X | X | X |
KingbaseES不会在内存里保存任何关于已修改行的信息,因此对一次锁定的行数没有限制。 不过,锁住一行会导致一次磁盘写,例如, SELECT FOR UPDATE将修改选中的行以标记它们被锁住,并且因此会导致磁盘写入。
五、咨询锁 ( Advisory Locks )
表级锁和行级锁对于应用程序来说已经足够了,但是在某些情况下,需要在应用程序中,增加一层锁定机制。
咨询锁可以解决此类与应用程序相关的问题,可用于以下场景:
- 应用程序在进行 API 调用时需要与多个服务通信时,例如微服务架构。
- 我们想计算并向我们的一些用户发送报告,但我们必须保证后台工作人员不会同时开始计算。
- 多节点任务调度器可以使用建议锁来协调工作人员,或分片中的任务分配。
下表描述了咨询锁
等级 | 类型 | 动作 | 函数 |
Session | Exclusive | Acquire | pg_advisory_lock |
Release | pg_advisory_unlock | ||
Try | pg_try_advisory_lock | ||
Shared | Acquire | pg_advisory_lock_shared | |
Release | pg_advisory_unlock_shared | ||
Try | pg_try_advisory_lock_shared | ||
Transaction | Exclusive | Acquire | pg_advisory_xact_lock |
Release | 锁在当前事务结束时自动释放 | ||
Try | pg_try_advisory_xact_lock | ||
Shared | Acquire | pg_advisory_xact_lock_shared | |
Release | 锁在当前事务结束时自动释放 | ||
Try | pg_try_advisory_xact_lock_shared |
如何使用这些咨询锁?
在我们了解如何使用这些锁之前,让我们了解一些关于它们的概念。
- 每个锁都与一个标识符相关联,该标识符可以是 32 位整数或 64 位 bigint。
- 会话级锁不绑定到任何数据库事务,一旦获得,它们需要由应用程序开发人员显式释放。
- 事务级咨询锁绑定到当前正在执行的事务,因此当事务结束时释放锁,无论是提交还是回滚。
- 排他咨询锁将阻止同一锁键上的任何排他或共享咨询锁。
- 共享咨询锁将阻止同一锁键上的任何独占咨询锁,同时仍允许为同一锁键获取其他共享咨询锁。
- 变体立即返回,try_ 您可以使用 boolean 结果值来验证是否已成功获取锁。
- 如果同一资源被锁定了 3 次,则必须将其解锁 3 次才能释放以供其他会话使用。
例子
每个锁都与一个标识符相关联,该标识符可以是 32 位整数或 64 位。
对于 64 位值,您可以将一个参数传递给函数 pg_advisory_lock(key)
(SESSION 1)#select pg_advisory_lock(1);
pg_advisory_lock
------------------
(1 row)
(SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory';
locktype | classid | objid | pid | mode | granted
----------+---------+-------+------+---------------+---------
advisory | 0 | 1 | 5758 | ExclusiveLock | t
(1 row)
22:11:50 (SESSION 1)#select pg_advisory_unlock(1);
pg_advisory_unlock
--------------------
t
(1 row)
对于 32 位值,您可以将两个参数传递给函数 pg_advisory_lock(key, key)
(SESSION 1)#select pg_advisory_lock(1,3);
pg_advisory_lock
------------------
(1 row)
(SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory';
locktype | classid | objid | pid | mode | granted
----------+---------+-------+------+---------------+---------
advisory | 1 | 3 | 5758 | ExclusiveLock | t
(1 row)
(SESSION 1)#select pg_advisory_unlock(1,3);
pg_advisory_unlock
--------------------
t
(1 row)
(SESSION 1)#
会话级锁不绑定到任何数据库事务,一旦获得,它们需要由应用程序开发人员显式释放。
(SESSION 1)#begin;
BEGIN
(SESSION 1)#select pg_advisory_lock(1,3);
pg_advisory_lock
------------------
(1 row)
(SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory';
locktype | classid | objid | pid | mode | granted
----------+---------+-------+------+---------------+---------
advisory | 1 | 3 | 5911 | ExclusiveLock | t
(1 row)
(SESSION 1)#commit;
COMMIT
(SESSION 1)#end;
WARNING: there is no transaction in progress
COMMIT
(SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory';
locktype | classid | objid | pid | mode | granted
----------+---------+-------+------+---------------+---------
advisory | 1 | 3 | 5911 | ExclusiveLock | t
(1 row)
(SESSION 1)#
即使我提交了交易,锁仍然存在。
事务级咨询锁绑定到当前正在执行的事务上,所以当事务结束时锁会被释放
(SESSION 1)#begin;
BEGIN
(SESSION 1)#select pg_advisory_xact_lock(1);
pg_advisory_xact_lock
-----------------------
(1 row)
(SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory';
locktype | classid | objid | pid | mode | granted
----------+---------+-------+------+---------------+---------
advisory | 0 | 1 | 5911 | ExclusiveLock | t
(1 row)
(SESSION 1)#commit;
COMMIT
(SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory';
locktype | classid | objid | pid | mode | granted
----------+---------+-------+-----+------+---------
(0 rows)
排他咨询锁将阻止同一锁键上的任何排他或共享咨询锁。
会话 1:使用锁键 1 获得排他锁
(SESSION 1)#begin;
BEGIN
(SESSION 1)#select pg_advisory_lock(1), * from emp;
pg_advisory_lock | id | sno | name | sal | dept
------------------+----+-----+------+-----+-------
| 2 | 2 | B | 200 | IT
| 3 | 3 | C | 300 | SALES
| 1 | 1 | A | 300 | IT
(3 rows)
(SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory';
locktype | classid | objid | pid | mode | granted
----------+---------+-------+------+---------------+---------
advisory | 0 | 1 | 5911 | ExclusiveLock | t
(1 row)
(SESSION 1)#
会话 2:尝试使用锁键 1 获取排他锁
(SESSION 2)#begin;
BEGIN
(SESSION 2)#select pg_advisory_lock(1), * from emp;
..
..
会话 2 被挂起,只有在会话 1 释放锁的情况下才能运行
在警报日志文件中收到的错误消息是
2020-04-13 22:37:07.829 IST [5975] LOG: process 5975 still waiting for ExclusiveLock on advisory lock [13451,0,1,1] after 1000.565 ms
2020-04-13 22:37:07.829 IST [5975] DETAIL: Process holding the lock: 5911. Wait queue: 5975.
2020-04-13 22:37:07.829 IST [5975] STATEMENT: select pg_advisory_lock(1), * from emp;
共享咨询锁将阻止同一锁键上的任何独占咨询锁,同时仍允许为同一锁键获取其他共享咨询锁。
会话 1:使用锁键 1 获取共享锁
(SESSION 1)#select pg_advisory_lock_shared(1), * from emp;
pg_advisory_lock_shared | id | sno | name | sal | dept
-------------------------+----+-----+------+-----+-------
| 2 | 2 | B | 200 | IT
| 3 | 3 | C | 300 | SALES
| 1 | 1 | A | 300 | IT
(3 rows)
会话 2:仍然使用锁键 1 获取共享锁
(SESSION 2)#select pg_advisory_lock_shared(1), * from emp;
pg_advisory_lock_shared | id | sno | name | sal | dept
-------------------------+----+-----+------+-----+-------
| 2 | 2 | B | 200 | IT
| 3 | 3 | C | 300 | SALES
| 1 | 1 | A | 300 | IT
(3 rows)
(SESSION 2)#
变体立即返回,try_ 您可以使用 boolean 结果值来验证是否已成功获取锁。
会话 1:获取锁
(SESSION 1)#select pg_advisory_lock(1);
pg_advisory_lock
------------------
(1 row)
会话 2 与 try 变体,它立即返回 false 而无需等待
(SESSION 2)#select pg_try_advisory_lock(1);
pg_try_advisory_lock
----------------------
f
(1 row)
(SESSION 2)#
如果同一资源被锁定了 3 次,则必须将其解锁 3 次才能释放以供其他会话使用。
(SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory';
locktype | classid | objid | pid | mode | granted
----------+---------+-------+------+---------------+---------
advisory | 0 | 2 | 6184 | ExclusiveLock | t
advisory | 0 | 1 | 6184 | ExclusiveLock | t
(2 rows)
在这里,我获得了两个锁,所以我需要运行两个解锁。
六、锁/锁空间的内存
这个内存组件是用来存储 KingbaseES 实例使用的所有类型的锁。这些锁在所有连接到数据库的后台服务器和用户进程之间共享。两个数据库参数的非默认较大设置,即max_locks_per_transaction 和 max_pred_locks_per_transaction,在某种程度上会影响此内存组件的大小。
kingbase.conf 文件中锁定空间的默认值。
-
deadlock_timeout = 1s
-
max_locks_per_transaction = 64
-
max_pred_locks_per_transaction = 64
-
max_pred_locks_per_relation = -2
-
max_pred_locks_per_page = 2
kingbase=# select pg_advisory_lock(v) from generate_series(1,10000) v;
ERROR: out of shared memory
HINT: You might need to increase max_locks_per_transaction.
kingbase=#
七、最佳实践
用下表回顾一下到目前为止我们对 KingbaseES 锁的理解。
SQL 语句 | |
---|---|
1 | SELECT |
2 | SELECT FOR UPDATE SELECT FOR SHARE |
3 | UPDATE DELETEF INSERT |
4 | VACUUM (without FULLL) ANALYZE CREATE INDEX CONCURRENTLY ALTER TABLE VALIDATE |
5 | CREATE INDEX(without CONCURRENTLY) |
6 | REFRESH MATERIALIZED VIEW CONCURRENTLY |
7 | DROP TABLE TRUNCATE REINDEX CLUSTER VACUUM FULL REFRESH MATERIALIZED VIEW (without CONCURRENTLY) ALTER TABLE ADD COLUMN ALTER TABLE DROP COLUMN |
- 如果在会话1中执行1,会话2必须等待7
- 如果在会话1中执行2,会话2必须等待6和7
- 如果在会话1中执行3,会话2必须等待5.6和7
- 如果在会话1中执行4,会话2必须等待4、5、6和7
- 如果在会话1中执行5,会话2必须等待3、4、6和7
- 如果在会话1中执行6,会话2必须等待2、3、4、5、6和7
- 如果在会话1中执行7,会话2必须等待1、2、3、4、5、6和7
例如
如果在会话1中执行1,会话2执行7,则必须等待。这意味着,如果在会话1中执行1(select * from emp),则会话2中的执行 7(drop table emp)必须等待。
1、锁定队列
考虑下面的例子,
02:39:12 (SESSION 1)#select *, pg_sleep(10000) from dept;
..
02:39:11 (SESSION 2)#alter table dept drop add1;
..
02:39:10 (SESSION 3)# alter table dept add add2 varchar(10);
..
现在,数据库上有什么锁
kingbase=# \! psql -f implicit.sql
pid | vxid | lock_type | lock_mode | granted | xid_lock | relname
------+-------+-----------+---------------------+---------+----------+-----------
8464 | 6/5 | relation | AccessExclusiveLock | f | | dept
8473 | 3/446 | relation | AccessShareLock | t | | dept
8473 | 3/446 | relation | AccessShareLock | t | | dept_pkey
8477 | 5/118 | relation | AccessExclusiveLock | f | | dept
(4 rows)
kingbase=# select pid,
kingbase-# usename,
kingbase-# pg_blocking_pids(pid) as blocked_by,
kingbase-# query as blocked_query
kingbase-# from pg_stat_activity
kingbase-# where cardinality(pg_blocking_pids(pid)) > 0;
pid | usename | blocked_by | blocked_query
------+----------+-------------+----------------------------------------
8477 | kingbase | {8473} | alter table dept drop add1;
8464 | kingbase | {8473,8477} | alter table dept add add2 varchar(10);
(2 rows)
- 会话 1 正在举行, AccessShareLock 而第 2 季和第 3 会话正在等待获取 AccessExclusiveLock 。请注意,锁在队列中,即会话 2 必须等待会话 1 释放锁,但会话 3 必须等待会话 1 和会话 2 释放锁。
- pg_dump 还持有 AccessShareLock
因此,如果有任何长时间运行的查询,或需要被 AccessShareLock 阻止的资源的查询,可以为后续查询实施以下之一:
- SET statement_timeout = 40;
- 将 lock_timeout 设置为 '10s'
八、利用并发
- Indexes:如上图所示,create index concurrently 属于第 4 类,不会阻塞第 3 类,这意味着 create index concurrently 不会阻塞表上的 INSERTS、UPDATES 和 DELETES,这在OLTP 环境。
- 物化视图:如上图所述,并发物化视图创建不会阻塞选择,这在报表应用环境中很重要。
九、需要重写SQL语句
例如,如果想完全替换表的内容,请在临时表中执行,然后使用更改命令将临时表重命名为主表,这样就可以避免在主表和测试表上长时间运行导入/导出。
十、主键和外键
需要复制的表上有主键,才能进行逻辑复制。如果没有,告警日志中将出现以下错误
18892 2020-04-12 17:27:24.267 IST [2709] DEBUG: unregistering background worker "logical replication worker for subscription 16458"
18893 2020-04-12 17:27:24.267 IST [2709] LOG: worker process: logical replication worker for subscription 16458 (PID 3888) exited with exit code 1
18894 2020-04-12 17:27:29.135 IST [2774] ERROR: there is no unique constraint matching given keys for referenced table "dept"
在这里,不是使用需要获取锁 7 的 alter table 命令创建主键,而是同时创建需要锁 5 的唯一索引,然后将其作为主键约束。
最后,请留意上图中提到的第 7 点,如果这需要运行时间,请尽量避免在工作时间进行,例如 VACUUM FULL。