磁盘一把锁一个感叹号_TBase中的一些锁
1.前言
锁在生活中随处可见,电子锁、挂锁、门锁等等。在数据库中,同样也存在着各式各样的锁,表级锁、行级锁、页锁等等,数据库的并发能力除了和它的并发控制机制有关, 还和数据库的锁粒度控制息息相关,粒度越细冲突范围就越小,并发能力就越强。锁的最终目的无外乎是为了保证数据的一致性和完整性。
2.锁
为了确保复杂的事务可以安全地同时运行,TBase提供了各种级别的锁来控制对各种数据对象的并发访问,使得对数据库关键部分的更改序列化。事务并发运行,直到它们尝试获取互相冲突的锁为止(比如两个事务更新同一行时)。当多个事务同时在数据库中运行时,并发控制是一种用于维持一致性和隔离性的技术,在TBase中,使用快照隔离Sanpshot Isolation(简称SI)来实现多版本并发控制,同时以两阶段锁定(2PL)机制为辅。在执行DDL时使用2PL,在执行DML时使用SI。
在TBase中,最主要的是表级锁与行级锁,此外还有页锁、咨询锁和死锁等。
锁根据是否对用户可见可以进一步划分,Regular Locks、Advisory Locks和Dead Locks属于可见的锁,而Spin Lock、LWLock和SIRead属于不可见的锁。
2.1.Table-Level Locks
在TBase中,表级锁分为8种,如下:
上图不方便理解和记忆,将对应的锁转换成常见的SQL就明了了,如下:
ACCESS SHARE和ACCESS EXCLUSIVE可以理解为多版本读/写,SELECT会在查询的表上获取ACCESS SHARE,而那些很硬的操作,诸如TRUNCATE、DROP TABLE等都会获取ACCESS EXCLUSIVE。
ROW SHARE和ROW EXCLUSIVE可以理解为意向读/写锁,意向锁根据名字,就是意向做一件事,但并非实际执行,所以可以看到ROW SHARE和ROW EXCLUSIVE之间互不冲突。当要更新插入时,需要先在对应的表上获取ROW EXCLUSIVE锁。
SHARE和EXCLUSIVE为传统的读写锁,在TBASE中有点变化,EXCLUSIVE锁出现频率很低,SHARE锁用在了创建索引的时候,因为SHARE锁不自斥,所以也就意味着在一张表上可以同时创建多个索引,但是会堵塞插入更新等(和ROW EXCLUSIVE冲突)。
而SHARE UPDATE EXCLUSIVE和SHARE ROW EXCLUSIVE两种锁比较难记忆,SHARE UPDATE EXCLUSIVE在VACUUM和ALTER TABLE SET(XXX)等操作时会获取,因为SHARE UPDATE EXCLUSIVE是自斥的,所以目前TBASE无法做到表级的并行VACUUM,但可以做到库级的并行VACUUM(好消息是,在刚刚发布不久的POSTGRESQL13中,对索引的清理支持并行了,TBase在不久也会合入相关代码);SHARE ROW EXCLUSIVE出现的概率较低,一般常见的是创建触发器。
2.2.Row-Level Locks
表级锁存在于TBase的shared buffers中,所以可以很直观的通过pg_locks查看。但是行级锁不一样,TBase不会将已获得的行级锁信息存储在内存中,如果在pg_locks中查看到了行锁信息,并不代表行锁信息存储在内存中,这种情况往往出现在行锁等待,比如后文会介绍的“tuple锁”,行锁通常只在tuple的header中设置标记位来标识此行记录已经被锁。这两个关键的标记为xmax和infomask(HEAP_UPDATED)。xmax放置当前事务的xid,infomask放置标记位。
假设每个行锁都在内存有一条记录的话,全表更新时,表有多少行,就在内存中有多少条行锁信息,那么内存会吃不消。设置infomask的目的是为了与正常的dead tuple区别开来,xmax是用来标识被删除的记录。基于这点,理论上一次可以锁定的行数没有限制。
另外这里可以衍生出一个经典PG系面试题,select是否会产生写?除了较为熟知的设置标志位(后面的DML或者DQL,VACUUM等SQL扫描到对应的TUPLE时,会触发对前面扫描到的行set bits的操作),还有这里的行级锁也会有写IO。当然,行级锁也并非不能观察,可以通过pgrowlocks和pageinspect插件来观察。
行级锁的冲突矩阵如下:
FOR UPDATE:对整行进行更新,包括删除行
FOR NO KEY UPDATE:对除主(唯一)键外的字段更新
FOR SHARE:读该行,不允许对行进行更新
FOR KEY SHARE:读该行的键值,但允许对除键外的其他字段更新。在外键检查时使用该锁
2.3.Page-Level Locks
TBase中,有对于表的数据页的共享/互斥锁,一旦这些行读取或者更改完成后,相应的页锁就被释放。
比较常见的有:
WalInsertLock,wal buffer是固定大小的,向wal buffer中写wal record需要竞争的锁,如果把synchronous_commit关闭,这个锁的竞争会更加激烈;
WALWriteLock:一般都是同步提交,要保证commit时,wal是刷盘的,那么刷盘就会竞争这个锁。将页面写入磁盘会受WALWriteLock锁保护,一次只能有一个进程可以执行此操作。
ProcArrayLock,对于每一个连接,在shared buffer中都有一个结构体PGPROC用于追踪正在运行的后端进程和事务,每个backend在事务在commit或abort时,都要对其PGPROC和PGXACT中关于事务状态的属性进行设置,标记对应的事务不再运行。
一般不怎么需要关心页锁,但是假如在pg_stat_activity视图里面看到了大量相关的等待事件,也需要特别注意。比如出现了大量的BufferMapping,说明buffer发生了频繁替换导致了锁等待,可能存在大量表扫描,buffer太小了或io存在瓶颈、磁盘太烂了等原因。
2.4.Spin Lock
编程中常听说的自旋锁,TBase中同样也有,SpinLock主要用于对于临界变量的并发访问控制,所保护的临界区通常是简单的赋值语句,读取语句等。另外,自旋锁的显著特点就是死等,占着茅坑不拉屎,SpinLock没有等待队列、死锁检测机制,在事务结束之后不会自动释放,需要每次显式释放。在TBase中,使用CPU指令集test-and-set或者信号量来实现自旋锁。
2.5.Deadlocks
死锁在各大主流数据库中都有,当一个事务试图获取另一个事务已在使用的资源,而第二个事务又试图获取第一个事务在使用的资源时,就会发生死锁,在TBase中,有lock_timeout、deadlock_timeout、log_lock_waits(日志记录)等参数控制,另外TBase中还有一个专门的死锁检测进程pg_unlock。死锁检测是一件十分耗费资源的事情,大多数时候,一旦检测到死锁,则其中一个事务(大多数情况下是发起检查的事务)将被迫中止。
2.6.Advisory Locks
当MVCC模型和锁策略不符合应用时,采用咨询锁。TBase允许用户创建咨询锁,该锁与数据库本身没有关系,比如多个进程访问同一个数据库时,如果想协调这些进程对一些非数据库资源的并发访问,就可以使用咨询锁。咨询锁是提供给应用层显示调用的锁方法,在表中存储一个标记位能够实现同样的功能,但是咨询锁更快;并且避免表膨胀,且会话(或事务)结束后能够被自动清理。
2.7.实战
TBase是CN + DN + GTM的经典架构,锁的情况在CN和DN上是独立的,需要在各个节点上观察。
在CN上创建一张test_lock的测试表:
psql (PostgreSQL 10.0 TBase V2) Type "help" for help. postgres=# create table test_lock(id int primary key,info text); CREATE TABLE postgres=# insert into test_lock values(1,'test1'),(2,'test2'); COPY 2 postgres=# begin; BEGIN postgres=# select pg_backend_pid(),txid_current(); pg_backend_pid | txid_current ----------------+-------------- 17729 | 142160 (1 row) postgres=# update test_lock set info = 'mytest_lock' where id = 1; UPDATE 1 |
此处未提交,同时再开启一个会话,进行更新
postgres=# begin; BEGIN postgres=# select pg_backend_pid(),txid_current(); pg_backend_pid | txid_current ----------------+-------------- 19135 | 142162 (1 row) postgres=# update test_lock set info = 'mytest_lock2' where id = 1; 此处会夯住 |
在CN上查看,未发现锁等待(granted = t)
postgres=# select locktype,relation::regclass as relname,page,tuple,pid,mode,granted from pg_locks where pid in (17729,19135); locktype | relname | page | tuple | pid | mode | granted ---------------+-----------+------+-------+-------+------------------+--------- relation | test_lock | | | 19135 | AccessShareLock | t relation | test_lock | | | 19135 | RowExclusiveLock | t virtualxid | | | | 19135 | ExclusiveLock | t relation | test_lock | | | 17729 | AccessShareLock | t relation | test_lock | | | 17729 | RowExclusiveLock | t virtualxid | | | | 17729 | ExclusiveLock | t transactionid | | | | 19135 | ExclusiveLock | t transactionid | | | | 17729 | ExclusiveLock | t (8 rows) |
锁的阻塞通常需要在DN上查看。以下可以看到,1289这个进程被1290阻塞住,1290进程处于idle in transaction的状态,也就是还未提交,1289进程正在等待某一个事务结束。
postgres=# select pid,pg_blocking_pids(pid) as blocked,state,query,wait_event,wait_event_type from pg_stat_activity where query like '%UPDATE%' and pid <> pg_backend_pid(); pid | blocked | state | query | wait_event | wait_event_type ------+---------+---------------------+-----------------------------------------------------------------+---------------+----------------- 1289 | {1290} | active | UPDATE test_lock SET info = 'mytest_lock2'::text WHERE (id = 1) | transactionid | Lock 1290 | {} | idle in transaction | UPDATE test_lock SET info = 'mytest_lock'::text WHERE (id = 1) | ClientRead | Client (2 rows) |
pg_stat_activity视图可以看一个大概,具体锁信息还需要结合pg_locks查看,SQL如下:select relation::regclass as relname,locktype,'('||page||','||tuple||')' as ctid,transactionid,virtualtransaction,virtualxid,pid,mode,granted from pg_locks where pid in (1289,1290) order by pid;
可以看到,1290这个进程持有两个ExclusiveLock,分别是事务ID和虚拟事务ID,virtualxid是虚拟事务ID,每产生一个事务ID,都会在commit log(CLOG)文件中占用2bit。但有些事务没有产生任何实质的变更,比如只读事务或空事务,这样给它们也分配一个事务ID就很浪费。因此对这类没有实质变更的事务只分配虚拟事务ID,比如查询。另外也可以防止事务ID快速回卷(大约每满21亿就会发生回卷)。
在整个事务运行过程中,服务器进程对虚拟事务ID持有排他锁。如果分配了永久性事务ID,它还将对永久性事务ID持有排他锁,直到结束。所以印证了上面的说法。同时1290这个进程对test_lock表加上了RowExclusiveLock,这个仍然是表级锁。当1290进程想要修改表上的某一行时,会在这一行上加上行级锁。但在加行级锁之前,还需要先在这张表上加上一把意向锁,表示自己将会在表中的若干行上加锁,这样其他事务来了就可以知道,表上还有意向锁,说明已经有某些行上持有了锁。因此,RowExclusiveLock是行级锁(对应级别低的资源)的上面资源的锁(对应级别高的资源,此处为表),换言之,给表加上了RowExclusiveLock也就意味着自己准备在表内的某些行上加锁。
1289这个进程,同样可以看到持有两个RowExclusiveLock锁,因为还涉及到了主键索引;同时也对事务ID和虚拟ID持有ExclusiveLock,当某个进程发现需要等待另一笔事务结束时,它还会通过尝试获取另一笔事务ID(可能是虚拟ID也可能是永久事务ID)上的共享锁。仅当另一个事务终止并释放锁时,该操作才会成功。所以此处可以看到,1289进程在申请150841事务的共享锁,由于1290进程已经持有对150841事务的ExclusiveLock,所以会阻塞住。其实换个角度思考也不难理解,因为第一个150841事务对test_lock表进行了更改,但未提交,所以也就可能提交也可能回滚,然后150843事务也需要对表的同一行进行更改,并且依赖于150841事务的“结果”,所以会等待150841事务。而看到的locktype = tuple的这一行,注意不要和行级锁混淆,此处的tuple代表tuple锁,tuple锁可以保证多个修改事务加锁的顺序问题,原则是先来先拿锁,修改完tuple后,tuple锁会立即释放,而事务锁不会释放。所以并未看到1290进程的tuple锁,原因是更改成功并立即释放了。假设有3个事务,A、B、C依次对同一行修改,均未提交。那么这个时候,A处于idle in transaction状态,B持有tuple锁但是等待A的事务锁,C等待B持有的tuple锁。
在CN上再开一个会话,更新同一行,DN上可以看到,新来的24268进程被1289阻塞,1289进程被1290阻塞,位于一个锁队列中,同时可以看到24268进程在等待给tuple加锁,但1289进程已经持有了tuple锁,但在等待1290进程的事务锁。
postgres=# select pid,pg_blocking_pids(pid) as blocked,state,query,wait_event,wait_event_type from pg_stat_activity where query like '%UPDATE%' and pid <> pg_backend_pid(); pid | blocked | state | query | wait_event | wait_event_type -------+---------+---------------------+-----------------------------------------------------------------+---------------+----------------- 1289 | {1290} | active | UPDATE test_lock SET info = 'mytest_lock2'::text WHERE (id = 1) | transactionid | Lock 1290 | {} | idle in transaction | UPDATE test_lock SET info = 'mytest_lock'::text WHERE (id = 1) | ClientRead | Client 24268 | {1289} | active | UPDATE test_lock SET info = 'mytest_lock2'::text WHERE (id = 1) | tuple | Lock (3 rows) postgres=# select relation::regclass as relname,locktype,'('||page||','||tuple||')' as ctid,transactionid,virtualtransaction,virtualxid,pid,mode,granted from pg_locks where pid in (1289,1290,24268) and locktype = 'tuple' order by pid; relname | locktype | ctid | transactionid | virtualtransaction | virtualxid | pid | mode | granted -----------+----------+-------+---------------+--------------------+------------+-------+---------------+--------- test_lock | tuple | (0,1) | | 15/1214049 | | 1289 | ExclusiveLock | t test_lock | tuple | (0,1) | | 92/331586 | | 24268 | ExclusiveLock | f (2 rows) |
至此,锁分析清楚了。由于TBase暂未集成pageinspect和pgrowlocks插件,所以为了演示,以下在PostgreSQL中的环境,大同小异。老样子开两个会话
postgres=# begin; BEGIN postgres=# select txid_current(),pg_backend_pid(); txid_current | pg_backend_pid --------------+---------------- 545 | 10698 (1 row) postgres=# update test_lock set id = 100 where id = 1; UPDATE 1 |
session 2更新同一行
postgres=# begin; BEGIN postgres=# select txid_current(),pg_backend_pid(); txid_current | pg_backend_pid --------------+---------------- 547 | 10903 (1 row) postgres=# update test_lock set id = 100 where id = 1; 此处夯住 |
通过pgrowlocks查看行级锁,很明了,10698进程的545这个事务,持有No Key Update的行级锁,因为没有多个事务,所以此处的multi为f
postgres=# select * from test_lock as t,pgrowlocks('test_lock') as lc where t.ctid = lc.locked_row; id | locked_row | locker | multi | xids | modes | pids ----+------------+--------+-------+-------+-------------------+--------- 1 | (0,1) | 545 | f | {545} | {"No Key Update"} | {10698} (1 row) |
如果是多个事务同时去上锁一行记录,那么就会使用multixact。继续在会话1中执行:
postgres=# select * from test_lock where id = 2 for update; id ---- 2 (1 row) postgres=# select * from test_lock where id = 3 for share; id ---- 3 (1 row) |
查看行锁
postgres=# select * from test_lock as t,pgrowlocks('test_lock') as lc where t.ctid = lc.locked_row; id | locked_row | locker | multi | xids | modes | pids ----+------------+--------+-------+-------+-------------------+--------- 1 | (0,1) | 545 | f | {545} | {"No Key Update"} | {10698} 2 | (0,2) | 545 | f | {545} | {"For Update"} | {10698} 3 | (0,3) | 545 | f | {545} | {"For Share"} | {10698} (3 rows) |
新开一个会话,并且也获取for share的行级锁
postgres=# begin; BEGIN postgres=# select pg_backend_pid(),txid_current(); pg_backend_pid | txid_current ----------------+-------------- 17936 | 551 (1 row) postgres=# select * from test_lock where id = 3 for share; id ---- 3 (1 row) |
再次查看行锁,这次可以看到使用了multixact。因为对于FOR SHARE和FOR KEY SHARE,一行上面可能会被多个事务加锁,Tuple上动态维护这些事务代价很高,为此引入了multixact机制,将多个事务记录到MultiXactId,再将MultiXactId记录到tuple的xmax中。
postgres=# select * from test_lock as t,pgrowlocks('test_lock') as lc where t.ctid = lc.locked_row; id | locked_row | locker | multi | xids | modes | pids ----+------------+--------+-------+-----------+-------------------+--------------- 1 | (0,1) | 545 | f | {545} | {"No Key Update"} | {10698} 2 | (0,2) | 545 | f | {545} | {"For Update"} | {10698} 3 | (0,3) | 1 | t | {545,551} | {Share,Share} | {10698,17936} (3 rows) |
此外,对于锁,可以使用如下SQL快速观察,也可以创建成视图
postgres=# SELECT blocked_locks.pid AS blocked_pid, blocked_activity.usename AS blocked_user, blocking_locks.pid AS blocking_pid, blocking_activity.usename AS blocking_user, blocked_activity.query AS blocked_statement, blocking_activity.query AS current_statement_in_blocking_process FROM pg_catalog.pg_locks blocked_locks JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocking_locks.pid != blocked_locks.pid JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid WHERE NOT blocked_locks.granted; blocked_pid | blocked_user | blocking_pid | blocking_user | blocked_statement | current_statement_in_blocking_process -------------+--------------+--------------+---------------+---------------------------------------------+--------------------------------------------- 27592 | postgres | 27574 | postgres | update test_lock set id = 100 where id = 1; | update test_lock set id = 100 where id = 1; |
可以看到,27592进程被27574进程阻塞,被阻塞语句是update test_lock set id = 100 where id = 1;
3.小结
本篇主要介绍了TBase中常见的锁,其中最为常见的是RegularLock,RegularLock又可以分为表级锁和行级锁,由于行级锁不在内存中,一般行级锁的等待表现为等待另一个会话的transacitonid Exclusive Lock的释放,显示的形式通常是一个事务等待另一个事务,而不是等待某个具体的行锁。相比于SpinLock、LWLock,RegularLock加锁的开销更大,但是提供更加丰富的锁模式,为数据库不同的操作场景提供了更细粒度的锁冲突控制,尽可能地提供了数据库的高并发访问。而LWLock主要提供对共享内存变量的互斥访问,比如Clog buffer(事务提交状态缓存)、Shared buffers(数据页缓存)、Substran buffer(子事务缓存)等等,所以backend进程独有的buffer是不需要锁的,因为它只被本backend进程独享,是local的。如临时表等。SpinLock主要用于对于临界变量的并发访问控制,另外TBase中的咨询锁对于应用来说也是一个福音,可以避免争用数据库资源,至于死锁,在任何数据库中都是需要特别注意的DB Killer。
4.参考
https://habr.com/en/company/postgrespro/blog/504498/
https://www.postgresql.org/docs/12/explicit-locking.html
https://wiki.postgresql.org/wiki/Lock_Monitoring
https://www.postgresql.org/docs/current/monitoring-stats.html#WAIT-EVENT-ACTIVITY-TABLE
磁盘一把锁一个感叹号_TBase中的一些锁相关推荐
- android 监听锁屏 权限,Android中监听锁屏变化和防止锁屏
Android app中可能存在某些可视化耗时操作,需要防止锁屏. 一.监听锁屏 添加权限 首先来看如何监听锁屏,使用BroadcastReceivercaset来监听 1.锁屏监听 public c ...
- 小米id锁状态查询_Mysql中的三类锁,你知道吗?
点击上方"码农沉思录",选择"设为星标" 优质文章,及时送达 导读 正所谓有人(锁)的地方就有江湖(事务),人在江湖飘,怎能一无所知? 今天不聊江湖,来细说一下 ...
- 磁盘一把锁一个感叹号_电脑C盘出现一把锁和黄色感叹号是什么原因,求大神赐教。...
bitlocker功能要到控制面板bitlocker里面关闭www.mh456.com防采集. 1.打开2113控制面板--进入bitlocker磁盘加密.5261 Chkdsk 命令,是微软提供的在 ...
- java lock unlock_详解Java中的ReentrantLock锁
ReentrantLock锁 ReentrantLock是Java中常用的锁,属于乐观锁类型,多线程并发情况下.能保证共享数据安全性,线程间有序性 ReentrantLock通过原子操作和阻塞实现锁原 ...
- MySQL中的各种锁(行锁、间隙锁、临键锁等等LBCC)
目录 1.快照读和锁定读 1.1 一致性读 / 快照读 1.2 锁定读 1.2.1 共享锁和独占锁 1.2.2 锁定读的语句 1.2.2.1 Lock In Share Mode 对记录加S共享锁 1 ...
- 可重入锁 不可重入锁_什么是可重入锁?
可重入锁 不可重入锁 在Java 5.0中,增加了一个新功能以增强内部锁定功能,称为可重入锁定. 在此之前,"同步"和"易失性"是实现并发的手段. public ...
- 可重入锁_什么是可重入锁?
可重入锁 在Java 5.0中,增加了一个新功能以增强内部锁定功能,称为Reentrant Lock. 在此之前,"同步"和"易失"是实现并发的手段. publ ...
- mysql5.7官网直译锁操作优化--并发添加,元数据锁,外部闭锁
8.11.3 Concurrent Inserts 并发插入 MyISAM存储引擎支持并发插入从而来减少对读写对给出表的竞争:如果一张MyISAM表的数据文件没有漏洞存在(也就是在表中删除了中间的行) ...
- 将磁盘上的一个文本文件的内容复制到另一个文件中
<程序设计基础实训指导教程-c语言> ISBN 978-7-03-032846-5 p198 8.1.2 上级实训内容 [实训内容2]将磁盘上的一个文本文件的内容复制到另一个文件中 #in ...
最新文章
- 一口一个,超灵活的Python迷你项目
- 浅析Java内存模型--ClassLoader
- 怎么把模组直接装在Java里面_如何使用jythonj将python模块添加到java中
- markdown如何设置图片大小_不会吧,还不会用markdown排版吗
- asterisk概述和代码分析
- ES6新特性_let使用案例---JavaScript_ECMAScript_ES6-ES11新特性工作笔记004
- 你的代码,“拯救”过多少人?
- java 为何 无效_java – 为什么compareTo无效导致Collections.sor...
- 洛谷 1563 玩具谜题——模拟水题
- 声纹识别的模式识别方法
- Eclipse安装Tomcat插件全攻略
- c语言编译器turbo,C语言编译器TurboC使用技巧解析
- Java常用实现八种排序算法与代码实现
- android代码改字体颜色,如何更改Android Studio的代码字体和颜色
- 速卖通店铺流量下滑什么原因,如何做提升?(测评补单)
- Unity3D上路_01-2D太空射击游戏
- 计算机中数据的格式化,分享一个电脑格式化数据恢复方法-数据恢复百科
- Windows Azure 虚机密码忘记处理
- DirectX12(D3D12)基础教程(六)——多线程渲染
- echart 三维可视化地图_实测三个工具后,我终于找到了地图可视化的神器