24 Транзакционное поведение

      Комментарии к записи 24 Транзакционное поведение отключены

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 10 мес., 1 неделя назад.

  • Автор
    Сообщения
  • #2949

    Что такое транзакционное поведение

    Код, обладающий транзакционным поведением, это такой код, любой участок которого, будучи помещенным в блок catch, имеет полноценную семантику транзакции. Транзакция это, в свою очередь, последовательность команд, которая либо выполняется полностью, либо, в случае ошибки, не выполняется вообще. Точнее — в случае ошибки откатывает все изменения, внесенные в систему, с начала транзакции до момента возникновения ошибки.

    Транзакционное поведение с точки зрения языка C++ — это способность автоматически откатывать изменения, внесенные в систему, в процессе «полета» исключения от места генерации до места его перехвата.

    Что дает транзакционное поведение

    Механизм транзакций как способ обеспечения отказоустойчивости системы имеет неоспоримые преимущества перед любым другим способом обработки ошибок. Достоинства механизма транзакций очевидны:

    1. Логика транзакций предельно проста и универсальна — весь механизм построен на единственном, интуитивно понятном и легко формализуемом правиле;
    2. Достаточным условием транзакционности системы является транзакционность каждого отдельно взятого ее элемента — транзакционность легко интегрируется, «разделяй и властвуй»;
    3. При правильной реализации отказоустойчивость и обработка ошибок происходят автоматически, без каких-либо дополнительных действий — обработка ошибок, говоря формально, не увеличивает энтропию системы;

    Реализация любого другого способа обработки ошибок потребует написания дополнительного кода на уровне использования функциональности, тем самым увеличивая вероятность возникновения ошибок по вине человеческой недальновидности и невнимательности. Реализация же транзакционного поведения, напротив, придаст коду дополнительную строгость и однозначность, значительно снизив вероятность возникновения ошибок, связанных с человеческим фактором.

    Механизм транзакций на сегодняшний день является наиболее популярным способом обработки ошибок и обеспечения отказоустойчивости в крупнейших коммерческих решениях.

    Как добиться транзакционного поведения?

    По большому счету, любой код потенциально небезопасен с точки зрения исключений. Чтобы быть на сто процентов уверенным в безопасности кода, необходимо сделать полный снимок системы перед входом в блок try, и восстановить по нему систему перед входом в блок catch. Как вы понимаете, C++ таких возможностей не предоставляет.

    Одна из основных проблем состоит в том, что от кода, небезопасного с точки зрения исключений, невозможно защититься. Его нельзя спрятать, инкапсулировать или адаптировать. Один небезопасный элемент делает небезопасной всю систему, в которой он участвует. Единственное, решение состоит в том, чтобы полностью переписать «плохой» элемент.

    Когда исключение покидает область видимости, все локальные объекты этой области уничтожаются. Однако изменения, внесенные во внешние объекты, как вы понимаете, остаются на месте. Сложность реализации транзакционного поведения состоит в откате именно этих изменений.

    Практически любая функциональная единица работает по одному и тому же обобщенному сценарию: она получает какие-то входные данные, выполняет какую-то обработку и выдает какой-то результат. С этой точки зрения обеспечение транзакционности достигается откладыванием сохранения изменений на самый последний шаг работы функциональной единицы. Говоря простым языком — сделайте все вычисления, подготовьте новые данные и только потом сохраните их в каком-то внешнем хранилище.

    Пример плохого кода:

    #include <iostream>
    #include <string>
    #include <vector>
    
    #include "object.h"
    
    typedef std::vector<std::string> file_list;
    typedef std::vector<object_ptr> object_container;
    
    void load_objects(file_list const& files, object_container& result)
    {
        for(file_list::const_iterator it = files.begin(); it != files.end(); ++it)
        {
            file_ptr fl = create_file(*it);   // Может сгенерировать исключение
            object_ptr obj = load_object(fl); // Может сгенерировать исключение
            result.push_back(obj);
        }
    }
    
    int main()
    {
        file_list files;
        object_container objects;
    
        // ...
        // Инициализация списка файлов
        // ...
    
        try
        {
            load_objects(files, objects);
        }
        catch(std::exception const& e)
        {
            std::cout < < "Не могу загрузить объекты: " << e.what() << std::endl;
        }
    
        return 0;
    }

    Более безопасный код:

    // Добавление загруженных объектов в пустой контейнер
    
    void load_objects(file_list const& files, object_container& result)
    {
        object_container local_result;
    
        for(file_list::const_iterator it = files.begin(); it != files.end(); ++it)
        {
            file_ptr fl = create_file(*it);   // Может сгенерировать исключение
            object_ptr obj = load_object(fl); // Может сгенерировать исключение
            local_result.push_back(obj);
        }
    
        result.swap(local_result);
    }
    
    // Добавление загруженных объектов в, возможно, непустой контейнер
    
    void load_objects(file_list const& files, object_container& result)
    {
        object_container local_result;
    
        for(file_list::const_iterator it = files.begin(); it != files.end(); ++it)
        {
            file_ptr fl = create_file(*it);   // Может сгенерировать исключение
            object_ptr obj = load_object(fl); // Может сгенерировать исключение
            local_result.push_back(obj);
        }
        
        result.insert(result.end(), local_result.begin(), local_result.end());
    }

    Наиболее частая ошибка заключается в чередовании кода, потенциально генерирующего исключения, и кода, сохраняющего новые данные в системе. Для обеспечения транзакционности следует разделять код на две части — «бросающую» обработку данных и «небросающее» сохранение результатов. Кстати говоря, это одна из основных причин того, что все эксперты языка настоятельно рекомендуют для пользовательских типов данных обеспечивать бессбойные (не генерирующие исключения) конструктор копирования, оператор присваивания и деструктор. Я, в свою очередь, дам более общую рекомендацию: реализуйте операции по сохранению изменений в системе как можно более локально и сделайте все, чтобы обеспечить их бессбойность, при этом не нарушая их семантику.

    Следует выдерживать код однородным с точки зрения безопасности с точки зрения исключений. Транзакционность должна быть везде и во всем. Транзакционность не должна «сломаться», если добавить новый или убрать существующий блок try-catch, причем в совершенно произвольном месте. Следует стремиться к тому, чтобы блок catch не содержал какого-то уникального кода по ремонту системы. В идеальном мире содержимое блока catch должно укладываться в один макрос.

    Обратите особое внимание на работу с глобальными сущностями (глобальные переменные, объекты и синглтоны), а также с сущностями, обращение к которым идет через дополнительный уровень косвенности (указатели, ссылки, индексы и ключи). Подобная работа дает возможность вносить сквозные изменения из любого в любой этаж иерархии сущностей в системе. Выполнить откат таких изменений обычно очень сложно, если вообще возможно. Старайтесь избегать подобных действий. Помимо этого, предпочитайте свободные функции, принимающие объекты по константным ссылкам. Старайтесь не использовать модификатор mutable. Если функция-член класса по своей семантике не должна вносить изменений в объект, то объявите ее как const.

    Существуют искусственные приемы обеспечения транзакционности; например — паттерн «Transaction». Однако, в реальной жизни просто так взять абы какой код и искусственно обеспечить его транзакционность, практически невозможно, поскольку простого (даже глубинного) копирования объектов-членов транзакции недостаточно — объекты-члены транзакции могли модифицировать внешние объекты, список которых не был полностью известен на момент начала транзакции.

    Еще хуже дела обстоят с обеспечением транзакционности, когда речь идет о работе с внешними системами. Представьте себе, что вам нужно откатить изменения, внесенные функцией удаления файла или отправки сетевого пакета. В самом деле, не будете же вы призывать ушедший пакет вернуться обратно. Здесь, очевидно, нет и не может быть никакого решения, поэтому следует особо аккуратно выполнять любую работу с внешними системами.

    Можно выделить два основных способа реализации механизма транзакций:

    1. Функциональность сохраняет данные во временный буфер. При удачном окончании транзакции данные копируются в основное хранилище. Например таким образом работают транзакции Microsoft SQL Server. Минус такого способа заключается в том, что функциональности придется явно указывать на то, что она подчиняется какой-то транзакции. Так, в общем-то, и происходит в ADO.NET. Однако, у этого способа есть и один неоспоримый плюс — если в момент транзакции вынуть вилку из розетки, то транзакционное поведение не будет нарушено. Кроме того, данный способ оптимален по производительности, так как копировать придется только те данные, которые действительно были изменены (если конечно временный буфер не представляет собой журнал событий, а транзакция не длится пол дня);
    2. Данные предварительно копируются во временный буфер. Функциональность вносит изменения непосредственно в основное хранилище. При неудачном окончании транзакции данные из временного хранилища копируются в основное. Этот способ реализован в паттерне «Transaction», и имеет зеркальную противоположность по достоинствам и недостаткам с предыдущим способом. Кроме того, только этот способ может быть реализован прозрачно для пользователя в языке C++.

    Я рекомендую вам ознакомиться с тем, как реализованы транзакции и транзакционное поведение в других системах, какие достоинства и недостатки присутствуют на той или иной платформе — не стоит зацикливаться в этом вопросе только на возможностях языка C++.

    Резюме

    Разделяйте «бросающую» обработку данных и их «небросающее» сохранение. Вносите изменения локально и бессбойно. Не «затачивайте» код под конкретные блоки try-catch. Будьте аккуратны при работе с дополнительным уровнем косвенности и вдвойне аккуратны при работе с внешними системами, не предоставляющими откатной функциональности. Обеспечивайте транзакционность изнутри, а не снаружи.

    Статья взята с сайта insidecpp. К сожалению, сайт уже пол года не работает, но раньше на нем размещались качественные материалы. Хотелось бы, чтобы такие материалы были доступны. Я пробовал связаться с автором статей, но не достучался. Если вы являетесь автором и против размещения статей — свяжитесь со мной.

Для ответа в этой теме необходимо авторизоваться.