我刚刚完成了一个平凡的Pijul:Sanakirja数据库的跨进程锁。在这篇文章中,我解释了它是如何工作的。
Sanakirja本质上是文件中B树的写时复制集合。我们可以启动可变事务,这允许我们更改数据库,并且可以删除(此后数据库仍保持不变)或提交,以及不可变事务(仅允许我们读取数据库)。
有一些规则可以防止出现争用情况:一次只能启动一个可变事务,如果在最后一次提交之前仍启动了一个不可变事务,则可变事务就无法启动。换句话说,允许以下情形:在这些图中,绿色可变事务首先开始,而蓝色事务在绿色事务提交后开始。粉红色的不可变交易先于其他所有交易。即使在提交绿色事务之后,粉红色事务也可以继续,并且将像启动绿色事务之前一样读取数据库。
但是,在开始蓝色事务之前,我们必须等待粉红色事务的结束。
如下图所示,粉红色交易是在绿色交易开始之前还是之后开始都没有关系:
该系统的另一个显着特点是,甚至可以在两个可变交易之间启动另一个不变交易(在下图中以橙色绘制),与粉红色交易并行。在这种情况下,橙色和粉红色事务将读取数据库的不同版本。这是允许的,因为橙色事务在提交绿色事务之后开始。
B树中的节点大小为4096字节,这是大多数平台上的内存页大小(UltraSPARC是一个例外,只有8192字节页)。
在任何平台上,页面都是从原子上同步到磁盘的:即使较大的块可能仅部分同步,也可以保证页面原子同步。这就是为什么总是选择比平台的页面大小小的块大小总是安全的原因:UltraSPARC上的大页面同步速度会变慢,但是由于磁盘缓存的原因,同步仍然会自动发生。
在Sanakirja中,第一页包含指向(1)空闲页列表的指针,以及(2)最多500个指向B树的不同指针的集合。
在可变事务中,不修改现有页面:而是将它们复制到新分配的页面。就性能而言,这听起来很可怕,但实际上并没有那么糟糕,因为我们必须在访问程序之前将这些页面复制到内存中,并在修改它们和这些同步输入后将它们写回到磁盘中。 / output操作比复制页面大小的块要昂贵得多,即使在SSD上也是如此。
我之前写过有关Sanakirja的锁模型的信息,但这仅限于单个进程的范围,并且依靠文件锁来工作,因为使用Sanakirja的任何进程都必须首先获得排他文件锁,并且然后在进程内的线程之间使用正确的并发模型。
但是,Pijul有时需要的还不止这些:例如,@ cole-h在第43期中报道了pijul记录锁定了其他命令,如果您想在编辑更改时打开日志,这会带来不便。
当然,防止两个记录(或一个记录和一个提取)并行发生是基本的,但是完全排他锁显然太多了。
在上一次提交期间处于活动状态的事务计数器。这对于在上图中的粉红色和橙色事务之间进行区别很有用。
每个不可变事务都会在启动时记住时钟的值,并增加活动事务计数器的值。当它停止时,它会减少活动的不可变事务的计数器,并且如果它在上一次提交之前启动,那么还会减少在上一次提交期间活动的事务的计数器。
每次操作一次将所有这些值读取并更新到文件中,该文件将受到锁的保护。从原则上讲,这听起来可行,但是有一个陷阱:在最后一次提交之前启动的最后一个不可变事务的结尾,我们需要一种机制来向潜在的可变事务发出可以启动的信号。在线程之间,这可以通过条件变量来实现。在进程之间,这更加复杂:我们可以使用Unix信号,但是它们的处理程序可以中断任何正在运行的函数,这意味着这些处理程序不能使用任何互斥量,也不能与Tokio进行通信,这限制了它们的实用性。
我最终实现的另一个选择是在外部过程中编写锁定模型的准系统实现,使用Tokio任务对事务进行建模,并使用Unix域套接字与该Tokio运行时进行通信。每次事务启动时,它都会尝试在指定位置连接到Unix域套接字。如果失败,将派生一个新进程,以在后台运行pijul lock hidden命令(使用std :: process :: Command :: spawn),这将打开Unix域套接字并在其标准输出中打印新行。父进程等待该换行符,然后连接到套接字。然后pijul lock命令的工作方式如下:
在UDS上实现的协议具有三个命令(启动不可变事务,启动可变事务和提交)和两个答案(称为ack和locked)。所有这些消息都编码在一个字节上。
当最后一个客户端与套接字断开连接时,pijul锁将等待一秒钟,检查是否还有其他客户端,如果没有,则退出。此延迟避免了在脚本中使用pijul时反复启动pijul锁定实例。
可以在Nest上看到该新命令的源代码,在Nest上也可以找到这两种事务的源代码。
首先,这目前仅在Unix平台上有效,但似乎Windows上的UDS可能很快将在Tokio中实现。 Windows上的Pijul仍然依赖于标准文件锁。
而且,这是在Pijul中实现的,而不是在Libpijul中实现的,因为从长远来看Libpijul并不依赖于Tokio(该依赖关系将在1.0的beta之前删除)。 最后要注意的是文件锁是建议性的,外部过程也是如此。 这意味着只要写入数据库的所有进程都知道它们,它们就可以安全使用。 但是,就像文件系统中所有文件的情况一样,拒绝合作并具有文件访问权限的进程将始终能够破坏该文件。