Недавно пришлось поработать с интересной вещью и я решил поделиться этим с читателями «Хабрахабра». Я хочу рассказать об опыте использования 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, я тоже буду очень рад и смогу добавить себе в копилку еще немного материала.
Заранее спасибо.
Автор: Dki56t
Можно ли использовать такой способ для гарантированного отслеживания изменений в базе, для ETL процессов из ORACLE?