Недавно пришлось поработать с интересной вещью и я решил поделиться этим с читателями «Хабрахабра». Я хочу рассказать об опыте использования Object Change Notification в Oracle. О том, как узнать, что данные изменились, не делая запрос.

Итак, зачем нужен Object Change Notification (OCN)


Нам необходимо было наблюдать за изменением данных в базе. Решение, которое первым пришло в голову, было простое — обновление данных в течение заданного интервала времени (к примеру, раз в минуту). Минусы такого подхода очевидны — данные могли и не измениться за выбранный период и тогда мы просто нагрузим сервер и канал передачи данных (данные, которые мы наблюдаем, весьма объемны). Либо данные изменятся в начале временного интервала и мы не узнаем об этом, пока время не кончится (это не так критично, но вот первое вполне вероятно). С учетом того, что клиентов могло быть довольно много, первый минус действительно серьезен. К счастью, мы обнаружили, что Oracle предлагает более совершенное решение этой проблемы, начиная с версии 10g.

Как работает OCN


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

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



В начале необходимо разрешить на сервере использование регистраций запросов и разрешить использовать параллельные уведомления. Затем, пользователь регистрирует оповещение. При регистрации указывается запрос, который необходимо наблюдать, к примеру:
select * from TESTUSER.TESTTABLE;
После того, как данные в результирующем наборе изменятся, oracle оповестит клиента об этом (либо пошлет специальный пакет по зарегистрированному ip, либо выполнит зарегистрированную хранимую процедуру). Поддержка этого механизма встроена в Database Provider для .NET и Java. Поскольку наше приложение мы писали на .NET, дальше я тоже буду говорить в его контексте (хотя, большинство вещей за исключением примера кода, я полагаю, от этого не зависят, поскольку самое интересное творится на сервере).

Куда же без примера кода


Пойдем по порядку:
  • Разрешаем пользователю регистрировать непрерывные уведомления;
    GRANT CHANGE NOTIFICATION TO TESTUSER;
  • Устанавливаем значение переменной «JOB_QUEUE_PROCESSES» значение больше 0 (по умолчанию она и так больше 0, но вдруг у вас на сервере что-то нарушило умолчания);
    ALTER SYSTEM SET "JOB_QUEUE_PROCESSES"=4;
  • Создаем проект, добавляем ссылку на библиотеку Oracle.ManagedDataAccess.dll (можно скачать с сайта Oracle вместе с пакетом ODP.NET with ODAC, или забрать ее через NuGet);
  • Дальше уже можно просто пробовать.

Большой пример кода
using System;
using System.Threading;
using System.Data;
using Oracle.ManagedDataAccess.Client;

namespace ChangeNotificationSample
{
    public class ChangeNotificationSample
    {
        public static bool IsNotified;

