InterSystems IRIS全球交易

InterSystems IRIS和交易 InterSystems IRIS DBMS支持奇怪的数据存储结构-全局变量。 实际上,这些是多级密钥,具有事务处理,遍历数据树的快速功能,锁和它们自己的ObjectScript语言等各种形式的其他优点。

在“ Globals-数据存储的剑匠”系列文章中,有关globals的更多信息:

树木。 第一部分
树木。 第二部分
稀疏数组。 第三部分

对于我来说,如何在全球范围内实施交易,具有哪些功能变得很有趣。 毕竟,这是用于存储数据的结构与通常的表完全不同的结构。 低得多的水平。

从关系数据库理论可以知道,良好的事务实现必须满足ACID要求:

A-原子(原子性)。 记录对交易所做的所有更改或根本不进行任何更改。

C-一致性。 事务完成后,数据库的逻辑状态必须在内部保持一致。 在许多方面,此要求都适用于程序员,但对于SQL数据库,它也适用于外键。

我-隔离(隔离)。 并行事务不应相互影响。

D-耐用。 事务成功完成后,较低级别的问题(例如,电源故障)不应影响事务更改的数据。

全局变量是非关系数据结构。 它们是为在非常有限的硬件上进行超快速工作而创建的。 让我们了解使用官方IRIS docker image在全局变量中的实现。

为了支持IRIS中的事务,使用了以下命令: TSTARTTCOMMITTROLLBACK

1.原子性


检查原子性的最简单方法。 从数据库控制台检查。

Kill ^a TSTART Set ^a(1) = 1 Set ^a(2) = 2 Set ^a(3) = 3 TCOMMIT 

然后我们得出结论:

 Write ^a(1), “ ”, ^a(2), “ ”, ^a(3) 

我们得到:

 1 2 3 

一切都很好。 观察到原子性:记录所有变化。

我们使任务复杂化,引入错误并查看如何部分或完全不保存交易。

让我们再次检查原子性:

 Kill ^A TSTART Set ^a(1) = 1 Set ^a(2) = 2 Set ^a(3) = 3 

然后强行停止容器,开始观察。

 docker kill my-iris 

该命令几乎等同于强制关闭电源,因为它会发送信号立即停止SIGKILL进程。

也许交易被部分保存了?

 WRITE ^a(1), ^a(2), ^a(3) ^ <UNDEFINED> ^a(1) 

-不,未保存。

测试回滚命令:

 Kill ^A TSTART Set ^a(1) = 1 Set ^a(2) = 2 Set ^a(3) = 3 TROLLBACK WRITE ^a(1), ^a(2), ^a(3) ^ <UNDEFINED> ^a(1) 

也没有保留任何东西。

2.一致性


因为在全局变量数据库中,密钥也是在全局变量上创建的(我记得全局变量是用于存储数据的低级结构,而不是关系表),为了满足一致性要求,必须在与全局更改相同的事务中包含密钥更改。

例如,我们有一个全球^人,其中存储了个性,我们使用TIN作为密钥。

 ^person(1234567, 'firstname') = 'Sergey' ^person(1234567, 'lastname') = 'Kamenev' ^person(1234567, 'phone') = '+74995555555 ... 

为了快速搜索姓氏和名字,我们制作了键^索引。

 ^index('Kamenev', 'Sergey', 1234567) = 1 

为了达成共识,我们必须添加以下个性:

 TSTART ^person(1234567, 'firstname') = 'Sergey' ^person(1234567, 'lastname') = 'Kamenev' ^person(1234567, 'phone') = '+74995555555 ^index('Kamenev', 'Sergey', 1234567) = 1 TCOMMIT 

因此,在删除时,我们还应使用以下事务:

 TSTART Kill ^person(1234567) ZKill ^index('Kamenev', 'Sergey', 1234567) TCOMMIT 

