CRDT множества — G-set, 2P-set, LWW-set, OR-set
CRDT-структуры для множеств. От простейшего G-set (только добавление) до OR-set (полноценный set с add/remove). Каждый следующий — компромисс между функциональностью и накладными расходами.
G-set (Grow-only set)
Только добавление. Merge = union.
class GSet {
constructor { this.items = new Set(); }
add(item) { this.items.add(item); }
merge(other) {
for (const x of other.items) this.items.add(x);
}
}
- + Простой, безконфликтный.
- - Не умеет remove.
2P-set (Two-phase set)
Два множества: added и removed. Удалённое нельзя вернуть.
class TwoPSet {
constructor {
this.added = new Set();
this.removed = new Set();
}
add(item) { this.added.add(item); }
remove(item) {
if (this.added.has(item)) this.removed.add(item);
}
has(item) {
return this.added.has(item) && !this.removed.has(item);
}
}
- Семантика add-wins при первой операции:
add → remove → addостаётся deleted. - + Можно удалять.
- - Удалённый навсегда удалён.
LWW-set (Last-Write-Wins set)
Add и remove с timestamps. Побеждает последняя по времени операция.
class LWWSet {
constructor {
this.added = new Map(); // item → timestamp
this.removed = new Map();
}
add(item) { this.added.set(item, Date.now()); }
remove(item) { this.removed.set(item, Date.now()); }
has(item) {
const addT = this.added.get(item) ?? -1;
const remT = this.removed.get(item) ?? -1;
return addT > remT;
}
}
- + Можно вернуть удалённое.
- - Зависит от синхронизации часов.
Date.now()в JS — миллисекунды, легко получить одинаковый timestamp.
OR-set (Observed-Remove set)
Каждое добавление получает уникальный tag. Remove удаляет конкретные tags. Параллельные add и remove обоих остаются → элемент в множестве.
class ORSet {
constructor { this.entries() = ; } // [{item, tag}]
add(item) { this.entries().push({ item, tag: uuid }); }
remove(item) {
this.entries() = this.entries().filter(e => e.item !== item);
}
}
- + Самая «обычная» семантика множества.
- - Накладные расходы на tags, нетривиальный GC удалённых.
PN-set
Композиция: count добавлений минус count удалений. Element-in-set если count > 0.
Сводная таблица
| Set | add | remove | re-add | накладные расходы |
|---|---|---|---|---|
| G-set | да | — | — | низкие |
| 2P-set | да | да (one-way) | — | средние |
| LWW-set | да | да | да (по timestamp) | средние (нужны часы) |
| OR-set | да | да | да | высокие (tags) |
| PN-set | да | да | да | средние |
Garbage collection
Главная боль CRDT-множеств: «множество из 1 элемента может занимать память на 2000 элементов».
В distributed-среде нельзя удалить tombstone, пока не убедился, что все реплики его видели. Это нетривиально без координатора.
Где использовать
- G-set — append-only логи, immutable списки.
- 2P-set — каталоги без re-add (банковские blacklists).
- LWW-set — простой shopping cart, лайки.
- OR-set — комментарии, теги, любые user-facing множества.