Как реализовать дедупликацию данных
Что проверяют
Понимаете ли вы, что дедупликация начинается с вопроса «что считать дублем», и знаете ли, на каком уровне её делать: в памяти, в базе или в потоке сообщений. Уточнение обычно такое: что будет при двух одновременных вставках или когда данных больше, чем влезает в память.
Как отвечать
Начните с ключа: полное совпадение объекта, бизнес-ключ (email, номер заказа) или нормализованное содержимое. Потом пройдите по уровням:
- в памяти:
distinct()иHashSetопираются наequalsиhashCode. Дедупликация по ключу с сохранением первого вхождения:toMap(Item::key, identity(), (a, b) -> a, LinkedHashMap::new). Для больших объектов храните в множестве не сам объект, а хэш ключа; - в базе: уникальный индекс плюс
INSERT ... ON CONFLICT DO NOTHINGв PostgreSQL илиMERGE. Уже накопленные дубли находят черезROW_NUMBER() OVER (PARTITION BY key ORDER BY created_at)и удаляют всё, где номер больше единицы; - в сообщениях: Kafka и большинство брокеров гарантируют доставку «хотя бы один раз», поэтому потребитель должен быть идемпотентным. Храните обработанные идентификаторы сообщений в той же транзакции, что и результат (паттерн inbox). Настройка
enable.idempotenceу продюсера убирает только дубли от его собственных ретраев; - на больших объёмах: внешняя сортировка или фильтр Блума (есть в Guava) как быстрый предварительный отсев. Он даёт ложные срабатывания, поэтому «возможно дубль» проверяют точно;
- почти одинаковые записи: нормализация (регистр, пробелы, формат телефона), а для текстов MinHash или SimHash.
Где спотыкаются
Делают «проверить, потом вставить» двумя запросами. При конкурентной записи оба потока видят пустоту и вставляют дубль. Надёжно работает только ограничение уникальности в самой базе.
Вызывают distinct() на объектах без переопределённых equals и hashCode и удивляются, что ничего не отфильтровалось.
Советуют INSERT IGNORE в MySQL, не упоминая, что он глушит не только конфликт ключа, но и другие ошибки, превращая их в предупреждения. Грязные данные тихо попадают в таблицу.