换句话说,一致性要求的实现完全取决于程序员。 但是当涉及到全局变量时,这是很正常的,因为它们的层次很低。

3.隔离


这是荒野开始的地方。 许多用户同时在同一个数据库上工作,修改相同的数据。

这种情况与许多用户使用代码同时使用同一存储库并尝试一次将更改提交到其中的情况可比。

数据库应实时解决此问题。 考虑到在严肃的公司中,甚至还有一个专门的人负责版本控制(用于合并分支,解决冲突等),数据库应该实时完成所有这些工作,任务的复杂性以及数据库和数据库的正确设计。服务它的代码。

数据库无法理解用户执行的操作的含义,以防止用户在相同数据上工作时发生冲突。 它只能取消与另一笔交易相反的一笔交易,或按顺序执行它们。

另一个问题是,在事务执行期间(在提交之前),数据库的状态可能不一致,因此希望其他事务无法访问数据库的不一致状态,这可以通过关系数据库中的许多方式实现:创建快照,多版本行和等

在并行执行事务中,对我们而言重要的是它们之间不会相互干扰。 这是隔离的属性。

SQL定义了4个隔离级别:

  • 读未提交
  • 读已提交
  • 可重复读取
  • 可序列化

让我们分别考虑每个级别。 实施每个级别的成本几乎呈指数增长。

READ UNCOMMITTED是最低的隔离级别,但最快的隔离级别。 事务可以读取彼此所做的更改。

READ COMMITTED是下一个隔离级别,这是一个折衷方案。 事务不能读取彼此在提交之前所做的更改,但是可以读取在提交之后进行的任何更改。

如果我们有一个长事务T1,在此期间事务T2,T3 ... Tn中的提交与T1使用相同的数据,那么当我们在T1中请求数据时,每次都会得到不同的结果。 这种现象称为不可重复阅读。

可重复读取 -在此隔离级别中,我们没有不可重复读取的现象,因为对于每个读取数据的请求,都会创建结果数据的快照,并且在同一事务中重用时会使用快照中的数据。 但是,在此隔离级别,可以读取幻像数据。 这是指读取并发已提交事务添加的新行。

SERIALIZABLE是最高级别的隔离。 其特征在于,在事务中以任何方式使用(读取或更改)的数据仅在完成第一个事务之后才可供其他事务使用。

首先,让我们找出事务中的操作是否与主线程隔离。 打开两个终端窗口。
 Kill ^t Write ^t(1) 2 
 TSTART Set ^t(1)=2 

没有隔离。 一个线程看到打开事务的第二个线程做什么。

让我们看看不同流的事务是否看到它们内部正在发生什么。

我们打开2个终端窗口并并行打开2个事务。
 kill ^t TSTART Write ^t(1) 3 
 TSTART Set ^t(1)=3 

并发事务查看彼此的数据。 因此,我们获得了最简单但最快的隔离级别READ UNCOMMITED。

原则上,这对于全球人来说是可以预期的,对于他们而言,速度始终是至关重要的。

但是,如果我们在全球运营中需要更高级别的隔离,该怎么办?

在这里,您需要考虑为什么需要绝缘水平以及它们如何工作。

SERIALIZE的最高隔离级别意味着并发执行的事务的结果等同于它们的顺序执行,从而保证了不存在冲突。

我们可以借助ObjectScript中有效的锁来完成此操作,ObjectScript具有许多不同的应用方式:您可以使用LOCK命令来进行常规的,增量的,多个锁。

较低的隔离级别是折衷方案,旨在提高数据库的速度。

让我们看看如何使用锁实现不同级别的隔离。

使用此运算符,您不仅可以获取更改数据所需的排他锁,还可以获取所谓的共享锁,即共享线程,当它们需要读取在读取过程中不应被其他进程更改的数据时,它们可以一次占用多个线程。

有关俄语和英语两阶段锁定方法的更多信息:

两相锁
两相锁定

