InterSystems IRIS 全局中的事务

InterSystems IRIS 全局中的事务InterSystems IRIS DBMS 支持有趣的数据存储结构 - 全局变量。 本质上,这些是多级密钥,具有事务形式的各种附加功能、用于遍历数据树的快速函数、锁及其自己的 ObjectScript 语言。

在“全局变量是存储数据的宝剑”系列文章中阅读有关全局变量的更多信息:

树木。 第1部分
树木。 第2部分
稀疏数组。 第三部分

我开始对全局事务如何实现、有哪些功能感兴趣。 毕竟,这是与通常的表完全不同的存储数据结构。 级别低很多。

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

A - 原子性(原子性)。 交易中所做的所有更改或根本没有更改都会被记录。

C——一致性。 事务完成后,数据库的逻辑状态必须内部一致。 在许多方面,这一要求与程序员有关,但对于 SQL 数据库,它还与外键有关。

我——隔离。 并行运行的事务不应互相影响。

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

全局变量是非关系数据结构。 它们被设计为在非常有限的硬件上运行超快。 让我们看看全局事务中使用的实现 官方 IRIS docker 镜像.

为了支持 IRIS 中的事务,使用以下命令: 开始, TCOMMIT, 回滚.

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. 一致性

由于在基于全局变量的数据库中,键也是在全局变量上创建的(提醒一下,全局是比关系表更底层的数据存储结构),为了满足一致性要求,必须包含键的更改在同一事务中作为全局的更改。

例如,我们有一个全局 ^person,我们在其中存储个性并使用 TIN 作为密钥。

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

为了按姓氏和名字快速搜索,我们创建了 ^index 键。

^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个隔离级别:

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

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

读未提交 - 这是最低级别的隔离,但同时也是最快的。 事务可以读取彼此所做的更改。

读已提交 是下一个级别的隔离,这是一种妥协。 事务在提交之前无法读取彼此的更改,但可以读取提交之后所做的任何更改。

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

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

可序列化 — 最高级别的绝缘。 其特点是,一个事务中以任何方式使用的数据(读取或更改)只有在第一个事务完成后才可供其他事务使用。

首先我们要弄清楚事务中的操作与主线程是否存在隔离。 让我们打开 2 个终端窗口。

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 中的智能锁来做到这一点,它有很多不同的用途:您可以使用命令进行常规、增量、多重锁定 LOCK.

较低的隔离级别是为了提高数据库速度而设计的权衡。

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

该运算符不仅允许您获取更改数据所需的排它锁,还允许获取所谓的共享锁,当多个线程在读取过程中需要读取不应被其他进程更改的数据时,可以并行获取多个线程。

有关两阶段阻塞方法的更多信息(俄语和英语):

两相闭锁
两相锁定

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

使用锁,我们将创建可见性窗口,其中数据库的状态将保持一致。 所有对约定状态的可见性窗口的访问都将由锁控制。

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

独占锁用于数据更改——只有一个进程可以获取这样的锁。 独占锁可以通过以下方式获取:

  1. 如果数据免费,任何流程
  2. 只有对该数据具有共享锁并且第一个请求独占锁的进程。

InterSystems IRIS 全局中的事务

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

已提交读 ——这个级别的本质是我们只看到来自其他线程提交的数据。 如果另一个事务中的数据尚未提交,那么我们会看到它的旧版本。

这使我们能够并行化工作,而不是等待锁被释放。

如果没有特殊的技巧,我们将无法在 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)

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

LOCK +^person(123, amount)
изменение ^person(123, amount)
LOCK -^person(123, amount)

чтение ^person(123, amount)
LOCK -^person(123, amount)#”S”

当列出用逗号分隔的锁时,它们是按顺序获取的,但如果您这样做:

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

然后它们会被一次性全部原子化。

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

如果我们讨论在全局 ^person 中借记资金,那么只有 SERIALIZE 隔离级别是可接受的,因为资金必须严格按顺序花费,否则可能会多次花费相同的金额。

4. 耐用性

我使用硬切割容器进行了测试

docker kill my-iris

基地对他们的容忍度很好。 没有发现任何问题。

结论

对于全局变量,InterSystems IRIS 提供事务支持。 它们是真正原子的且可靠的。 为了确保基于全局的数据库的一致性,需要程序员的努力和事务的使用,因为它没有复杂的内置结构(例如外键)。

不使用锁的全局变量的隔离级别是READ UNCOMMITED,使用锁时可以保证到SERIALIZE级别。

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

来源: habr.com

添加评论