PG的事务锁

上篇关于PG锁的文章,不过只是开了个头,并没有做深入的探讨。如果你理解了PG的PG_LOCKS表,那么对于PG的应用相关的锁也不难理解,比如我们打交道最多的PG事务锁。

对于PG数据库来说,事务锁的类型是transactionid,似乎刚刚看到这个名字的时候觉得有点费解。不过其实PG的事务锁锁定的就是事务号,通过事务号实现锁的互斥操作实际上在大多数数据库里都是类似的,只是其他数据库都称之为事务锁,而PG成为针对TransactionId的对象锁,以Oracle为例,Oracle的事务锁TX锁实际上也是指向了一个事务。不过PG的事务号还有一些让人觉得有点不容易理解的东西。我们来看一个例子,首先我们登录一个PG数据库,然后执行下面的查询语句:

SELECT locktype, relation::REGCLASS, virtualxid AS virtxid, transactionid AS xid, mode, granted FROM pg_locks WHERE pid = pg_backend_pid();

我们可以看到,我们刚刚登录的这个backend有两个锁,一个是relation锁,模式是AccessShareLock,一个是virtualxid锁,模式是ExclusiveLock,这是一个排它锁。在PG里,VirtualXid到底是怎么回事呢?

VirtualTransactionId这个数据结构定义在lock.h里,包含两个字段,一个是Backend启动顺序有关的BackendId,一个是本地事务号的LocalTransactionId(仅仅是Backend中的事务顺序,和系统的事务号没有任何关联关系),这是一个在短期内不会重复的数据结构,是一个唯一性的标识。大家都知道,PG的每个事务都需要记录在CLOG中,而很多事务可能是只读事务或者空事务,如果每个空事务或者只读事务都分配了一个真实的transactionid,那么CLOG中就会产生大量的浪费,TransactionId也会浪费。因此对于此类事务,只需要一个虚拟的XID,让这个虚拟xid只要在某个时间段内是唯一的,不会引起冲突就行了,这个虚拟XID不会被记录在CLOG中,因此PG采用了上面的这个数据结构来实现。

如果在本事务中,只有只读SQL,则不会产生真正的TransactionId。如果执行了一个DML操作,则会分配真正的XID。

当执行了一条UPDATE语句后,这个会话里产生了一个新的锁,tranactionid锁,而这个锁的xid正好是当前这个会话当前的transactionid。如果在另一个会话中执行一个同样修改这张表的操作,此时事务会HANG住。

在另外一个会话中,我们查询该会话的锁的情况。

可以看出,这个事务的TRANSACTIONID是11295998,在申请一个针对11295993事务的共享锁,不过没有获得。同时因为这个SQL会影响到test_covering表和索引idx_test_covering,因此在这两个对象上获得了一个RowExclusiveLock。在这个SQL执行完成之前,这些对象的DDL操作会受到锁的影响。最后一行是一个tuple锁,这个锁是以独占方式锁定了test_covering表上的某个元组。

PostgreSQL 将行锁排他性地存储在数据页内的行版本中而不是存储在共享内存里。这意味着它不是通常意义上的锁,而只是一些TUPLE中的标识。实际上PG使用在tuple的XMAX字段上实现行锁(Oracle则更为节省,使用行头里的一个表示位)。这种实现方式的优点是我们可以在不消耗任何资源的情况下锁定任意数量的行有利必有弊,由于锁的信息没有存储在共享内存中,其他会话要排队等待该行锁的时候,需要一些其他的算法来辅助实现,同时如果我们想查询哪些行被锁定了,就必须从PAGE中去统计了。这种锁的算法的另外一个副作用就是在DML增加了一个tuple类型的排它锁,这就是我们上面看到的情况。

如果某个会话需要等待某个行锁被释放,PG并不会等待这个行的行锁,而是需要等到锁定事务完成:所有锁在事务提交或回滚时被释放。并且为此,我们可以在锁定事务的 ID 上请求一个锁。因此,使用的锁数与同时运行的进程数成正比,而不是与正在更新的行数成正比。

元组锁(Tuple)是和数据行上的DML操作有关的锁,但是并不是行锁,上面一段已经说明了,行锁是通过元组里的XMAX和INFOMASK等来实现的。元组锁(TUPLE)是元组对象上的锁。那么元组锁是怎么产生的呢?这和DML的行锁实现有关。当事务要更改行时,它会执行以下步骤序列:

  1. 获取要更新的元组的排他锁;
  2. 如果 xmax 和信息位显示该行已锁定,则请求锁定 xmax 事务 ID(此时会产生TransactionId锁的等待请求);
  3. 写入自己的 xmax 并设置所需的信息位;
  4. 释放元组锁。

当第一个事务更新行时,它也获得了一个元组锁(步骤 1),但立即释放它(步骤 4)。当第二个事务到达时,它获得了一个元组锁(第 1 步,因为已经第一个会话被释放,所以能够直接获得),但通过XMAX发现该元组已经被会话一的事务锁定,不得不在这个事务的 ID 上请求一个TransactionId锁(第 2 步),并把这个操作挂起了。如果出现第三次类似的交易会怎样?它将尝试获取元组锁(第 1 步),因为这个tuple锁已经被会话二持有,因此会话三无法获得tuple锁,因此会直接挂起。我们可以通过做个实验来验证这个步骤。

会话1

会话2:已经获得了tuple锁

会话3:无法获得tuple锁

我们可以看到第三个会话的锁的数量和类型都和会话二类似,不过区别是等待的位置不同,这个会话并不是在等待TransactionId锁,而是在等待tuple锁。

至此行锁、tuple锁、事务锁、虚拟事务锁这几个概念都介绍完了,我想大家看到这里已经基本上了解了PG事务锁的实现方式。关于行锁的实现,以前老白写过几篇文章,大家有兴趣可以在公众号上查阅。

如果某个会话被锁了,通过pg_blocking_pids函数可以查找某个会话的阻塞者,从这里我们可以看到的这个117059就是第一个执行UPDATE操作的会话。

通过今天的分析,可能大家再去看pg_locks是不是更容易一些了呢?

  • 55
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值