困难在于,在事务处理期间,数据库的状态可能会不一致,但是,此不一致的数据对于其他进程是可见的。 如何避免这种情况?

使用锁,我们将在可视状态窗口中建立一致的数据库状态。 并且所有对此类达成共识状态的可见窗口的调用都将由锁控制。

相同数据的共享锁可重复使用-多个进程可以使用它们。 这些锁可防止其他进程更改数据,即 它们用于形成数据库状态一致的窗口。

排他锁用于修改数据-只有一个进程可以使用这种锁。 排他性封锁可以采取:

  1. 数据免费的任何过程
  2. 仅对此数据具有共享锁的进程,而第一个进程请求排他锁。



可见性窗口越窄,等待其他进程花费的时间越长,但是数据库中的状态可以越一致。

READ_COMMITED-此级别的本质是,我们仅看到来自其他已锁定流的数据。 如果尚未提交另一个事务中的数据,那么我们将看到其旧版本。

这使我们可以并行化工作,而不必等待锁定被释放。

没有特殊的技巧,我们将无法在IRIS中看到数据的旧版本,因此我们必须使用锁。

因此,我们将必须使用共享锁以仅在一致性时才允许读取数据。

假设我们有一个用户基础^人,他们可以互相转移资金。

从人员123转移至人员242的时刻:

 LOCK +^person(123), +^person(242) Set ^person(123, amount) = ^person(123, amount) - amount Set ^person(242, amount) = ^person(242, amount) + amount LOCK -^person(123), -^person(242) 

在借记之前向个人123请求金额的那一刻必须伴随一个排他锁(默认情况下):

 LOCK +^person(123) Write ^person(123) 

而且,如果您需要在帐户中显示帐户状态,则可以使用共享锁或根本不使用共享锁:

 LOCK +^person(123)#”S” Write ^person(123) 

但是,如果我们假设数据库操作几乎是立即执行的(我记得全局变量是比关系表低得多的级别结构),那么对该级别的需求就会下降。

可重复读取 -在此隔离级别中, 假定可以通过并发事务修改数据的多次读取。

因此,我们将必须在读取正在更改的数据时设置共享锁,并在正在更改的数据上设置互斥锁。

幸运的是,LOCK运算符允许一个运算符详细列出所有必要的锁,该锁可以是很多。

 LOCK +^person(123, amount)#”S”  ^person(123, amount) 

其他操作(此时,并行线程尝试更改^人(123,数量),但不能)

 LOCK +^person(123, amount)  ^person(123, amount) LOCK -^person(123, amount)  ^person(123, amount) LOCK -^person(123, amount)#”S” 

列出以逗号分隔的锁时,它们是按顺序使用的,如果这样做,则:

 LOCK +(^person(123),^person(242)) 

然后就将它们原子化。

SERIALIZE-我们将必须设置锁,以便最终所有具有公共数据的事务都将顺序执行。 对于这种方法,大多数锁必须是独占的,并且必须带到全局的最小区域以提高性能。

如果我们谈论全球^人的注销,那么他只能接受隔离级别SERIALIZE,因为必须严格顺序地花钱,否则有可能花相同的钱数倍。

4.耐久性


我通过硬切容器进行了测试

 docker kill my-iris 

基地很好地容忍了他们。 没有发现问题。

结论


对于全球客户,InterSystems IRIS具有交易支持。 它们确实是原子的,可靠的。 为确保数据库在全局变量上的一致性,由于没有复杂的内置结构(例如外键),因此程序员的工作和事务的使用是必要的。

不使用锁的全局隔离级别为READ UNCOMMITED,使用锁时,可以确保达到SERIALIZE级别。

全局变量上事务的正确性和速度在很大程度上取决于程序员的技能:读取时使用的共享锁越广泛,隔离级别越高,采用的排他锁越多,速度就越高。

Source: https://habr.com/ru/post/zh-CN461753/


All Articles