• Postgresql源码(68)virtualxid锁的原理和应用场景


    相关:
    《Postgresql源码(40)Latch的原理分析和应用场景》
    《Postgresql源码(67)LWLock锁的内存结构与初始化》
    《Postgresql源码(68)virtualxid锁的原理和应用场景》
    《Postgresql源码(69)常规锁简单分析》

    0 总结速查

    • 事务的vxid.localTransactionId并不会变,只在begin时申请一次,也就是一个事务共享一个localTransactionId。事务内的fpVXIDLock一直是true状态,直到换成LWLock。
    • fpVXIDLock是fast-path锁,真正需要等锁时会把fpVXIDLock换成LWLock去等锁,换锁&加锁逻辑在VirtualXactLock函数中封装好了。
    • 在查看锁表时,每个会话都会有申请vxid,在事务启动时申请一次。
    • 在查看锁表时,如果发现vxid锁未granted,他等待的就是virtualtransaction列的vxid,或者用pg_blocking_pid也可以查询在等谁。

    1 前言

    我们在空载数据库上查询pg_locks:
    在这里插入图片描述

    • 第一行很明显是需要查询pg_locks视图,所以加了AccessShareLock锁。
    • 第二行就是本文要讨论的重点,为什么PG每次查询都要加一个virtualxid锁,virtualxid是在哪里加的?

    2 virtualxid加锁位置

    回到vxid的获取位置:

    #0  VirtualXactLockTableInsert (vxid=...) at lock.c:4477
    #1  0x0000000000583157 in StartTransaction () at xact.c:2019
    #2  0x0000000000583f43 in StartTransactionCommand () at xact.c:2870
    #3  0x0000000000978c12 in start_xact_command () at postgres.c:2689
    #4  0x0000000000976393 in exec_simple_query (query_string=0x2d65500 "select 1;") at postgres.c:989
    #5  0x000000000097acbd in PostgresMain (a) at postgres.c:4494
    #6  0x00000000008b6eb2 in BackendRun (port=0x2d86a60) at postmaster.c:4530
    #7  0x00000000008b6831 in BackendStartup (port=0x2d86a60) at postmaster.c:4252
    #8  0x00000000008b2ca9 in ServerLoop () at postmaster.c:1745
    #9  0x00000000008b257b in PostmasterMain (argc=1, argv=0x2d5f0e0) at postmaster.c:1417
    #10 0x00000000007b4df7 in main (argc=1, argv=0x2d5f0e0) at main.c:209
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在函数VirtualXactLockTableInsert中:

    void
    VirtualXactLockTableInsert(VirtualTransactionId vxid)
    {
    	//【1】
    	LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
    	//【2】
    	MyProc->fpVXIDLock = true;
    	//【3】
    	MyProc->fpLocalTransactionId = vxid.localTransactionId;
    	//【4】
    	LWLockRelease(&MyProc->fpInfoLock);
    	//【5】
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    1. 在位置【1】查询pg_locks的结果:为空
      在这里插入图片描述

    2. 在位置【2】、【3】、【4】查询pg_locks是会卡住(pg_locks视图对应pg_lock_status()函数,函数会遍历PGPROC拿fpInfoLock,所以会发生等锁);

    3. 在位置【5】查询pg_locks:出现virtualxid排他锁
      在这里插入图片描述
      所以我们看到的virtualxid排他锁,其实是MyProc->fpVXIDLock = true的结果,注意没有LWLock。

    2 virtualxid的组成

    typedef struct
    {
    	BackendId	backendId;		/* backendId from PGPROC */
    	LocalTransactionId localTransactionId;	/* lxid from PGPROC */
    } VirtualTransactionId;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    从上面分析来看,virtualxid加锁的同时也更新了MyProc->fpLocalTransactionId的值,这个值记录的是本地事务ID:LocalTransactionId,直观上可以理解为当前会话的命令ID,随着命令执行+1递增。

    LocalTransactionId的获取位置:启动任意一个会话后,后台私有内存会创建一个本地事务ID计数器:

    LocalTransactionId
    GetNextLocalTransactionId(void)
    {
    	LocalTransactionId result;
    
    	/* loop to avoid returning InvalidLocalTransactionId at wraparound */
    	do
    	{
    		result = nextLocalTransactionId++;
    	} while (!LocalTransactionIdIsValid(result));
    
    	return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    每次本地会话启动一个事务,都会在StartTransaction中调用GetNextLocalTransactionId拿到一个本地事务ID,并将nextLocalTransactionId++

    注意:

    • 事务的vxid.localTransactionId并不会变,只在begin时申请一次!也就是一个事务共享一个localTransactionId
    • 事务内的fpVXIDLock一直是true状态。

    3 virtualxid没有lwlock如何发生锁竞争?

    例如创建索引场景,需要等待表上的写事务结束.

    场景

    • session1删除表一半数据,vxid={3,21},事务未提交。
    • session2创建索引卡住,等待session事务结束。

    session2等锁发生过程分析

    【1】找到需要等待的会话的vxid={3,21}

    (gdb) p *lockholders
    $8 = {backendId = 3, localTransactionId = 21}
    
    • 1
    • 2

    【2】调用VirtualXactLock(*lockholders, true)等待vxid={3,21},现在锁表中可以看到:建索引本身的vxid4/46,建索引需要等老事务结束,所以用vxid等另外一个会话结束,可以看到最后一行在请求别人的vxid3/21
    在这里插入图片描述
    【3】VirtualXactLock函数进入后,找到vxid={3,21}的PROC,将假锁proc->fpVXIDLock换成真锁lwlock。(假锁 无法使用LWLock机制等锁)

    	if (proc->fpVXIDLock)
    	{
    		PROCLOCK   *proclock;
    		uint32		hashcode;
    		LWLock	   *partitionLock;
    
    		hashcode = LockTagHashCode(&tag);
    
    		partitionLock = LockHashPartitionLock(hashcode);
    		LWLockAcquire(partitionLock, LW_EXCLUSIVE);
    
    		proclock = SetupLockInTable(LockMethods[DEFAULT_LOCKMETHOD], proc,
    									&tag, hashcode, ExclusiveLock);
    		...
    		GrantLock(proclock->tag.myLock, proclock, ExclusiveLock);
    
    		LWLockRelease(partitionLock);
    
    		proc->fpVXIDLock = false;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1. 如果proc->fpVXIDLock == true,用vxid = {3,21}申请tag = {locktag_field1 = 3, locktag_field2 = 21}
    2. 【SetupLockInTable】在共享内存索引哈希表(LockMethodLockHash)中查询tag是否存在,如果没有创建出来。
    3. 【SetupLockInTable】查到了直接用传入的ExclusiveLock把LWLock锁上。
    4. 释放假锁proc->fpVXIDLock == true,现在锁表中已经有一把tag = {locktag_field1 = 3, locktag_field2 = 21}的LWLock了,不再需要fastpath锁fpVXIDLock。

    【4】开始等3/21结束

    	/* Time to wait. */
    	(void) LockAcquire(&tag, ShareLock, false, false);
    	LockRelease(&tag, ShareLock, false);
    
    • 1
    • 2
    • 3

    4 virtualxid应用场景

    目前只发现这两处用法:

    1. 除了上述创建索引的场景,并行创建索引的第三阶段也用到了VirtualXactLock来等待老快照的结束。
    2. 热备读写冲突时,如果默认超时30秒,会把读会话kill掉继续做日志,这个kill的机制也是用vxid来实现的。
  • 相关阅读:
    排序算法之-冒泡
    Educational Codeforces Round 17 ACD
    doker的多容器操作和强制删除容器的方法
    提升代码重用性:模板设计模式在实际项目中的应用
    操作系统概述
    给你的R语言再次提速
    Shiro和Cas的集成
    【vue】使用无障碍工具条(详细)
    KubeVela跨地域的多集群管理方案
    CK04# ClickHouse日志存储调优总结
  • 原文地址:https://blog.csdn.net/jackgo73/article/details/126242404