Удаление в CDC: почему последняя версия строки может не существовать
Поток изменений базы уже подключён к витрине. Создания и обновления обрабатываются одинаково: берём последнюю строку по ключу. Через неделю выясняется, что удалённый клиент всё ещё присутствует в отчёте. Его последнее событие не содержало нового набора полей, поэтому загрузчик его пропустил. Так приём «оставить последнюю версию» превратился в сохранение последней существовавшей версии.
CDC передаёт изменения состояния, включая исчезновение строки. Удаление нельзя трактовать как сообщение без полезных данных. Для каждого ключа нужно сохранить порядок версий и применить тип операции. Если удаление пришло после обновления, текущего объекта больше нет. При этом информацию о последней версии часто приходится хранить дольше самого объекта, чтобы запоздавшее событие не вернуло его обратно.
Различим удаление и tombstone
У коннектора Debezium для PostgreSQL событие удаления содержит op=d и after=null. Дополнительно может отправляться tombstone: сообщение с тем же ключом и null в значении, используемое при compaction Kafka. Это разные сообщения с разными назначениями. Настройки коннектора и преобразований влияют на то, что дойдёт до потребителя; до написания обработчика посмотрите реальную запись вашего потока.
Нельзя проверять только after и игнорировать событие, когда after отсутствует. Сначала разберите тип операции и ключ. Для удаления ключ особенно важен: без него невозможно убрать нужную запись из витрины. Если источник меняет первичный ключ, проверьте отдельный сценарий его представления в потоке. Предположение «любое изменение приходит как новая строка под тем же ключом» здесь опасно.
Удалим объект и не дадим старому обновлению воскресить его
Учебный поток ниже уже приведён к общей форме с op, key, version и data. Версия монотонна для одного ключа; в реальном источнике надо выбрать подходящую позицию журнала и правило сравнения. Мы оставляем последнюю принятую версию даже после удаления. Событие версии 2, пришедшее после версии 3, отклоняется.
events = [
{"key": "A", "version": 1, "op": "upsert", "data": {"name": "Анна"}},
{"key": "A", "version": 3, "op": "delete", "data": None},
{"key": "A", "version": 2, "op": "upsert", "data": {"name": "Анна М."}},
{"key": "B", "version": 1, "op": "upsert", "data": {"name": "Борис"}},
]
state, last = {}, {}
for event in events:
key, version = event["key"], event["version"]
if version <= last.get(key, -1):
continue
if event["op"] == "delete":
state.pop(key, None)
elif event["op"] == "upsert":
state[key] = event["data"]
else:
raise ValueError("unknown operation")
last[key] = version
print("state", state, "versions", last)
assert "A" not in state and last["A"] == 3
assert set(state) == {"B"}Анны в текущем состоянии нет, но версия 3 сохранена. Это защищает от старого обновления в пределах принятой модели версий. Если удалить и state, и last, позднее сообщение получит вид нового ключа и восстановит уже удалённый объект. Срок хранения сведений об удалении должен учитывать доступный горизонт повторного чтения и восстановления.
Как применить правило в витрине
При записи в базу сравнение версии и изменение строки должны быть согласованной операцией. Иначе два обработчика прочитают старую версию и запишут результаты в обратном порядке. Повтор одного и того же события должен оставлять то же состояние. Для удаления полезно также хранить техническую запись с признаком is_deleted, если формат таблицы и требования обработки истории это допускают.
Текущая витрина и история изменений решают разные задачи. В текущей таблице удалённого клиента может не быть, в журнале событий запись об операции нужна для воспроизведения. Не удаляйте её только потому, что отчёт показывает активных клиентов. При работе с персональными данными отдельно согласуйте сроки и допустимый состав технической истории; ключ и полезная нагрузка имеют разную чувствительность.
В тестовый поток включите создание, обновление, удаление, повтор удаления, запоздавшее обновление и новое допустимое создание того же ключа. Последний случай особенно важен, если бизнес разрешает повторное использование идентификатора: версия должна оставаться сравнимой либо ключ должен включать поколение объекта. Счётчики принятых удалений и пропущенных старых событий помогут заметить ошибку адаптера.
При восстановлении начинайте с согласованного снимка и известной позиции журнала. Если взять снимок после удаления, а события начать читать до него, старое создание может временно вернуть объект. Порядок стыковки снимка и потока требует отдельного воспроизводимого сценария.
В курсе вы работаете с Kafka, ключами и схемами сообщений, потоковой обработкой Spark и хранением в lakehouse. Следующий шаг — перенести разобранное правило в end-to-end конвейер и проверить, что обновления и удаления сохраняют согласованное состояние таблицы после повторного запуска. Откройте программу платного курса «Data Engineering Fast‑Track: Kafka → Airflow → Spark».
Продолжить на практике
- Data Engineering Fast‑Track: Kafka → Airflow → Spark ↗
Практический fast-track по data engineering: за 4–6 недель собираем рабочий конвейер Kafka → Spark → Lakehouse под управлением Airflow. Разберём batch и streaming, окна и watermark, схемы и контракты. Мини‑проекты, Docker Compose, шаблоны DAG и пайплайнов. Курс подойдёт инженерам, аналитикам и тем, кто хочет освоить современные пайплайны данных с уверенным Python.
