代码改变世界

【PostgreSQL】PostgreSQL中的锁--行锁

2022-06-13 10:14  abce  阅读(2677)  评论(0编辑  收藏  举报

 

示例环境

postgres=# create table locktest(c int);
CREATE TABLE
postgres=# insert into locktest values(1),(2);
INSERT 0 2
postgres=# 

 

行锁

场景:两个并发事务尝试查询一行记录用于更新。

PostgreSQL在这个场景中使用行锁。行锁与MVCC的实现紧密集成,使用隐藏的xmin、xmax列。xmin和xmax存储事务的id。所有需要行锁的语句修改xmax(即使是select for update)。修改发生在查询返回结果之后,所以为了看到xmax的改变,我们需要运行select for update两次。通常,xmax列用于标记一行记录是否过期:要么被事务彻底移除;要么表示被更新的行版本。但是xmax也用与行级锁的基础架构。

postgres=# begin;
BEGIN
postgres=*# select xmin,xmax,txid_current(),c from locktest where c=1 for update;
 xmin | xmax | txid_current | c 
------+------+--------------+---
  743 |    0 |          744 | 1
(1 row)

postgres=*# select xmin,xmax,txid_current(),c from locktest where c=1 for update;
 xmin | xmax | txid_current | c 
------+------+--------------+---
  743 |  744 |          744 | 1
(1 row)

postgres=*# 

如果,有语句尝试修改相同的行,它会检查所有尚未完成的事务。必须等到事务id=xmax的事务完成,才能执行修改。

-- second connection
SELECT xmin,xmax,txid_current() FROM locktest WHERE c=1 FOR UPDATE;

select for udpate查询运行在第二个会话无法完成,在等待第一个事务结束。

pg_locks

可通过查询pg_locks来查看产生的等待和锁信息:

postgres=# SELECT locktype,transactionid,virtualtransaction,pid,mode,granted,fastpath FROM pg_locks WHERE transactionid=744;
   locktype    | transactionid | virtualtransaction |  pid  |     mode      | granted | fastpath 
---------------+---------------+--------------------+-------+---------------+---------+----------
 transactionid |           744 | 3/4883             | 25981 | ExclusiveLock | t       | f
 transactionid |           744 | 4/522              | 30293 | ShareLock     | f       | f
(2 rows)

postgres=# 

可以看到,transactionid=744。来看看持有锁的对应的pid和backend id

postgres=*# SELECT id,pg_backend_pid() FROM pg_stat_get_backend_idset() AS t(id) WHERE pg_stat_get_backend_pid(id) = pg_backend_pid();
 id | pg_backend_pid 
----+----------------
  3 |          25981
(1 row)

postgres=*# 

每个backend有一个操作系统进程标识符(pid)和PostgreSQL内部标识符(backend id)。PostgreSQL可以处理多个事务,但是锁定只能发生在backend之间,每个backend执行一个事务。内部记录只需要一个虚拟事务标识符:backend id和backend内部的序列号组成的对。

无论锁定的行数有多少,PostgreSQL在pg_locks表中都只有一个相关的锁。查询可能会修改数十亿行,但PostgreSQL不会为冗余锁结构浪费内存。

写线程在它的transactionid上设置ExclusiveLock。所有行级锁等待者都设置了 ShareLock。一旦写线程释放锁,锁管理器就会恢复所有之前锁定的backend锁。

锁释放发生事务提交或者回滚时。

pg_stat_activity

另一个查看锁信息的方式是查看表pg_stat_activity:

postgres=# select pid,backend_xid,wait_event_type,wait_event,state,query FROM pg_stat_activity WHERE pid IN (25981,30293);
-[ RECORD 1 ]---+---------------------------------------------------------------------------------------------------------------------------
pid             | 25981
backend_xid     | 744
wait_event_type | Client
wait_event      | ClientRead
state           | idle in transaction
query           | SELECT id,pg_backend_pid() FROM pg_stat_get_backend_idset() AS t(id) WHERE pg_stat_get_backend_pid(id) = pg_backend_pid();
-[ RECORD 2 ]---+---------------------------------------------------------------------------------------------------------------------------
pid             | 30293
backend_xid     | 745
wait_event_type | Lock
wait_event      | transactionid
state           | active
query           | select xmin,xmax,txid_current() from locktest where c=1 for update;

postgres=# 

源代码层研究

可以使用gdb和pt-mpt工具来跟踪。结果类似如下:

# pt-pmp -p 31369
Sat Jul 28 10:10:25 UTC 2018
30	../sysdeps/unix/sysv/linux/epoll_wait.c: No such file or directory.
      1 epoll_wait,WaitEventSetWaitBlock,WaitEventSetWait,WaitLatchOrSocket,WaitLatch,ProcSleep,WaitOnLock,LockAcquireExte

WaitOnLock函数是由等待引起的,该函数在lock.c文件中。 lock表是一个共享的内存hash表。冲突进程睡眠等待storage/lmgr/proc.c中的锁。大多数时候,这些代码由lmgr.c或另外一个锁管理模块调用,而不是直接调用。

pg_stat_activity中列出的锁,也被称作重量级锁,由锁管理器管理。重量级锁也被用于很多高级别的操作。

总结

·避免长时间运行的事务修改频繁更新的行或过多的行

·不要在支持MVCC的数据库中使用热点数据(由多个应用程序客户端连接并行更新的单行或多行)。这种工作负载更适合内存数据库,通常可以与主要业务逻辑分离。