В случае конструкции
BEGIN TRAN
UPDATE 1
UPDATE 2 --error
UPDATE 3
COMMIT TRAN
(при выполнении команд не в блоке TRY ... CATCH ...) если на втором UPDATE возникнет ошибка, исполнение команд может продолжиться и дойти до COMMIT.
Использование варианта
BEGIN TRAN
UPDATE 1
UPDATE 2
UPDATE 3
IF @@ERROR <> 0
ROLLBACK TRAN
ELSE
COMMIT TRAN
не будет правильным, т.к. глобальная переменная @@ERROR содержит номер ошибки для последней исполненной команды. Это означает следующее: если UPDATE 1 или UPDATE 2 завершится с обшибкой, а UPDATE 3 — без ошибки, то после UPDATE 3 значение переменной @@ERROR станет равно 0, что сделает ложным вывод об успешности всей транзакции.
Если нужно при ошибке делать откат, то тут могут быть два варианта.
Первый — это исполнение команд в блоке TRY ... CATCH ...
BEGIN TRY
BEGIN TRAN
UPDATE 1
UPDATE 2 --error
UPDATE 3
COMMIT TRAN
END TRY
BEGIN CATCH
ROLLBACK TRAN
END CATCH
В этом случае, при возникновении ошибки на 2м шаге, исполнение не продолжится, а по-возможности перейдёт в блок CATCH, где можно принудительно вызвать ROLLBACK.
Второй вариант — включение опции XACT_ABORT перед входом в транзакцию.
SET XACT_ABORT ON
BEGIN TRAN
UPDATE 1
UPDATE 2 --error
UPDATE 3
COMMIT TRAN
В этом случае при возникновении ошибок (определённого рода, не любых) на 2-м UPDATE выполнение команд будет прервано и произойдёт автоматический откат изменений. (Может быть даже не стоит считать этот вариант самостоятельным, на мой взгляд включение XACT_ABORT — это некое дополнительное средство; не припомню случая, когда бы я для отката транзакции пользовался этой опцией обособленно).
В некоторых случаях используется и то и другое.
Ниже чуть подробнее об автоматическом и управляемом откате.
Автоматический ROLLBACK
ROLLBACK может происходить автоматически, при закрытии соединения, если для данного соединения есть незавершенные транзакции. Т.е. создали, например, таблицу
create table test (id int primary key);
Открываем соединение и в нём выполняем
begin tran;
insert into test (id) values (1);
select * from test;
закрываем соединение, не завершив транзакцию (ни COMMIT, ни ROLLBACK не сделали). SqlServer сделает откат такой транзакции, при разрыве соединения. Запросив затем данные из таблицы в новом соединении, мы увидим, что она пустая.
Также автоматический ROLLBACK может происходить при возникновении ошибок (таких как, например, нарушение PK, FK ограничений при вставке или удалении данных), если включена опция XACT_ABORT (по умолчанию OFF). Например:
set xact_abort on;
begin tran;
insert into test (id) values (2);
select * from test;
insert into test (id) values (2); --error: Violation of PK ...
select * from test;
commit tran;
в этом случае до второго select и до commit дело не дойдёт, и откат произойдёт автоматически. Теперь при выключенном xact_abort (то что по умолчанию):
set xact_abort off;
begin tran;
insert into test (id) values (3);
insert into test (id) values (3); -- error
insert into test (id) values (4);
commit tran;
select * from test;
Несмотря на ошибку дело дойдёт и до commit (соответственно отката не будет) и до select после него.
К сожалению опция set xact_abort on полезна далеко не всегда. В частности она не откатывает транзакцию при генерации пользовательских исключений (в том числе сгенерированных в DML-триггерах). Например:
set xact_abort on;
begin tran;
insert into test (id) values (5);
if not exists (select 1 from test where id = 0)
raiserror('Bad data', 16, 1);
commit tran;
select * from test;
Несмотря на set xact_abort on и сгенерированное исключение дело дойдёт и до commit и до select после него. Поэтому полезнее может быть целенаправленный вызов rollback.
-- вернули опцию в состояние по-умолчанию, если она была оставлена в состоянии ON
set xact_abort off;
Управляемый ROLLBACK
Часто применяется в catch блоке, при оборачивании транзакции в try ... catch ... конструкцию:
begin try
begin tran;
-- тут делаем что-то
commit tran;
end try
begin catch
rollback tran;
end catch
При xact_abort off (т.е. по умолчанию) ROLLBACK не происходит автоматически, если транзакция была открыта, но из-за ошибки не достигла COMMIT. В этом случае SqlServer позволяет программисту самому решить будет ли откат полезен при той или иной ошибке, или нет. Далее пара примеров, когда откат может быть полезен в catch и когда вреден.
Пример 1: Изменение данных в транзакции.
Пусть есть процедура, которая в транзакции делает вставку данных в две связанных таблицы:
create procedure dbo.SetUserInfo
(
@uid uniqueidentifier = NULL,
@info xml
)
as
begin try
set nocount, xact_abort on;
if @info is NULL or @info.exist('/User') = 0
begin
raiserror('No or bad data provided.', 16, 1);
return;
end;
begin transaction;
declare @inserted table (ID int not NULL);
declare @id int;
merge into dbo.Users t
using(
select
@uid,
@info.value('(/User/@FirstName)[1]', 'nvarchar(50)'),
@info.value('(/User/@LastName)[1]', 'nvarchar(50)')
) s(UID, FirstName, LastName)
on t.UID = s.UID
when matched then
update
set t.FirstName = s.FirstName, t.LastName = s.LastName
when not matched then
insert (UID, FirstName, LastName)
values (s.UID, s.FirstName, s.LastName)
output inserted.ID into @inserted (ID)
;
select @id = ID from @inserted;
merge into dbo.UserContacts t
using (
select @id, ct.ID, x.c.value('@Value', 'nvarchar(400)')
from @info.nodes('/User[1]/Contacts[1]/Contact') x(c)
join dbo.UserContactTypes ct on ct.Type = x.c.value('@Type', 'nvarchar(400)')
) s (UserID, ContactTypeID, ContactInfo)
on t.UserID = s.UserID and t.ContactTypeID = s.ContactTypeID
when not matched by source then
delete
when matched then
update
set t.ContactInfo = s.ContactInfo
when not matched then
insert (UserID, ContactTypeID, ContactInfo)
values (s.UserID, s.ContactTypeID, s.ContactInfo)
;
commit transaction;
end try
begin catch
declare
@errMsg nvarchar(4000) = error_message(),
@errLine int = error_line(),
@procName sysname = quotename(object_schema_name(@@procid)) + '.' + quotename(object_name(@@procid))
;
if @@trancount > 0
rollback transaction;
raiserror('%s in %s at %d', 16, 1, @errMsg, @procName, @errLine);
end catch
GO
Допустим теперь, что произошёл вызов процедуры и началась вставка данных. Предположим, что вставка в Users прошла успешно, а при вставке в UserContacts произошел конфликт с уникальным индексом (UserID, ContactTypeID) (из-за того, например, что в @info один и тот же <Contact Type="Phone" Value="0(000)000-00-00" /> затесался дважды).
Если логикой приложения продиктовано, что либо сущность вставляется целиком, либо вообще не вставляется — тогда в catch делается rollback (как в данном примере).
Но возможны ситуации, когда ошибки, возникшие в результате выполнения каких-то отдельных запросов, не являются серьёзным основанием для отката всех совершенных действий. Например, если у нас не две связанных таблицы, а импорт данных в несколько независимых таблиц, и мы не хотим откатывать ту часть данных, что была уже успешно внесена. Тогда в catch можно попытаться сделать commit (не любая ошибка сделает это возможным, о том как это сделать корректно — в следующем примере).
Т.е. rollback не обязан происходить при возникновении любой ошибки. Делать откат, или нет — зависит от семантики данных и логики приложения.
Пример 2: Чтение данных в транзакции.
Транзакции для изменения данных достаточно привычны, но иногда в транзакции нуждается и чтение. Для таких транзакций необдуманно вызванный rollback может оказать медвежью услугу.
Пусть есть процедура, которая в repeatable read или snapshot транзакции читает данные:
create procedure dbo.GetSalesData
(
@dateFrom datetime,
@dateTo datetime
)
as
begin try
set nocount on;
declare @userID int;
select @userID = UserID from #Session;
if @userID is NULL
begin
raiserror('Access denied.', 16, 1);
return;
end;
create table #Orders (OrderID int not NULL);
alter table #Orders add primary key (OrderID);
set transaction isolation level snapshot;
begin transaction;
insert into #Orders (OrderID)
select op.OrderID
from dbo.OrderPermissions(@userID) op
join dbo.Orders ord on ord.ID = op.OrderID
where op.[Permissions] > 0
and ord.[Date] >= @dateFrom and ord.[Date] < @dateTo
-- some check based on #Order and other data
if exists (select 1 from #Orders o join ... where ...)
begin
raiserror('Check fail.', 16, 1);
return;
end;
select ...
from dbo.Orders ord
join #Orders o on o.OrderID = ord.ID
select ...
from dbo.Invoices inv
join #Orders o on o.OrderID = inv.OrderID
select ...
from dbo.Shipment sh
join #Orders o on o.OrderID = sh.OrderID
commit transaction;
end try
begin catch
declare
@errMsg nvarchar(4000) = error_message(),
@errLine int = error_line(),
@procName sysname = quotename(object_schema_name(@@procid)) + '.' + quotename(object_name(@@procid))
;
if xact_state() = 1
commit transaction;
else if xact_state() = -1
rollback transaction;
raiserror('%s in %s at %d', 16, 1, @errMsg, @procName, @errLine);
end catch
GO
В процедуре происходит следующее. Открывается транзакция. В ней заполняется фильтрующая таблица #Orders (чтобы потом дать пользователю только то, что ему разрешено видеть). Затем некоторая проверка на основании #Orders и других данных. Если проверка проходит, то данные отдаются, если нет — генерируется ошибка.
Предположим, что эта проверка не была успешной. Открыта транзакция и произошла ошибка raiserror('Check fail.', 16, 1), вследствие чего управление передаётся в catch. Должен ли в catch произойти rollback? Нет. Ведь мы только читаем данные и ничего не изменяем (кроме временной таблицы #Orders). Более того, таблица #Orders создана до входа в транзакцию, а заполнялась в транзакции. Вследствие чего, если бы мы стали делать rollback начал бы происходить откат вставленных в неё данных, а это дольше, чем commit и простое уничтожение #Orders при выходе из процедуры. Т.е. в данном случае в catch лучше попытаться сделать commit, возможность или невозможность которого определяется функцией XACT_STATE().
ЗафиксироватьТранзакцию() если в транзакции произошло исключениеØ |
Я |
04.11.05 — 15:21
Есть обработка, которая создает новые документы
При записи документа происходит исключение
Попытка
Док.Записать();
Исключение КонецПопытки;
Обработка происходит в транзакции. Если возникает исключение, например «номер не уникальный», то транзакция не фиксируется.
«Ошибка при выполнении метода ЗафиксироватьТранзакцию()»
Как победить?
1 — 04.11.05 — 15:25
Ну и где логика? Все документы записываются же у тебя в транзакции — а это есть нечто целостное… можно самому сообщать и не фиксировать
2 — 04.11.05 — 15:26
Логика тут, что если документ не записался, то и хрен с ним, а вот остальные хотелось бы записать. Если бы я хотел остановится, то я в конструкции Исключение КонецПопытки написал бы соответствующий код.
3 — 04.11.05 — 15:28
Перед записью документы проверяй сам номер на уникальность, если не уникален, пропускай Док.Записать().
4 — 04.11.05 — 15:29
(3)
Такая мысль приходила мне в голову, но может есть другое решение?
Я так понимаю, что есть один способ проверки на уникальность. Попробовать найти документ с таким номером?
5 — 04.11.05 — 15:31
Так а если один пропустится — задача будет упешно выполнена? Иначе какой смысл транзакции кроме ускорения? Если что — используй нумератор
6 — 04.11.05 — 15:34
Для данной задачи можно пропустить документы, которые не удалось записать. Т.е. мысле кроме как выполнять поиск по номеру никаких?
7 — 04.11.05 — 15:35
(4) Чем тебя ЭТО решение не устраивает???
8 — 04.11.05 — 15:36
Хотелось бы получить нечто более универсальное. Меня вообще интересует, как продолжить транзакцию, если в ней произошла ошибка при записи.
9 — 04.11.05 — 15:37
Другая мысль:
…
ЗафиксироватьТранзакцию();
Док.Записать();
НачатьТранзакцию();
…
10 — 04.11.05 — 15:39
Не годится. Для данной задачи нужно, чтобы выполнение той же транзакции продолжалось. Хотя и такая мысль приходила мне в голову.
11 — 04.11.05 — 15:43
(8) Универсальное в чем??
Транзакция подразумевает под собой то, что в данном участке программы ошибок не было…
12 — 04.11.05 — 15:45
(10) Объясни для чего у тебя используются транзакции.
13 — 04.11.05 — 15:50
короче невозможно.
В данном случае есть обработка для иморта документов через ОЛЕ. Переписывать ее неохота. Происходит следующее.
Импортируется договор, в договоре есть контрагент, у контрагента есть поле БазДоговор, которое ссылается на импортируемый договор. В результате договор начинает затаскиваться, он еще не записан. Затаскивается контрагент, у контрагента есть поле Договор, затаскивается еще раз договор (он ведь не записан), он записывается, рекурсивный алгоритм возвращается к тому же договору и пытается записать его еще раз. Возникает исключение.
И вообще мне стало интересно, можно ли продолжить транзакцию, если чего-то не записалось.
14 — 04.11.05 — 15:58
(13) Я просил объяснить для чего используются транзакции, а не что происходит в обработке, которую неохота переписывать…
15 — 04.11.05 — 16:06
А я спрашивал о принципиальной возможности продолжения выполнения программы с фиксицией транзакции при возникновении ошибки записи в конструкции Попытка КонецПопытки;
Я понимаю зачем нужны транзакции и что в принципе логично, чтобы транзакция не фиксировалась. Вот только не люблю сильно умные программы.
Хотя щас попробую еще один вариант.
16 — 04.11.05 — 16:21
(15) Я спрашиваю не для чего нужны транзакции ВООБЩЕ, а для чего они именно ТЕБЕ в ТВОЕЙ задаче.
Попытка КонецПопытки — служит для программной ОБРАБОТКИ ошибки (без них программа останавливается), а не для исключения факта того, что ошибка произошла.
17 — 04.11.05 — 16:26
Я и не прошу, чтобы ошибка исключалась, я хочу, чтобы она игнорировалась. Т.е. не получилось записать документ, ну и хрен с ним, значит не сильно хотели, т.е. делать так, как если бы метод Записать вообще не использовался для данного документа. Вот.
А вообще я попробовал отдельную транзакцию на запись документа внутри общей транзакции на запись всех документов. Получилось.
18 — 04.11.05 — 17:14
(17) Получиться не может. 1С не поддерживает вложенные транзакции. Несмотря на то, что это не сразу видно.
Выход один (схема):
— Работать через список документов
— Фиксировать транзакциию порциями
— Использовать счетчик успешно записанных документов (с зафиксированной транзакцией)
— В случае исключения — откатываться на начало транзакции путем ОтменитьТранзакцию(), вносить данный документ в список исключения и повторять последнюю транзакцию сначала.
В случаях, если транзакция применяется для ускорения (ночное перепроведение и т.п.) — данный рецепт труднозаменим.
19 — 04.11.05 — 20:10
Убей транзакцию и будет что хотел…
20 — 04.11.05 — 22:31
(13) а придется переписывать….
.
«Импортируется договор, в договоре есть контрагент, у контрагента есть поле БазДоговор, которое ссылается на импортируемый договор. В результате договор начинает затаскиваться, он еще не записан. Затаскивается контрагент, у контрагента есть поле Договор, затаскивается еще раз договор (он ведь не записан), он записывается, рекурсивный алгоритм возвращается к тому же договору и пытается записать его еще раз.»
.
данный алгоритм слегка неверный…. у тебя есть циклические ссылки… их надо порвать, переделав алгоритм.
insider
21 — 05.11.05 — 03:44
(0) привет, давно не виделись 
По сабжу: я так понимаю речь о типовой укр. бухии, где нельзя записать договор с незаполненным контрагентом? А что если создать временного контрагента (т.е. он заведомо существует до начала транзакции), первоначально писать в договор его и записывать договор, затем создавать нужного контрагента, прописывать его в договор и напоследок заполнять основной договор у контрагента… хотя лучше алгоритм пересмотри, pit прав.
I need to catch errors in a transaction in a stored procedure and log them in a table in the catch block.
After starting the transaction, a loop will attempt to insert a handful of values in the table. I surround each insert statement with a try/catch block, so that if a Primary Key violation occurs on any one of the inserts, I handle the error by inserting a record in a log table. I will then commit the transaction once the loop completes and it has attempted all inserts, even if one of them failed.
I’m concerned that calling SET XACT_ABORT ON at the beginning of the procedure will cause the transaction to be aborted when the PK error occurs, even though I’m catching and handling the error. Is that true, or does the try/catch intercept the error and suppress it in a way that the transaction is not aborted?
If try/catch doesn’t stop the error from aborting the transaction, then could I call SET XACT_ABORT OFF instead to get the error trapping behavior I need?
Here is some code to test the behavior:
First, create the target table and a log table:
CREATE TABLE ErrorTestTable (a int primary key clustered)
CREATE TABLE ErrorLogTable (m nvarchar(500), d datetime2(7))
Next, create the procedure:
CREATE PROCEDURE [dbo].[ErrorTest]
AS
BEGIN
SET XACT_ABORT ON;
SET NOCOUNT ON;
DECLARE @Try int = 0;
DECLARE @MaxTries int = 5;
BEGIN TRAN;
WHILE @Try < @MaxTries
BEGIN
BEGIN TRY
print 'begin try - @@trancount: ' + cast(@@TranCount as nvarchar(max)) + '; xact_state: ' + cast(XACT_STATE() as nvarchar(max))
INSERT INTO ErrorTestTable (a) VALUES (1)
INSERT INTO ErrorLogTable(m,d) VALUES ('successfully inserted record!', sysutcdatetime());
END TRY
BEGIN CATCH
PRINT 'begin catch - @@trancount: ' + cast(@@TranCount as nvarchar(max)) + '; xact_state: ' + cast(XACT_STATE() as nvarchar(max))
INSERT INTO ErrorLogTable(m,d) VALUES ('pk violation!', sysutcdatetime());
END CATCH
SET @Try += 1;
END
COMMIT TRAN;
END
Finally, run these statements and observe the behavior:
DELETE FROM ErrorLogTable
DELETE FROM ErrorTestTable
SELECT * FROM ErrorLogTable
SELECT * FROM ErrorTestTable
BEGIN TRY
EXEC dbo.ErrorTest;
END TRY
BEGIN CATCH
PRINT 'Error thrown by procedure.';
END CATCH
SELECT * FROM ErrorLogTable
SELECT * FROM ErrorTestTable
From testing a simple procedure, I’ve found a few things out. With SET XACT_ABORT ON, when the first primary key violation occurs, execution jumps to the catch handler. At that point, the transaction according to XACT_STATE is uncommitable (-1). Within the catch block, when I try to insert into the log table, another error is thrown and the stored procedure exits. I catch that error outside the sproc and select from the two tables, where I can then see the uncommitted data.
At that point the transaction is still not rolled back, and it’s not until the whole batch is complete (including the ‘select’ calls after the sproc call) that it finally displays an error message saying
Uncommittable transaction is detected at the end of the batch. The transaction is rolled back.
and finally rolls back the operations
Response to comments
Why not just validate the information first, or do the error handling in the application calling the proc?
Already doing both. Not only validating, but taking out a custom application lock on the value-to-be-inserted, so lock contention is virtually non-existant, and PK-violations are basically impossible. I just want to know if I can handle the error if it were to occur. This is a very high-concurrency (100+ threads) rapid insert of «unused» random identifiers, so it has to not only check whether X random values are already in use, but settle on one to insert by attempting to take out a lock on the value. If, after successful lock acquisition, it still is not in use, I’ll insert it.
I need to catch errors in a transaction in a stored procedure and log them in a table in the catch block.
After starting the transaction, a loop will attempt to insert a handful of values in the table. I surround each insert statement with a try/catch block, so that if a Primary Key violation occurs on any one of the inserts, I handle the error by inserting a record in a log table. I will then commit the transaction once the loop completes and it has attempted all inserts, even if one of them failed.
I’m concerned that calling SET XACT_ABORT ON at the beginning of the procedure will cause the transaction to be aborted when the PK error occurs, even though I’m catching and handling the error. Is that true, or does the try/catch intercept the error and suppress it in a way that the transaction is not aborted?
If try/catch doesn’t stop the error from aborting the transaction, then could I call SET XACT_ABORT OFF instead to get the error trapping behavior I need?
Here is some code to test the behavior:
First, create the target table and a log table:
CREATE TABLE ErrorTestTable (a int primary key clustered)
CREATE TABLE ErrorLogTable (m nvarchar(500), d datetime2(7))
Next, create the procedure:
CREATE PROCEDURE [dbo].[ErrorTest]
AS
BEGIN
SET XACT_ABORT ON;
SET NOCOUNT ON;
DECLARE @Try int = 0;
DECLARE @MaxTries int = 5;
BEGIN TRAN;
WHILE @Try < @MaxTries
BEGIN
BEGIN TRY
print 'begin try - @@trancount: ' + cast(@@TranCount as nvarchar(max)) + '; xact_state: ' + cast(XACT_STATE() as nvarchar(max))
INSERT INTO ErrorTestTable (a) VALUES (1)
INSERT INTO ErrorLogTable(m,d) VALUES ('successfully inserted record!', sysutcdatetime());
END TRY
BEGIN CATCH
PRINT 'begin catch - @@trancount: ' + cast(@@TranCount as nvarchar(max)) + '; xact_state: ' + cast(XACT_STATE() as nvarchar(max))
INSERT INTO ErrorLogTable(m,d) VALUES ('pk violation!', sysutcdatetime());
END CATCH
SET @Try += 1;
END
COMMIT TRAN;
END
Finally, run these statements and observe the behavior:
DELETE FROM ErrorLogTable
DELETE FROM ErrorTestTable
SELECT * FROM ErrorLogTable
SELECT * FROM ErrorTestTable
BEGIN TRY
EXEC dbo.ErrorTest;
END TRY
BEGIN CATCH
PRINT 'Error thrown by procedure.';
END CATCH
SELECT * FROM ErrorLogTable
SELECT * FROM ErrorTestTable
From testing a simple procedure, I’ve found a few things out. With SET XACT_ABORT ON, when the first primary key violation occurs, execution jumps to the catch handler. At that point, the transaction according to XACT_STATE is uncommitable (-1). Within the catch block, when I try to insert into the log table, another error is thrown and the stored procedure exits. I catch that error outside the sproc and select from the two tables, where I can then see the uncommitted data.
At that point the transaction is still not rolled back, and it’s not until the whole batch is complete (including the ‘select’ calls after the sproc call) that it finally displays an error message saying
Uncommittable transaction is detected at the end of the batch. The transaction is rolled back.
and finally rolls back the operations
Response to comments
Why not just validate the information first, or do the error handling in the application calling the proc?
Already doing both. Not only validating, but taking out a custom application lock on the value-to-be-inserted, so lock contention is virtually non-existant, and PK-violations are basically impossible. I just want to know if I can handle the error if it were to occur. This is a very high-concurrency (100+ threads) rapid insert of «unused» random identifiers, so it has to not only check whether X random values are already in use, but settle on one to insert by attempting to take out a lock on the value. If, after successful lock acquisition, it still is not in use, I’ll insert it.
рубрики: Язык программирования 1С | Дата: 25 апреля, 2017
В прошлый раз мы рассмотрели простейший способ явной организации транзакций средствами встроенного языка 1С. На практике транзакции гораздо чаще используются совместно с конструкцией Попытка — Исключение. Это позволяет в случае ошибки продолжить выполнение кода, а также выдать адекватное сообщение об ошибке пользователю и записать информацию в журнал регистрации или в файл логов для последующего анализа администратором системы.
Если мы обратимся к технической документации или к диску ИТС, то увидим, что фирма 1С рекомендует следующий способ организации транзакции в попытке
Попытка
//1. Начало транзакции.
НачатьТранзакцию();
//2. Блок операций, выполняющихся в транзакции.
//3. Если все операции успешны, фиксируем транзакцию.
ЗафиксироватьТранзакцию();
Исключение
//4. Если при выполнении кода возникли ошибки, отменяем транзакцию.
ОтменитьТранзакцию();
//5. При необходимости запись в журнал регистрации.
//6. При необходимости вывод сообщения пользователю.
КонецПопытки;
Собственно каких-то особых пояснений код не требует. Если в процессе попытки выполнения транзакционного кода возникает ошибка, мы сразу проваливаемся в блок исключение, т.е. до метода ЗафиксироватьТранзакцию() мы просто не доходим. Ну а в исключении соответственно отменяем транзакцию и если это необходимо выводим сообщение об ошибке и записываем информацию в журнал регистрации. Фиксировать ошибки в журнале регистрации крайне желательно, особенно для тех операций, которые выполняются без участия пользователя (например, регламентные задания). Это позволит в дальнейшем проанализировать ошибку. Вместо записи в журнал регистрации можно организовать отправку сообщений администратору по электронной почте.
Теперь вооружившись новыми знаниями попробуем видоизменить код, рассмотренный в статье про простейшие транзакции. Напомню, что мы рассматривали запись в справочник Товары и в регистр сведений Цена по следующей схеме:
&НаСервереБезКонтекста
Процедура ВыполнитьТранзакциюНаСервере()
НачатьТранзакцию();
//записываем новый товар
Товар = Справочники.Товары.СоздатьЭлемент();
Товар.Наименование = "Дырокол";
Товар.Записать();
//записываем цену
НаборЗаписей = РегистрыСведений.Цена.СоздатьНаборЗаписей();
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Период = ТекущаяДата();
НоваяЗапись.Товар = Товар.Ссылка;
НоваяЗапись.Сумма = 100;
НаборЗаписей.Записать();
ЗафиксироватьТранзакцию();
КонецПроцедуры
А теперь поместим транзакцию в блок Попытка Исключение. Скорее всего ошибки могут возникнуть только в момент записи в справочник или в регистр сведений, поэтому предварительную подготовку вынесем за пределы транзакции.
&НаСервереБезКонтекста
Процедура ВыполнитьТранзакциюНаСервере()
//создаем новый товар
Товар = Справочники.Товары.СоздатьЭлемент();
Товар.Наименование = "Дырокол";
//Создаем запись с ценой
НаборЗаписей = РегистрыСведений.Цена.СоздатьНаборЗаписей();
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Период = ТекущаяДата();
НоваяЗапись.Сумма = 100;
//Выполняем транзакцию в попытке
Попытка
НачатьТранзакцию();
Товар.Записать();
НоваяЗапись.Товар = Товар.Ссылка;
НаборЗаписей.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Произошла ошибка при записи товара и его цены";
Сообщение.Сообщить();
ЗаписьЖурналаРегистрации("Произошла ошибка при записи товара и его цены");
КонецПопытки;
КонецПроцедуры
Как НЕ НАДО делать
У тех кто только начинает работать с транзакциями зачастую возникает желание сделать вот таким образом
НачатьТранзакцию();
Попытка
НачатьТранзакцию();
//Блок операций
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
КонецПопытки;
Попытка
НачатьТранзакцию();
//Блок операций
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
КонецПопытки;
ЗафиксироватьТранзакцию();
Или в цикле
НачатьТранзакцию();
Для каждого Данные Из МассивДанных Цикл
Попытка
НачатьТранзакцию();
Данные.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
КонецПопытки;
КонецЦикла;
ЗафиксироватьТранзакцию();
На первый взгляд мы сделали все в соответствии с рекомендациями фирмы 1С. Но дело в том, что платформа 1С не поддерживает вложенные транзакции. То есть чисто технически так писать можно. Но при этом все вложенные транзакции не образуют новые, а относятся к той же самой транзакции верхнего уровня. Таким образом, если в одной из вложенных транзакций произойдет ошибка, следующую вложенную транзакцию нельзя будет зафиксировать. Система выдаст сообщение вида: «В данной транзакции уже происходили ошибки!». Продемонстрируем это на примере. Допустим мы решили записать два товара, каждый в своей транзакции. И сделаем эти транзакции вложенными в третью. Далее искусственно вызовем ошибку в первой транзакции с помощью метода ВызватьИсключение:
&НаСервереБезКонтекста
Процедура ВыполнитьТранзакциюНаСервере()
НачатьТранзакцию();
Попытка
НачатьТранзакцию();
Товар = Справочники.Товары.СоздатьЭлемент();
Товар.Наименование = "Стол";
Товар.Записать();
ВызватьИсключение "Ошибка записи товара.";
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = ОписаниеОшибки();
Сообщение.Сообщить();
КонецПопытки;
Попытка
НачатьТранзакцию();
Товар = Справочники.Товары.СоздатьЭлемент();
Товар.Наименование = "Стул";
Товар.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = ОписаниеОшибки();
Сообщение.Сообщить();
КонецПопытки;
ЗафиксироватьТранзакцию();
КонецПроцедуры
В результате выполнения этой процедуры увидим в окне сообщений следующее:
{ВнешняяОбработка.ТранзакцииВПопытке.Форма.Форма.Форма(20)}: Ошибка записи товара.
{ВнешняяОбработка.ТранзакцииВПопытке.Форма.Форма.Форма(40)}: Ошибка при вызове метода контекста (Записать): В данной транзакции уже происходили ошибки!
Таким образом, организация вложенных транзакций в 1С абсолютно бессмысленна.
Возможные варианты
Теперь вернемся к варианту, где мы записывали товар и цену для него. Если у нас при выполнении транзакции произойдет ошибка, то будет трудно понять в какой момент она произошла — при записи товара или при записи цены, поскольку и то и другое происходит в рамках одной попытки. Чтобы определить место возникновения ошибки, нам надо каждую операцию записи заключить в свою собственную попытку и при этом избежать вложенных транзакций. Для этого введем булеву переменную Отказ и в зависимости от ее значения в конце всех операций будем фиксировать или отменять транзакцию.
&НаСервереБезКонтекста
Процедура ВыполнитьТранзакциюНаСервере()
// Начинаем транзакцию
Отказ = Ложь;
НачатьТранзакцию();
// Пытаемся записать товар
Попытка
Товар = Справочники.Товары.СоздатьЭлемент();
Товар.Наименование = "Дырокол";
Товар.Записать();
Исключение
Отказ = Истина;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Ошибка при записи товара";
Сообщение.Сообщить();
КонецПопытки;
// Пытаемся записать цену
Попытка
НаборЗаписей = РегистрыСведений.Цена.СоздатьНаборЗаписей();
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.Период = ТекущаяДата();
НоваяЗапись.Товар = Товар.Ссылка;
НоваяЗапись.Сумма = 100;
НаборЗаписей.Записать();
Исключение
Отказ = Истина;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Ошибка при записи цены";
Сообщение.Сообщить();
КонецПопытки;
// Фиксируем или отменяем транзакцию
Если НЕ Отказ Тогда
ЗафиксироватьТранзакцию();
Иначе
ОтменитьТранзакцию();
КонецЕсли;
КонецПроцедуры
Аналогичным образом можно поступить и когда мы перебираем и записываем какие-либо данные в цикле. В этом случае мы сможем получить перечень всех данных с ошибками, если такие будут.
Эта статья является конспектом книги «Designing Data-Intensive Applications».
В суровой реальности информационных систем очень многое может пойти не так — программное или аппаратное обеспечение базы данных может отказать в любой момент; в любой момент может произойти фатальный сбой приложения; разрывы сети могут неожиданно отрезать приложение от базы данных или один узел базы от другого; состояния гонки между клиентами могут привести к неожиданным ошибкам.
Транзакции в течение десятилетий считались предпочтительным механизмом решения этих проблем. Транзакция — способ группировки приложением нескольких операций записи и чтения в одну логическую единицу. По сути, все операции записи и чтения в ней выполняются как одна: вся транзакция или целиком выполняется успешно (с фиксацией изменений), или целиком завершается неудачно (с прерыванием и откатом). Транзакции значительно упрощают для приложения обработку ошибок, поскольку нет нужды заботиться о частичных отказах.
В этом конспекте рассмотрим примеры возможных проблем и изучим алгоритмы, которые используют БД для их предотвращения. Рассмотрим вопрос управления конкурентным доступом, обсудим различные виды возникающих состояний гонки, а также реализацию в базах различных уровней изоляции.
Скользкая концепция транзакции
В конце 2000-х годов приобрели популярность нереляционные (NoSQL) базы данных. Их целью было улучшить существующее положение дел с реляционными БД с помощью новых моделей данных, репликации и секционирования. Транзакции оказались главной жертвой этого новшества: многие базы нового поколения полностью от них отказались или поменяли значение термина: теперь он стал у них означать намного более слабый набор функциональных гарантий, чем ранее.
Вместе с шумихой вокруг этого нового обильного урожая распределенных баз данных стало широко распространяться мнение, что в любой крупномасштабной системе необходимо отказаться от транзакций ради сохранения хорошей производительности и высокой доступности. С другой стороны, производители БД иногда представляют транзакционные функциональные гарантии как обязательное требование для «серьезных приложений», оперирующих «ценными данными». Обе точки зрения — преувеличение.
Истина не столь проста: как и любое другое техническое проектное решение, транзакции имеют свои достоинства и ограничения. Чтобы лучше разобраться в их плюсах и минусах, глубже заглянем в подробности предоставляемых транзакциями функциональных гарантий.
Обеспечиваемые транзакциями гарантии функциональной безопасности часто описываются известной аббревиатурой ACID (atomicity, consistency, isolation, durability — атомарность, согласованность, изоляция и сохраняемость).
Однако на практике реализации ACID в разных базах отличаются друг от друга. На сегодняшний день заявление о «совместимости системы с ACID» не дает четкого представления о предоставляемых гарантиях. К сожалению, ACID стал скорее термином из области маркетинга.
Системы, не соответствующие критериям ACID, иногда называются BASE: «как правило, доступна» (Basically Available), «гибкое состояние» (Soft state) и «конечная согласованность» (Eventual consistency). Это понятие еще более расплывчатое, чем ACID.
Давайте посмотрим на определения атомарности, согласованности, изоляции и сохраняемости. Это позволит уточнить наши представления о транзакции.
Атомарность
В общем атомарность определяется как «невозможность разбиения на меньшие части». Данный термин означает немного различные вещи в разных отраслях информатики. Например, если в многопоточном программировании один из потоков выполняет атомарную операцию, это значит, что ни при каких обстоятельствах другие потоки не могут увидеть ее промежуточные результаты.
Напротив, в контексте ACID атомарность не связана с конкурентным доступом. Этот термин не описывает, что происходит, когда несколько процессов пытаются обратиться к одним и тем же данным одновременно, поскольку относится к понятию изоляции, то есть букве I в аббревиатуре ACID.
Атомарность в ACID описывает происходящее при сбое в процессе выполнения клиентом нескольких операций записи, в момент, когда выполнена лишь их часть. Если операции записи сгруппированы в атомарную транзакцию и ее не удается завершить (зафиксировать изменения) из-за сбоя, то она прерывается и базе данных приходится откатить все уже выполненные в рамках этой транзакции операции записи.
При возникновении ошибки во время выполнения нескольких изменений без атомарности было бы сложно понять, какие из них вступили в действие. Приложение способно попытаться выполнить их снова, но здесь возникает риск выполнения одних и тех же изменений дважды; это может привести к дублированию или к ошибкам в них. Атомарность упрощает задачу: если транзакция была прервана, то приложение может быть уверено, что ничего не было изменено и можно безопасно повторить изменения.
Согласованность
Слово «согласованность» ужасно перегружено.
-
Согласованность реплик и вопрос конечной согласованности, возникающий в асинхронно реплицируемых системах.
-
Согласованное хеширование — метод секционирования, используемый в некоторых системах для перебалансировки.
-
В теореме CAP слово «согласованность» используется для обозначения линеаризуемости (linearizability).
-
В контексте ACID под согласованностью понимается то, что база данных находится, с точки зрения приложения, в «хорошем состоянии».
О первых трех пунктах можно подробнее прочесть в книге. Для репликации и секционирования автор книги выделил по целой главе.
К сожалению, одно и то же слово применяется как минимум в четырех различных смыслах.
Идея согласованности в смысле ACID состоит в том, что определенные утверждения относительно данных (инварианты) должны всегда оставаться справедливыми — например, в системе бухгалтерского учета кредит всегда должен сходиться с дебетом по всем счетам. Если транзакция начинается при допустимом (в соответствии с этими инвариантами) состоянии базы данных и любые производимые во время транзакции операции записи сохраняют это свойство, то можно быть уверенными, что система всегда будет удовлетворять инвариантам.
Атомарность, изоляция и сохраняемость — свойства базы данных, в то время как согласованность (в смысле ACID) — свойство приложения. Оно может полагаться на свойства атомарности и изоляции базы данных, чтобы обеспечить согласованность, но не на одну только базу. По мнению автора книги, букве C на самом деле не место в аббревиатуре ACID.
Изоляция
К большинству баз данных обращается одновременно несколько клиентов. Это не вызывает проблем, когда они читают и записывают в различные части базы данных. Но если они обращаются к одним и тем же записям базы, то могут возникнуть проблемы конкурентного доступа (состояния гонки).
значение счетчика
Изоляция в смысле ACID означает, что конкурентно выполняемые транзакции изолированы друг от друга — они не могут помешать друг другу. Классические учебники по базам данных понимают под изоляцией сериализуемость (serializability). То есть каждая транзакция выполняется так, будто она единственная во всей базе. БД гарантирует, что результат фиксации транзакций такой же, как если бы они выполнялись последовательно, хотя в реальности они могут выполняться конкурентно.
Сохраняемость
Сохраняемость (durability) — обязательство базы не терять записанных (успешно зафиксированных) транзакций данных, даже в случае сбоя аппаратного обеспечения или фатального сбоя самой БД.
В одноузловой базе сохраняемость обычно означает запись данных на энергонезависимый носитель информации, например, жесткий диск или SSD. Она обычно подразумевает также наличие журнала упреждающей записи или чего-то в этом роде, обеспечивающего возможность восстановления в случае повреждения структуры данных на диске. В реплицируемой БД сохраняемость может означать, что данные были успешно скопированы на некоторое количество узлов. Для обеспечения гарантии сохраняемости база должна дожидаться завершения этих операций записи или репликаций, прежде чем сообщать об успешной фиксации транзакции.
Однако абсолютная надежность недостижима: если все жесткие диски и резервные копии будут уничтожены одновременно, то база данных, безусловно, не сможет никак вас спасти.
Выводы по ACID
В ACID понятия атомарности и изоляции характеризуют действия, которые должна предпринимать база данных в случае выполнения клиентом нескольких операций записи в одной транзакции.
-
Атомарность. Если посередине последовательности операций записи происходит ошибка, то транзакцию необходимо прервать, а выполненные до того момента операции аннулировать.
-
Изоляция. Конкурентно выполняемые транзакции не должны мешать друг другу. Например, если одна транзакция выполняет несколько операций записи, то другая должна видеть или все их результаты, или никакие, но не какое-то подмножество.
Эти определения предполагают, что необходимо модифицировать несколько объектов (строк, документов или записей) одновременно. Подобные многообъектные транзакции часто оказываются нужны для обеспечения синхронизации нескольких элементов данных.
Обработка ошибок и прерывание транзакций
Отличительная особенность транзакций — возможность их прерывания и безопасного повторного выполнения в случае возникновения ошибки. На этом принципе построены базы данных ACID: при возникновении риска нарушения гарантий атомарности, изоляции или сохраняемости БД скорее полностью отменит транзакцию, чем оставит ее незавершенной.
Но не все системы следуют этой стратегии. В частности, хранилища данных, использующие репликацию без ведущего узла, работают более или менее на основе принципа «лучшее из возможного». Он формулируется следующим образом: «База данных делает все, что может, и при столкновении с ошибкой не станет откатывать уже выполненные действия», поэтому восстановление после ошибок является обязанностью приложения.
Хотя повторение прерванных транзакций — простой и эффективный механизм обработки ошибок, он имеет недостатки:
-
Если причина ошибки — в перегруженности, то повтор транзакции только усугубит проблему.
-
Имеет смысл повторять выполнение транзакций только для временных ошибок (происходящих, например, из-за взаимной блокировки, нарушения изоляции, временных проблем с сетью или восстановления после сбоя). Попытка повтора выполнения при постоянной ошибке (допустим, при нарушении ограничения) бессмысленна.
-
Если у транзакции есть побочные действия вне базы данных, то они могут выполняться даже в случае ее прерывания. Например, вряд ли вы захотите повторять отправку сообщения электронной почты при каждой попытке повтора транзакции.
-
В случае, когда транзакция была выполнена успешно, но произошел сбой сети при подтверждении клиенту ее успешной фиксации (вследствие чего клиент думает, что она завершилась неудачей), повтор приведет к выполнению этой транзакции дважды.
Слабые уровни изоляции
Транзакции, не затрагивающие одних и тех же данных, могут спокойно выполняться конкурентно, поскольку друг от друга не зависят. Проблемы конкурентного доступа (состояния гонки) возникают, только если одна транзакция читает данные, модифицируемые в этот момент другой, или две транзакции пытаются одновременно модифицировать одни и те же данные.
Базы данных долгое время пытались инкапсулировать вопросы конкурентного доступа от разработчиков приложений путем изоляции транзакций (transaction isolation). Теоретически изоляция должна была облегчить жизнь разработчиков, которые смогли бы сделать вид, что никакого конкурентного выполнения не происходит: сериализуемая изоляция означает гарантию базой данных такого режима выполнения транзакций, как будто они выполняются последовательно.
На практике, к сожалению, с изоляцией не все так просто. Затраты на сериализуемую изоляцию довольно высоки, и многие базы данных не согласны платить столь высокую цену. Так что многие системы часто задействуют более слабые уровни изоляции, защищающие от части проблем конкурентного доступа, а не от всех.
Автор книги не стал отдельно рассматривать уровень изоляции «чтение незафиксированных данных» (read uncommitted). Он предотвращает «грязные» операции записи, но не «грязные» операции чтения.
Чтение зафиксированных данных
Этот уровень изоляции обеспечивает две гарантии:
-
При чтении из БД клиент видит только зафиксированные данные (никаких «грязных» операций чтения).
-
При записи в БД можно перезаписывать только зафиксированные данные (никаких «грязных» операций записи).
Если транзакция записала данные в базу, но еще не была зафиксирована или была прервана и другая транзакция увидела эти незафиксированные данные, то такая операция чтения называется «грязной» (dirty read).
Выполняемые при уровне изоляции транзакций read committed (чтение зафиксированных данных) транзакции должны предотвращать «грязные» операции чтения. Это значит, что любые операции записи, выполняемые транзакцией, становятся видны другим транзакциям только после фиксации данной.
Если более ранняя операция записи представляет собой часть еще не зафиксированной транзакции и более поздняя транзакция перезаписывает незафиксированное значение, то такая операция называется «грязной» операцией записи.
Чтение зафиксированных данных предотвращает казусы, например, как на рис. 3, связанные с грязной операцией записи.
Чтение зафиксированных данных — очень популярный уровень изоляции. Он используется по умолчанию в Oracle 11g, PostgreSQL, SQL Server 2012, MemSQL и многих других базах данных.
Чаще всего базы используют блокировки строк для предотвращения «грязных» операций записи: прежде чем модифицировать конкретный объект (строку или документ), транзакция должна сначала установить блокировку на этот объект. Данная блокировка должна удерживаться вплоть до фиксации или прерывания транзакции. Удерживать блокировку на конкретный объект может только одна транзакция одновременно, другой транзакции, желающей выполнить операцию записи в этот объект, придется дождаться фиксации или прерывания первой транзакции и лишь затем получить на него блокировку и продолжить свою работу. Подобные блокировки выполняются базами автоматически в режиме чтения зафиксированных данных (и на более сильных уровнях изоляции).
Большинство БД предотвращают «грязные» операции чтения с помощью подхода, показанного на рис. 2: база запоминает для каждого записываемого объекта как старое зафиксированное значение, так и новое, устанавливаемое транзакцией, удерживающей в данный момент блокировку записи. Во время выполнения транзакции всем другим транзакциям, читающим объект, просто возвращается старое значение. Только после фиксации нового значения транзакции начинают получать его при чтении.
Изоляция снимков состояния и воспроизводимое чтение
На первый взгляд уровня изоляции чтения зафиксированных данных вполне достаточно для транзакций.
Однако на этом уровне изоляции все еще существует множество возможных ошибок конкурентного доступа. Например, на рис. 4 показана одна из вероятных проблем при чтении зафиксированных данных.
Подобная аномалия носит название невоспроизводимого чтения (nonrepeatable read) или асимметрии чтения (read skew): если Алиса прочитала бы баланс счета 1 опять в конце транзакции, то увидела бы значение ($600), отличное от прочитанного предыдущим запросом. Асимметрия считается допустимой при изоляции уровня чтения зафиксированных данных: видимые Алисе балансы счетов были, безусловно, зафиксированы на момент их чтения.
В случае Алисы — это лишь временная проблема. Однако в некоторых ситуациях подобные временные несоответствия недопустимы.
-
Резервное копирование. Резервная копия представляет собой копию всей базы данных, и ее создание на большой БД может занять несколько часов. Операции записи в базу продолжают выполняться во время создания резервной копии. Следовательно, может оказаться, что одни части копии содержат старые версии данных, а другие — новые. В случае восстановления БД из подобной резервной копии упомянутые расхождения (например, пропавшие деньги) станут из временных постоянными.
-
Аналитические запросы и проверки целостности. Иногда приходится выполнять запросы, просматривающие значительные части базы данных. Они также могут быть частью периодической проверки целостности (мониторинга на предмет порчи данных). Если подобные запросы будут видеть разные части БД по состоянию на различные моменты времени, то их результаты будут совершенно бессмысленными.
Изоляция снимков состояния — чаще всего используемое решение этой проблемы. Ее идея состоит в том, что каждая из транзакций читает данные из согласованного снимка состояния БД, то есть видит данные, которые были зафиксированы в базе на момент ее (транзакции) начала.
В других источниках данный уровень изоляции может называться повторяющимся чтением (repeatable read)
Операции чтения не требуют никаких блокировок. С точки зрения производительности основной принцип изоляции снимков состояния звучит как «чтение никогда не блокирует запись, а запись — чтение».
Базы данных применяют для реализации изоляции снимков состояния похожий механизм, действие которого по части предотвращения «грязных» операций чтения мы наблюдали на рис. 2. БД должна хранить для этого несколько различных зафиксированных версий объекта, поскольку разным выполняемым транзакциям может понадобиться состояние базы на различные моменты времени. Вследствие хранения одновременно нескольких версий объектов этот метод получил название многоверсионного управления конкурентным доступом (multiversion concurrency control, MVCC).
Если базе необходима только изоляция уровня чтения зафиксированных данных, но не уровня изоляции снимков состояния, достаточно было бы хранить только две версии объекта: зафиксированную версию и перезаписанную, но еще не зафиксированную версию. Однако поддерживающие изоляцию снимков состояния подсистемы хранения обычно используют MVCC и для изоляции уровня чтения зафиксированных данных. При этом обычно при чтении таких данных применяется отдельный снимок состояния для каждого запроса, а при изоляции снимков состояния — один и тот же снимок состояния для всей транзакции.
с помощью многоверсионных объектов
В каждой строке таблицы есть поле created_by, содержащее идентификатор транзакции, вставившей эту строку в таблицу. Более того, в каждой строке таблицы есть поле deleted_by, изначально пустое. Если транзакция удаляет строку, то строка на самом деле не удаляется из базы данных, а помечается для удаления путем установки значения этого поля в соответствии с идентификатором запросившей удаление транзакции. В дальнейшем, когда уже никакая транзакция точно не обратится к удаленным данным, процесс сборки мусора БД удалит все помеченные для удаления строки и освободит занимаемое ими место.
Более подробно о правилах видимости для согласованных снимков состояния можно прочесть в книге.
На этом первая часть конспекта, посвященного транзакциям, закончена. В следующей части рассмотрим асимметричные записи и фантомы, изоляцию уровня сериализуемости, в том числе различные методы, которые обеспечивают сериализуемость.
Ссылки на все части
-
Подсистемы хранения и извлечение данных. Конспект книги «Designing Data-Intensive Applications».
-
Транзакции. Часть 1. Конспект книги «Designing Data-Intensive Applications».
-
Транзакции. Часть 2. Конспект книги «Designing Data-Intensive Applications».