        public static void Main(string[] args)
        {
            const string constr = "Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=myHost)(PORT=myPort)))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=myServiceName))); Validate Connection = true; Connection Timeout=180;User Id=TESTUSER;Password=myPass";
            OracleConnection con = null;
            try
            {
                con = new OracleConnection(constr);
                OracleCommand cmd = new OracleCommand("select * from TESTUSER.TESTTABLE", con);
                con.Open();

                // Устанавливаем номер порта для прослушивания уведомлений.
                OracleDependency.Port = 1005;

                //Создаем OracleDependency
                var dep = new OracleDependency(cmd);
                //указывает, что оповещать надо не один раз
                //в противном случае, регистрация удалится из базы после первого оповещения
                cmd.Notification.IsNotifiedOnce = false;
                //таймаут в секундах, время жизни регистрации
                cmd.Notification.Timeout = 300;

                //Подписываемся на событие "Что-то изменилось"
                dep.OnChange += OnMyNotificaton;

                //теперь, когда мы выполним команду, уведомление зарегистрируется на сервере
                cmd.ExecuteNonQuery();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            finally
            {
                if (con != null)
                {
                    con.Close();
                    con.Dispose();
                }
            }

            // Чтобы не нагружать процессор, будем проверять наличие уведомления только 10 раз в секунду
            while (IsNotified == false)
            {
                Thread.Sleep(100);
            }
            Console.ReadLine();
        }

        private static void OnMyNotificaton(object sender, OracleNotificationEventArgs eventArgs)
        {
            Console.WriteLine("Notification Received");
            DataTable changeDetails = eventArgs.Details;
            Console.WriteLine("Data has changed in {0}",
              changeDetails.Rows[0]["ResourceName"]);
            IsNotified = true;
        }
    }
}

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

insert into testuser.testtable (col1, col2) values (1,1); commit;

Выполнив, мы должны увидеть вот это:



Ура, все работает! Более подробную информацию можно взять, к примеру, в документации. Лучше перейдем сразу к тому, что обычно не указывают в документации – возникшие трудности.

Минусы


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

Первым делом мы попробовали запустить это у себя и у нас все прекрасно заработало. И, как это обычно и бывает, ничего не заработало у заказчика. Вернее сказать, даже не взлетело. Разобраться, в чем же дело, оказалось довольно нетривиальной задачей. После того, как вы заводите регистрацию, ее можно увидеть, выполнив специальный запрос:

select * from USER_CHANGE_NOTIFICATION_REGS;

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

Не все запросы на регистрацию поддерживаются. Надо помнить о том, что возможно не удастся вычитывать данные одним запросом и на него же вешать уведомление. Если в запросе много join-ов, лучше морально готовить себя к тому, что придется писать похожий, но простой запрос. Кроме того, если вы регистрируете оповещение на запрос с join-ми, на самом деле зарегистрируется столько оповещений, сколько таблиц фигурирует в запросе (каждый на свою таблицу), что может понапрасну расходовать ресурсы.

По умолчанию регистрации создаются без таймаута. То есть, как бы, навечно. Как правило, такая регистрация никому не нужна, ведь время работы приложения обычно ограничено. Такую регистрацию очень сложно потом выковырять из базы, тем более, если их будет много. Правильным путем, по всей видимости, было бы удаление регистрации при завершении работы с ней (если делать это в том же соединении, и есть ссылка на экземпляр OracleDependency это сделать просто, достаточно вызвать метод RemoveRegistration у экземпляра класса OracleDependency). Однако, как мне кажется, этот метод подходит только для идеальных миров, в которых ваши приложения никогда не падают, каналы связи никогда не рвутся и ресурсы всегда освобождаются. В противном случае, в конечном счете, мы будем иметь то же самое, что и без удаления, правда, последствия начнут проявляться позже (как раз, когда мы выйдем в продакшн). Мы решили проблему таким образом – стали всегда указывать таймаут при регистрации. И за несколько секунд до его истечения удалять старую регистрацию и создавать новую (чтобы минимизировать время отсутствия регистрации). Решение, конечно, не самое идеальное, но нас в принципе устроило. Если у вас есть идеи, как сделать лучше, буду очень рад переписать этот кусок кода.

Для того, чтобы в рамках одного приложения оповещения работали быстрее, мы выделили в отдельные классы логику по работе с базой данных, логику по оповещению наших объектов о событиях изменения данных и логику по управлению тем и другим. Благодаря этому мы смогли использовать один общий механизм оповещения и посылать уведомления внутри приложения вручную, не выходя наружу (в oracle). Для пользователя, который работает в рамках своей сессии и должен видеть мгновенно результат своих действий, все работает хорошо.

Спасибо


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

Заранее спасибо.

Комментарии (22)


  1. pihel
    17.06.2015 13:45

    С OCN не работал, а нельзя для этих целей использовать триггеры на таблицах? В триггере также можно вызывать процедуру или послать сообщение по сети DBMS_TCP. Какие тут минусы?


    1. Dki56t Автор
      17.06.2015 14:23
      +1

      Ну, как мне кажется у любого подхода есть своя применимость. Вообще, обилие триггеров на таблицах может снизить производительность insert-ов. Код который Вы выполняете в триггере по умолчанию выполняется в той же транзакции что и insert. И в том же потоке. А здесь все сделает менеджер задач Oracle, в фоновом потоке. Еще, Вам видимо придется самостоятельно реализовать обработку сообщений, полученных из триггеров, а здесь предлагается уже готовое решение. Зато, вероятно, гибкость которую Вы получите в случае отказа от OCN будет больше. Вопрос только в том, нужна ли она.


      1. Rupper
        18.06.2015 18:24

        Не совсем так. OCN построена поверх Advanced Queue которые, в свою очередь построены на обычных таблицах.
        Так что производительность триггеров, которые пишут в сокет будет скорее всего ВЫШЕ чем при использовании OCN (у которого будут писаться редо логи, архив логи, undo segment и т.п.)
        Проблема с триггерами и записью в сокет в том, что в оракле нет события (триггера) OnCommit, на которое можно было бы повесить обработчик (само событие вообще-то есть — materialized view умеют по OnCommit обновлятся).
        Я даже где-то понимаю оракловцев мужественно терпящих висящий с 90-х годов тикет про это событие. Действительно — а что делать если в обработчике произошло исключение? делать роллбек? или все-таки коммит?
        Так вот, в триггере на таблице, который пишет в сокет проблема в том, что вы не можете определить достоверность данных. Т.е. можно реализовать идиотский метод — запрашивать данные «в обратку» но тогда вы нагрузите оракл лишними запросами и достоверность не увеличите. Вы же не знаете, закончилась транзакция или нет.


