以下翻译自lock0lock.c的文件头部注释,翻译的比较凌乱…
////////////////////////////////////////////////////////////////////////////////////
几个hardcode的宏:
LOCK_MAX_N_STEPS_IN_DEADLOCK_CHECK |
1000000 |
该宏用于控制在事务的waits-for-graph中的查找深度 |
LOCK_MAX_DEPTH_IN_DEADLOCK_CHECK |
200 |
用于控制死锁递归检测深度 |
LOCK_RELEASE_KERNEL_INTERVAL |
1000 |
在释放锁的过程中(lock_release_off_kernel),每隔这么多次循环释放kernel mutex |
LOCK_PAGE_BITMAP_MARGIN |
64 |
创建锁的bitmap至少有这么多个比特位 |
一个显式锁同时影响记录和记录前面的gap。一个隐式x锁不影响gap,只会锁住读活更新的索引记录。
如果一个事务修改或插入了一个索引记录,它拥有在该就上的隐式x锁。如果事务修改了聚集索引记录,会在二级索引记录上也有一个隐式x锁,二级索引记录所在的page上的事务id>=当前事务的trx id, 并且没有二级索引记录上的显式 非gap锁请求
这种对二级索引的相容性定义是根据其实现决定的:我们希望通过查看聚集索引记录来判断一个二级索引记录是否有一个隐式x锁,而不是根据二级索引记录的历史版本。
不同的事务可能同时在同一个GAP上产生冲突锁。在gap上的锁被单纯的禁止:无法插入,或者如果一个不同的事务在gap上有一个冲突的锁,一个select cutsor可能需要等待。GAP上的x锁并不意味着有权力向GAP中插入记录。
一个显式锁可以设置在一个用户记录或者supremum记录上。在supremum记录上的锁通常被认为是一个GAP锁,尽管GAP位没有设置。当我们修改一条记录并且修改了记录的大小时,可能会临时的将它的显式锁存储到infimum记录上,除此之外,不会在infimum记录上加锁。
一个正在等待的记录锁也可以是GAP类型;当在显式锁队列中,一个锁请求的前面没有另外一个冲突模式锁对象时,就可以获得锁。
RULE1:如果在记录上有一个隐式X锁,并且有一个non-gap锁请求在队列中等待,这时候持有隐式x锁的事务同样也会有一个显式的Non-gap记录X锁。这样当锁被释放时,我们只需要通过查看队列中的显式锁请求来为等待锁请求赋予锁
RULE3:不同的事务不可以在同一个记录上同时拥有冲突的NON-GAP锁。但是,他们可以拥有冲突的GAP锁。
RULE4:如果在队列中有一个等待的锁请求。没有锁请求,gap或者not-gap锁可以插入到队列中该等待锁请求前面。在记录删除和page分裂时,数据库管理者可以为一个事务创建新的gap类型的锁。如果没有RULE4,事务waits-for graph可能变成循环而数据库无法注意到这一点,因为死锁检测只有在一个事务本身请求一个锁时才发生。
当在下一个记录上没有其他事务持有的显式锁时,允许在GAP中插入记录。这些锁请求被赋予或等待,gap位是否被设置都无所谓,除了另外一个事务的gap类型请求,并且正在等待轮到他去插入时,会被忽略掉。换句话说,一个其他事务持有的隐式X锁不会阻止一次写入,这允许在是用类似ORACLE序列生成器为许多事务并发产生主键,这会允许更大的并发度。
如果事务在记录上有X锁,或者其他事务没有在该记录上任何non-gap锁时,则允许该事务修改记录
如果事务有一个Non-gap显式、或者隐式锁在记录上,或者其他事务有在该记录上的非X锁请求,则允许是用一个cursor读取一个用户记录。在一个page上supremum总是允许被读。
总的来说,一个隐式锁看起来仅仅像是在一个记录上被赋予的X锁,而不是在GAP上。一个没有设置GAP位的显示锁是一个同时在记录和GAP上的锁。如果设置了GAP位,则表明锁只在GAP上。不同的事务不可以在同一时间拥有相互冲突的锁。但可以在GAP上拥有冲突的锁。被赋予的记录锁允许操作记录的权限,但gap类型的锁仅仅禁止操作。
注意:如果一些事务在一个二级索引记录上拥有隐式x锁,这种情况比较难处理。我们可能需要查看对应聚集索引记录的前面的版本,以查找一个被标记删除的二级索引记录是否是被一个活跃的事务标记删除的,而不是一个已经提交的事务。
FACT A:如果一个事务已经插入了一条记录,它可以在任何时候删除它,无需等待任何锁。
PROOF:事务拥有插入记录在每个索引记录上的隐式X锁,并且可以修改每个索引记录而无需等待。
FACTB:如果一个事务通过一个cursor产生结果集,它可以再次读取,并获取一些结果集合,如果它没有同事修改他们的话。因此没有幻读问题。如果被cursor访问到的按照字母序的最大记录被移除,一次锁等待可能会发生,其他情况则不会。
PROOF:当一次读cursor执行时,它会对扫描过的用户记录设置S锁,以及每个page supremum上一个gap类型的S锁。cursor必须等待直到它拥有了这些锁。这样其他事务就不可以在这些用户记录上加任何X锁,也不能修改用户记录。甚至其他事务不可以插入到cursor扫奥的GAP中。Page分裂和合并,或者移除废弃版本的记录不会受此影响,因为当一个用户记录或一个Page上的supremum被移除时,下一个记录继承 了其锁作为GAP类型,因此会阻塞插入到相同的GAP中。同样的,如果一个page supremum被插入,它从成功的记录那继承它的锁。当cursor被重新放置到结果集的起始位置时,它将查看到的记录是上次查看到的或者新插入的page supremums.它能够立刻获取这些记录,当到达最大的记录时,它注意到结果集已经完成。如果最大的记录被移除,可能需要等待锁,因此下一个记录只继承了GAP类型的锁,这时候需要等待。
如果一个索引记录可以被修改或者新的插入,我们必须检查该记录和下一个记录上的锁。当一个read cusor开始读数据时,我们会在每个经过的记录上设置一个记录级别的s锁,除了在开始获取记录之前cursor所在的初始化记录。我们的索引树查找约定B-TREE上的cursor在查询时被防止在第一个可能匹配的记录前面,这里可能会有一些优化:如果记录是通过唯一索引上的等值条件查找的,我们实际上设置了一个特殊的锁在记录上,这个锁不会阻止任何在该记录之前的插入。而一个记录上的next-key x Lock同样会阻止该记录之前的插入。
每个Page上有特殊的infimum和supremum记录。一个supremum记录可以被一个read curosr锁住。这个记录无法被更新,但这个锁可以阻止将用户记录插入到该Page的用户记录尾部(也就是最大的那个记录之后)
next-key锁也可以防止幻读,防止两次select的结果集不同,防止幻读保证了事务的串行化。
当需要插入一条新记录时,我们需要检查什么呢?只需要检查同一个Page上的下一条记录,因为supremum记录也会持有一个锁。一个S锁会阻止插入,但一个X锁呢?如果是由一个查询更新(searched update)加上的锁,这时候同样有一个隐式的s锁,插入会被阻止。但如果我们的事务拥有下一个记录的X锁,但在下一个记录上有一个等待的S锁请求呢?如果这个S锁是被一个在索引上升序移动的read cursor锁设置的,我们无法立刻做插入操作,因为当我们最终提交事务时,read cursor应该可以看到新插入的记录。所以我们应该跳到新插入记录的下一个记录。这种向后移动可能太难处理。如果我们正处于将在下一个记录上第二个x锁请求入队列的过程中,这时候死锁检测机制会发现我们的事务和其他持有s锁请求事务之间的死锁。这种解决方案看起来是Ok的。
我们可以约定赋予的显式记录锁,锁住对应的记录以避免修改,同时锁住记录前面的gap以避免插入。隐式记录x锁,根据聚集索引记录推导而来,只锁住记录本身,避免其被修改。而不会锁住记录前面GAP中的插入。
我们如何存储更新锁呢?如果查找是通过唯一键来完成的,我们仅仅修改对应记录的事务id。否则,我们可能在记录上放置一个x锁。如果update修改了聚集索引记录的顺序,新插入的新记录无需记录锁,事务Id已经足够了。对于一个二级索引记录而言同样如此。查找删除(searched delete)和update类似。
PROBLEM:
等待锁请求会怎么样呢?如果一个事务正在等待去更新其他事务已经修改的记录,其他事务如何发送一个end-lock-wait信号给他呢?如果我们约定一个事务每次只会等一个锁,在锁等待结束时如何进行保护呢?
PROBLEM:
检查一个二级索引记录的事务id。在做修改而非插入的时候,这是必要的吗?一个二级索引记录通过设置或充值删除标记来进行修改。一个二级索引记录包含了唯一标示对应聚集索引记录的列。因此一个二级索引记录被修改,同时聚集索引记录也被修改,在我们修改二级索引记录之前,在聚集索引记录上已经检查了 事务Id。所以在对一个二级索引设置删除标记位这样的操作,我们无需关系事务Id,只需要关心锁表中必须检查的锁。在从二级索引查询记录的情况,事务id是有用的,这种情况下我们还需要回查聚集索引记录。
PROBLEM:
在Page分裂、合并或者记录被删除/更新时。如何更新记录锁呢?如果一个记录的大小发生了变化,我们通过先删除再插入的方式来执行UPDATE。我们如何保持记录上的锁设置或锁等待呢。因为一个记录锁是通过记录的heap no在锁对象的bitmap中标记的,当我们从记录链表中移除记录时。可能仍然需要保持对应的锁Bit位。如果Page被重新组织了,我们可以生产一个用新记录和旧记录的heap no表,然后交换在锁对象中对应的bit。我们可以向表中增加一行来告诉更新的记录在哪里结束。如果更新无需重组织Page,我们就可以简单的把更新记录的锁bit转移到新的新的heap no上。
一种更复杂的情况是,重插入的更新记录是一种悲观操作,因为索引树的结构可能已经发生了变化。
PROBLEM:
如果一个supremum记录在一次page合并或者Purge被移除时,正在等待的锁请求该做什么呢?当把记录分裂到右边时,我们仅仅将锁请求移动到新的supremum上。如果一个记录被移除,我们可以将等待锁请求转移到它的继承者身上,也就是索引上的下一个记录。但是下一个记录本身可能在它的队列里已经有了锁请求。这时候会有新的死锁检测。可能释放等待的事务会更简单,他们会随后将锁请求设置到更合适的记录上,并加入到队列中。
PROBLEM:
当插入一个记录时,它应该从它上面的邻居继承什么样的锁呢?插入一个新的supremum记录,分裂总是可能发生的,但如果插入一个新的用户记录要求其上面的邻居没有其他任何事务的锁请求。解决办法:我们可以将锁作为GAP类型拷贝,这样所有等待锁被转换为已经赋予的在插入记录上的GAP类型锁。