You are here: Foswiki>Gnumed Web>DBConcurrencyHandlingRu (18 Mar 2013, IvanLykov)EditAttach

Обработка одновременных изменений в базе данных GNUmed

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

Однако, на практике довольно часто один пациент открывается и редактируется на нескольких рабочих местах. Существует две проблемы, о которых в таком случае необходимо позаботиться:

1) Передача отображения на многие компьютеры при изменении записи пациента

Предположим, как пример, такой сценарий:

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

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

Для доступности такого уведомления GNUmed использует функцию PostgreSQL NOTIFY/LISTEN.

Бэкэнд

При создании базы данных таблица аллергии настроена для доставки сигналов заинтересованным интерфейсам:
   select gm.add_table_for_notifies('clin', 'allergy', 'allg');
Имеется несколько разновидностей add_table_for_notifies(), доступных и документируемых в схеме gm. Метаданные для таблиц, отправляющих уведомления, хранятся в gm.notifying_tables. Последний параметр (boolean) определяет, будет ли система уведомлена.

Позже в процессе начальной загрузки скрипт Python gmNotificationSchemaGenerator.py создает соответствующие триггеры AFTER UPDATE/INSERT/DELETE, основываясь на информации в gm.notifying_tables. Если соответствующая таблица содержит столбец, известный для прямой или косвенной связки с определением пациента, триггер добавит соответствующий первичный ключ пациента для сигнала, отправляемого - в противовес, скажем, таблицам подстановки.

Во время работы обычной базы данных эти триггеры рассылают уведомление NOTIFY всякий раз, когда завершается UPDATE/INSERT/DELETE от любого клиента. В приведенном примере сигнальное имя, в конечном итоге, отправило бы allg_mod_db:123, предполагая, что строка, связанная с пациентом 123 была изменена.

Интерфейс

При запуске клиента будет создан поток, который прослушивает уведомление сигналов NOTIFY из базы данных. Он автоматически настраивает себя для прослушивания всех сигналов, записанных в gm.notifying_tables. Также он слушает сигналы изменений от пациента в своем собственном клиенте. Всякий раз, при изменении активного пациента, поток перенастраивает себя слушать только связанные с пациентом сигналы бэкэнда, которые являются специфическими для этого пациента -- с этой целью сигналы бэкэнда помечаются первичными ключами пациента, куда соответствовать (см. выше). Сигнал удаляет ID пациента и закачивается в клиенте для дальнейшей обработки через gmDispatcher.py. Таким образом, код интерфейса должен прослушывать только общий сигнал (например, allg_mod_db) и может уже уверенно принимать сигналы, имеющие отношение только к активному пациенту.

Наряду с приведенным примером: верхняя панель (из двух строк вверху клиента) прослушивает сигнал allg_mod_db. Обратите внимание, что этот сигнал может асинхронно прийти в любое время, но в контексте потока прослушивания ! Поскольку за пределами операций GUI wxPython записывает ненадежно, поток графического интерфейса пользователя (часто основной поток, но в любом случае поток, инициализированный wxPython) заботится о необходимости принимать при сигналах. Если необходимо сделать работу какого-либо GUI (например, обновление полей окна), рекомендуемая процедура использует wx.CallAfter() для выполнения кода GUI в контексте потока графического интерфейса пользователя следующим образом:
gmDispatcher.connect(signal = 'allg_mod_db', receiver = _on_allg_mod_db_in_listener_thread)

def _on_allg_mod_db_in_listener_thread(*args, *kwargs):
   wx.CallAfter(self._on_allg_mod_db_in_gui_thread, *args, *kwargs)

def _on_allg_mod_db(*args, *kwargs):
   update_allergies_display()

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

2) Обновления записи конфликтующего пациента

GNUmed "принимает блокировку на запись" в строке, только тогда, когда она на самом деле для записи. Это не:

  • блокирует ее
  • позволяет пользователю изменить данные
  • сохраняет ее потом

но лучше

  • прочесть ее
  • дать пользователю изменить данные
  • "блокировать" и сохранить ее

Предположим, как пример, такой сценарий:

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

Две потенциальные проблемы в данной ситуации:

1) Две конфликтующие транзакции пытаются обновить одну и ту же строку

Это случается очень редко и происходит в следующей последовательности событий (обратите внимание, касается только двух параллельных транзакций):

транзакция A транзакция B
НАЧАЛО  
  НАЧАЛО
... работа с UI ... ... работа с UI ...
ФИКСАЦИЯ  
  ФИКСАЦИЯ

GNUmed использует уровень транзакции, упорядочиваемость которого означает, что транзакция B даст сбой на шаге ФИКСАЦИЯ. Это стандартное поведение транзакций. Использование уровня упорядочиваемой транзакции означает, что значения "блокировки записи" упомянуты выше. GNUmed никогда действительно не принимает фактическую блокировку, так как семантика MVCC и транзакций позволяет необходимое поведение.

Обратите внимание, что поскольку GNUmed держит продолжительность обновленных транзакций на минимуме (через их запуск только точно вовремя), вероятность таких упорядочиваемых конфликтов очень мала.

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

2) Два пользовательских действия, обрамляя друг друга, пытаются обновить одну и ту же строку

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

сестра врач
читает строку (begin; ...; commit;)  
  читает строку (begin; ...; commit;)
изменяет строку в UI  
  изменяет строку в UI
  обновляет строку (begin; ...; commit;)
обновляет строку (begin; ...; commit;)  

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

Одним из решений этой проблемы параллелизма является оптимистическая блокировка. GNUmed использует стратегию столбца индикатора изменения ("маркирует источник уникальным идентификатором") для обнаружения периодических обновлений (google для "удобного прокси неизмененной строки – чтобы увидеть, если его системный столбец XMIN до сих пор тот же самый"). Индикатор обновления представляется в виде столбца XMIN PostgreSQL. XMIN - это ID транзакции базы данных, последней изменившей строку. Если строка изменяется, также изменяется XMIN. Это является основной особенностью архитектуры MVCC PostgreSQL и, таким образом, весьма маловероятно для потери (google о "семантике риска XMIN").

В client/pycommon/gmBusinessDBObject.py имеется базовый класс cBusinessDBObject, который реализует оптимистическую блокировку для всех его подклассов. Каждый экземпляр в базе данных представляет собой строку. Когда данные таблицы считываются из бэкэнда, запоминается соответствующий XMIN и значение первичного ключа. Позже, когда данные обновлены и записаны обратно в бэкэнд, класс использует следующую конструкцию в операторе WHERE его запроса на обновление:
update <table> set
   <column> = <value>
   ...
where
   <pk column> = <pk value of this row> and
   XMIN = <original XMIN value>

При условии, если запрос является синтаксически правильным, могут возникнуть три ситуации:

  • исходная строка была удалена другим писателем
    • ни XMIN, ни первичный ключ не будут соответствовать никакой строке, так что строка, которая определяется как конфликт параллелизма, не обновляется

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

  • исходная строка не затронута
    • как первичный ключ, так и XMIN найдут строку в запросе и разрешат обновление для выполнения, так что одна строка обновляется, и все в порядке

GNUmed нуждается в улучшении управления обнаруженных удалений и одновременных обновлений. В настоящее время он, по крайней мере, определяет их и молча отказывается перезаписывать существующие медицинские данные.
Topic revision: 18 Mar 2013, IvanLykov
 
Download.png
This site is powered by the TWiki collaboration platformCopyright © by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding Foswiki? Send feedback
Powered by Olark