Ваш дельта-поток теряет поздние коммиты: постройте надёжный курсор
Одна строка пришла поздно, потому что водяной знак не был границей коммита. Пройдите сбой, затем определите охват, границу, хранение и безопасное возобновление.
Вот сбой, который я видел дважды, в двух разных движках. Экспорт запускается в 10:00 и записывает наибольшую увиденную отметку времени последнего изменения. Одна транзакция, начавшаяся в 09:58, всё ещё открыта, фиксируется в 10:03 и несёт отметку времени с момента своего начала. Следующий экспорт запрашивает только строки новее 10:00 и никогда её не видит. Размер передачи был отличным, а данные — неверными.
Пройдите экспорт, потерявший строку
Следите за порядком событий, а не за SQL. Приложение записывает время изменения строки при начале транзакции, а не при коммите. Экспортёр читает водяной знак по настенным часам, пока транзакция ещё открыта. Затем транзакция фиксируется со временем изменения старше сохранённого водяного знака. Следующая партия потребителя начинается после водяного знака, поэтому строка остаётся за курсором и больше никогда не будет выбрана.
Ничто не пишет ошибку, потому что ничто не отказало. Именно это делает такой класс ошибок дорогим: дельта-поток быстр, счётчики строк выглядят правдоподобно, а пропавшая строка всплывает недели спустя как заявка на сверку, которую никто не может воспроизвести.
У порядковых номеров та же форма дефекта по другой причине. Монотонный ID сообщает порядок выделения значений, а не порядок фиксации транзакций. Транзакция, взявшая низкий номер, может зафиксироваться после взявшей высокий, поэтому курсор, основанный на порядке выделения, тоже может оставить дыру.
Пять обещаний, которые поток должен дать
Прежде чем выбирать транспорт, попросите у системы-источника договор из пяти пунктов. Если не хватает любого пункта, у вас эвристика, а не поток изменений.
- Охват: вставки, обновления и удаления, каждое с устойчивым ключом, нужным для применения.
- Граница: что означает значение курсора и что содержит согласованная партия.
- Согласованность: как читаются связанные таблицы, не смешивая несовместимые состояния источника.
- Хранение: сколько времени отключённый потребитель может продолжать со своей сохранённой позиции.
- Переинициализация: поддерживаемый полный снимок на случай, когда эта позиция больше не действительна.
Поддерживаемый механизм может удовлетворить все пять. Документация SQL Server по отслеживанию изменений — один конкретный опубликованный пример: она определяет минимальную допустимую версию, записывает удаления наряду с изменениями и описывает транзакцию снимка, охватывающую проверку, захват версии и связанные запросы изменений. Это пример того, что требовать от источника, а не синтаксис для вставки в другой движок.
Проектируйте контрольную точку как часть конвейера
Сохранённая позиция потребителя — это настоящая машина состояний, поэтому так её и опишите. Курсор может продвигаться только через рабочий процесс, который уже применил партию и зафиксировал целевую транзакцию.
cursor = durable_checkpoint() # last fully applied position
batch = extract(cursor, upper_bound) # one consistent batch
apply(batch) # idempotent at the target
commit()
checkpoint(batch.upper_bound) # only now
Если доставка срывается на полпути, сверяйте ту же партию, а не изобретайте новую позицию источника. Повторное извлечение с более свежей верхней границей — это путь, которым частичное применение превращается в постоянную дыру. Зеркальное правило действует и с другой стороны: идемпотентное применение на цели предотвращает дубликаты и ничего не делает для изменения, которого извлечение никогда не видело.
Удаления требуют записей, а не выводов
Сравнение ещё существующих строк не может сказать потребителю, какие ключи исчезли. Источник, который никогда не записывает удаления, оставляет два несовершенных варианта: держать историю ключей достаточно долго для diff или запускать явную полную сверку по расписанию. Оба — проектные решения с ценой, и ни одного не существует, пока кто-то им не владеет.
Хранение — такое же решение. У каждого поддерживаемого потока изменений есть окно; укажите его в договоре и предупреждайте, когда контрольная точка потребителя приближается к нему. Потребитель, проснувшийся после двухнедельного перерыва, должен прочитать ясную ошибку «требуется переинициализация», а не молча получить частичную партию.
Затем измерьте, стоила ли дельта усилий
Возврат к сравнению каждого ключа и хеша иногда правилен, и стоит знать, сколько это стоит. Вы по-прежнему передаёте информацию о каждой строке, хеши не дают согласованного снимка по таблицам, а схема хешей приносит свои вопросы о представлении и коллизиях. Меньше байтов с проверяемой границей — это цель; меньше байтов, которые молча теряют зафиксированное изменение, — вовсе не дельта.
Столбец последнего изменения — это подсказка. Курсор, переживающий поздний коммит, удаление и долгий перерыв, — это договор.