В прошлых статьях мы уже упоминали о управлении транзакциями в нашей платформе. В этой статье расскажем подробнее о реализации транзакций, их управлении и прочем.
С самого начала мы решили, что сервер приложений должен поддерживать «транзакционную целостность». Под этим термином мы понимаем, что любое обращение к серверу приложений должно либо завершиться успешно, либо все изменения должны быть отменены. Соответственно, при начале обработки серверного вызова создается транзакция (если быть точным она возникает при первом изменении в базе данных) и фиксируется (или отменяется) при выходе из вызова:
При этом, прикладной разработчик не имеет прямого доступа к соединению с БД, и все общение происходит через соответствующую прослойку. В итоге, конечно прикладной разработчик может попытаться выполнить фиксирующие запросы (например выполнить процедуру, в которой выполняется truncate), однако от выстрела в ногу такие разработчики никогда не застрахованы.
Да, такая реализация, теоретически, увеличивает время и, как следствие, вероятность избыточного блокирования. На практике избежать этого достаточно легко. Транзакционная целостность позволяет нам поддерживать данные в согласованном состоянии в условиях частого изменения большого (около 5МБ) объема исходного кода (в прикладной части). В свою очередь, ее отсутствие часто приводит к нарушению согласованности, как правило в результате следующего сценария:
Пусть у нас есть некая функциональность, реализованная в функции UpdateDocument:
Пусть у нас есть команда над документом, которая вызывает метод UpdateDocument:
когда эти 2 функции написаны рядом и не меняются, легко заметить проблему — после вызова UpdateDocument в функции Command часть изменений, сделанных до этого вызова будут зафиксированы. Если в дальнейшем что-то пойдет не так, то произойдет откат только тех изменений, которые сделаны после вызова UpdateDocument!
Менее очевидно, что после вызова UpdateDocument начнется новая транзакция. Это имеет много следствий, например, временные таблицы будут пустыми, или снимутся блокировки установленные до фиксации изменений, и, значит, в новой транзакции запросы будут возвращать иные, нежели предполагалось, значения.
Ошибки такого рода крайне тяжело диагностируются, более того, они могут приводить к постепенной коррозии данных которые никто не заметит в течение дней, месяцев и даже лет, когда проблема окажется очень серьезной.
Именно поэтому мы выбрали вариант реализации с транзакционной целостностью. Однако, случаются ситуации когда необходимо делать промежуточные фиксации. Например — одна из задач в решении занимается «снятием резервов». По сути она перебирает документы и последовательно их изменяет. По традиции, приведем как было реализовано в первых версиях (весь лишний для понимания код удален):
Все работало неплохо, пока логика не поменялась и метод Save не начал выбрасывать исключение. Это привело к тому, что в данных начали появляться странные артефакты, определить их происхождение было крайне сложно.
А происходило вот что — на какой-то итерации выбрасывалось исключение, которое записывалось в лог. А на следующей, успешной итерации фиксировались изменения сделанные до выбрасывания исключения!
Как решение, было предложено отказаться от возможности получения доступа к текущему соединению, но предоставить возможность запросить новое соединение, для которого уже можно выполнить фиксацию изменений. При этом, все вложенные вызовы, которые не запрашивают явно новое соединение, работают с полученным выше по cтеку вызовов соединением.
Код при этом выглядит так:
Все вызовы (включая вложенные) внутри using используют транзакцию созданную в вызове CreateTransactionScope. Как видно, разработчику не требуется самостоятельно беспокоится о получении соединения, более того, у него нет для этого инструментов. Прикладной разработчик во вложенных вызовах при необходимости фиксации промежуточных данных может только запросить новую транзакцию. Таким образом, практически на уровне языковых конструкций мы избавлены от промежуточных фиксаций, приводящих в коррозии данных.
Существуют и другие варианты приводящие к коррозии данных с которыми можно бороться похожими методами. Попытаемся рассказать о них в будущих статьях.
С самого начала мы решили, что сервер приложений должен поддерживать «транзакционную целостность». Под этим термином мы понимаем, что любое обращение к серверу приложений должно либо завершиться успешно, либо все изменения должны быть отменены. Соответственно, при начале обработки серверного вызова создается транзакция (если быть точным она возникает при первом изменении в базе данных) и фиксируется (или отменяется) при выходе из вызова:
При этом, прикладной разработчик не имеет прямого доступа к соединению с БД, и все общение происходит через соответствующую прослойку. В итоге, конечно прикладной разработчик может попытаться выполнить фиксирующие запросы (например выполнить процедуру, в которой выполняется truncate), однако от выстрела в ногу такие разработчики никогда не застрахованы.
Да, такая реализация, теоретически, увеличивает время и, как следствие, вероятность избыточного блокирования. На практике избежать этого достаточно легко. Транзакционная целостность позволяет нам поддерживать данные в согласованном состоянии в условиях частого изменения большого (около 5МБ) объема исходного кода (в прикладной части). В свою очередь, ее отсутствие часто приводит к нарушению согласованности, как правило в результате следующего сценария:
Пусть у нас есть некая функциональность, реализованная в функции UpdateDocument:
public void UpdateDocument(Document doc)
{
try
{
// что-то делаем с документом и фиксируем транзакцию
Database.Commit();
}
catch (Exception ex)
{
Log("Ошибка при обновлении документа: ", ex);
Database.Rollback();
}
}
Пусть у нас есть команда над документом, которая вызывает метод UpdateDocument:
public void Command(Document doc)
{
try
{
UpdateDocument(doc);
// еще какие-то действия
if (somethingWrong)
{
throw new Exception("Случилось страшное!");
}
Database.Commit();
}
catch (Exception e)
{
Log("Ошибка в команде над документом: ", ex);
Database.Rollback();
}
}
когда эти 2 функции написаны рядом и не меняются, легко заметить проблему — после вызова UpdateDocument в функции Command часть изменений, сделанных до этого вызова будут зафиксированы. Если в дальнейшем что-то пойдет не так, то произойдет откат только тех изменений, которые сделаны после вызова UpdateDocument!
Менее очевидно, что после вызова UpdateDocument начнется новая транзакция. Это имеет много следствий, например, временные таблицы будут пустыми, или снимутся блокировки установленные до фиксации изменений, и, значит, в новой транзакции запросы будут возвращать иные, нежели предполагалось, значения.
Ошибки такого рода крайне тяжело диагностируются, более того, они могут приводить к постепенной коррозии данных которые никто не заметит в течение дней, месяцев и даже лет, когда проблема окажется очень серьезной.
Именно поэтому мы выбрали вариант реализации с транзакционной целостностью. Однако, случаются ситуации когда необходимо делать промежуточные фиксации. Например — одна из задач в решении занимается «снятием резервов». По сути она перебирает документы и последовательно их изменяет. По традиции, приведем как было реализовано в первых версиях (весь лишний для понимания код удален):
....
foreach(docId in docs)
{
try
{
var doc = DocumentManager.GetDocument(docId);
doc.state = DocStateCancelled;
DocumentManager.Save(doc);
Server.Commit();
}
catch(Exception e)
{
LogManager.Log("something is wrong");
}
}
...
Все работало неплохо, пока логика не поменялась и метод Save не начал выбрасывать исключение. Это привело к тому, что в данных начали появляться странные артефакты, определить их происхождение было крайне сложно.
А происходило вот что — на какой-то итерации выбрасывалось исключение, которое записывалось в лог. А на следующей, успешной итерации фиксировались изменения сделанные до выбрасывания исключения!
Как решение, было предложено отказаться от возможности получения доступа к текущему соединению, но предоставить возможность запросить новое соединение, для которого уже можно выполнить фиксацию изменений. При этом, все вложенные вызовы, которые не запрашивают явно новое соединение, работают с полученным выше по cтеку вызовов соединением.
Код при этом выглядит так:
....
foreach(docId in docs)
{
using (var ts = new TransactionScope())
{
try
{
var doc = DocumentManager.GetDocument(docId);
doc.state = DocStateCancelled;
DocumentManager.Save(doc);
ts.Complete();
}
catch(Exception e)
{
LogManager.Log("something is wrong");
}
}
}
...
Все вызовы (включая вложенные) внутри using используют транзакцию созданную в вызове CreateTransactionScope. Как видно, разработчику не требуется самостоятельно беспокоится о получении соединения, более того, у него нет для этого инструментов. Прикладной разработчик во вложенных вызовах при необходимости фиксации промежуточных данных может только запросить новую транзакцию. Таким образом, практически на уровне языковых конструкций мы избавлены от промежуточных фиксаций, приводящих в коррозии данных.
В качестве заключения
Существуют и другие варианты приводящие к коррозии данных с которыми можно бороться похожими методами. Попытаемся рассказать о них в будущих статьях.