        1. xtender
          18.06.2015 20:16
          +2

          Проблема с триггерами и записью в сокет в том, что в оракле нет события (триггера) OnCommit, на которое можно было бы повесить обработчик
          есть очень древний подход — раньше в таких случаях делали в триггере dbms_job.submit, который должен вызвать нужную процедуру-хэндлер события. Соответственно, пока основная транзакция не делала коммит — джоб не запускался.


          1. Rupper
            18.06.2015 22:01

            Тяжеловесно :) Так же как делать триггер на materialized view которая OnCommit обновляется.
            И не решает проблемы обработки Rollback. Я этой задачей интересовался в попытках реализовать хранение логов изменений данных в NoSQL DB. Собственно я хотел избежать дополнительных записей в таблицы и иметь актуальные данные. При наличии обработки коммита можно только частично решить задачу.


            1. xtender
              18.06.2015 22:13

              &

              И не решает проблемы обработки Rollback.
              в смысле? Вообще-то как раз решают — при роллбэке джоб-хэндлер вызван не будет.


              1. Rupper
                18.06.2015 22:15

                Да, но как отличить это от ситуации что транзакция еще не закончилась?


                1. xtender
                  18.06.2015 22:21

                  Опять не понял вопроса… Отличать что, от чего и зачем? Предполагается, что обработка нужна только для закоммиченных изменений — так она и будет запущена только для закоммиченных изменений. Для незакоммиченных ничего не будет происходить.


                  1. Rupper
                    18.06.2015 22:41

                    Либо придется весь объем данных для передачи «наружу» передавать в параметрах для джоба, что не уменьшает нагрузку на диск, либо передавать их сразу, но тогда надо знать — закончилась транзакция или нет, чтобы пометить переданные в триггере данные как закомиченные или удалить.


                    1. xtender
                      18.06.2015 22:46

                      Если уж совсем облегчать, то надо переводить на другие механизмы типа CDC


          1. pihel
            19.06.2015 16:22
            +1

            Спасибо за ответ, про commit я действительно забыл. Но решения через dbms_job.submit очень интересное, надо записать в блокнотик :)


        1. xtender
          18.06.2015 20:18

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


          1. Rupper
            18.06.2015 22:03

            Абсолютно согласен. Хотя латчи самого оракла такое иногда вытворяют… и нагрузка на редологи только увеличивает вероятность.
            Короче, опасности везде поджидают )


            1. xtender
              18.06.2015 22:29

              Хотя латчи самого оракла такое иногда вытворяют… и нагрузка на редологи только увеличивает вероятность.
              что-то я не понял к чему было про латчи и редулоги? :) Кстати, количество латчей в оракле все уменьшается и уменьшается — все больше механизмов переводится на мьютексы.


              1. Rupper
                18.06.2015 22:39

                К тому, что иногда оракл сам задумывается так, что можно через сокет полбазы перелить.


                1. xtender
                  18.06.2015 22:45

                  Значит что-то в консерватории не так — анализируйте-тюньте :)


                  1. Rupper
                    18.06.2015 22:46

                    Чудеса, если у вас такого не случается :)


                    1. xtender
                      18.06.2015 22:47

                      у меня работа такая :)


        1. pihel
          19.06.2015 16:32

          Я даже где-то понимаю оракловцев мужественно терпящих висящий с 90-х годов тикет про это событие.

          Почему бы им не сделать 2 события: beforecommit — ошибка бы откатывала и aftercommit — ошибка не откатывала.
          Вообще хватило бы и одного aftercommit.


          1. Rupper
            19.06.2015 16:37

            потому, что подавляющее большинство приложений не ожидают никаких откатов при выполнении commit — один из ответов.
            Правда есть есть deffered constraints которые могут commit отменить.
            Про все это уже в металинке и форумах 1000 раз обсуждено, но события нет.


    1. xtender
      17.06.2015 14:32

      Можно, конечно, но не в лоб, т.к.могут быть и роллбэки


    1. realfreeman
      19.06.2015 15:51

      Триггеры в большинстве случаев «забываются» разработчиками, особенно если не они их писали :)
      Кстати, если в БД логика вынесена в plsql, то можно еще использовать стандартные пакеты dbms_pipe и dbms_alert. Естественно, соответствующий код надо прописать ручками, зато функционал потенциально интереснее.
      В одной из систем (2-звенка) перед установкой патча на БД выгоняли пользователей из приложения через алерты.