TC$mC$ on klassinen ja kriittinen tietokantasuunnittelun ongelma: miten
yhdistC$C$ in-memory -nopeus ja I/O-operaatioiden hitaus ilman pullonkauloja.
Ratkaisu tC$hC$n on Write-Ahead Logging (WAL) yhdistettynC$ asynkroniseen
puskurointiin ja mikro-erC$ajoon (micro-batching).
Jotta pC$C$sC$ie (Chromessa DOM/UI ja Node.js:ssC$ event loop) pysyy tC$ysin
vapaana, meidC$n on irrotettava RAM-muistin pC$ivitys I/O-kirjoituksesta
erillisellC$ tuottaja-kuluttaja -arkkitehtuurilla (Producer-Consumer). TC$hC$n
tarkoitukseen lukottomat (lock-free) SPSC (Single-Producer, Single-Consumer)
rengaspuskurit (Ring Buffers) tai yksinkertaiset asynkroniset jonot ovat
erinomaisia.
TC$ssC$ on isomorfinen arkkitehtuuri, joka ratkaisee tC$mC$n molemmissa
ympC$ristC6issC$.
1. Yhteinen Ydin: Mutaatiotapahtumat (Mutation Events)
Kun rajapinta (PUT, POST, PATCH, DELETE) muuttaa dataa, ydinjC$rjestelmC$
tekee kaksi asiaa:
PC$ivittC$C$ vC$littC6mC$sti Map-objektin RAM-muistissa (synkroninen,
nanosekunteja).
Luo mutaatiosta kevyen tapahtumaobjektin ja tyC6ntC$C$ sen muistissa olevaan
asynkroniseen jonoon/puskuriin.
Rajapinta palauttaa vastauksen asiakkaalle heti kun data on RAM-muistissa ja
jonossa. Itse levylle/kantaan kirjoitus tapahtuu taustalla.
2. Node.js Toteutus: FS JSONL & Stream-puskurointi
Node.js-ympC$ristC6ssC$ tiedostojC$rjestelmC$n I/O voi blokata event looppia,
jos tiedostoja avataan/suljetaan jatkuvasti tai kirjoitetaan tavu kerrallaan.
Ratkaisu on pitC$C$ auki jatkuvaa WriteStream -yhteyttC$ ja kerC$tC$
yksittC$iset mutaatiot taustalla isompiin blokkeihin ennen levylle flussausta.
Jos taustalla on moderni tiedostojC$rjestelmC$ (jossa on esimerkiksi
lC$pinC$kyvC$ ZSTD-pakkaus kC$ytC6ssC$ datasektoreilla), rivipohjainen
JSONL-append on sille erittC$in luonnollinen ja nopea operaatio.
import fs from 'fs';
export class NodeWALAdapter {
constructor(logPath) {
this.logPath = logPath;
// Avataan jatkuva stream append-tilassa
this.stream = fs.createWriteStream(this.logPath, { flags: 'a' });
this.queue = [];
this.isFlushing = false;
}
// TC$mC$ kutsutaan synkronisesti heti kun RAM on pC$ivitetty
persist(collection, id, operation, data) {
const event = JSON.stringify({ t: Date.now(), op: operation, c:
collection, id, d: data }) + '
';
this.queue.push(event);
// HerC$tetC$C$n taustaprosessi, jos se nukkuu
if (!this.isFlushing) {
this.isFlushing = true;
setImmediate(() => this.flush());
} }
// Asynkroninen kuluttaja, joka ei blokkaa event looppia
async flush() {
if (this.queue.length === 0) {
this.isFlushing = false;
return;
}
// Mikro-batching: Napataan kaikki tC$hC$n mennessC$ kertyneet tapahtumat
const batch = this.queue.join('');
this.queue = []; // TyhjennetC$C$n jono uusia tapahtumia varten
// Kirjoitetaan koko erC$ kerralla
// stream.write on asynkroninen ja kC$yttC$C$ Node.js:n taustasC$ikeitC$
(libuv)
const canContinue = this.stream.write(batch);
if (!canContinue) {
// Backpressure-hallinta: Puskuri on tC$ynnC$, odotetaan
'drain'-tapahtumaa
this.stream.once('drain', () => this.flush());
} else {
// Jatketaan jonon purkamista seuraavalla event loopin kierroksella
setImmediate(() => this.flush());
} } }
3. Chrome/Selain Toteutus: Web Worker & IndexedDB
Selaimessa IndexedDB on asynkroninen, mutta transaktioiden luonti ja
serialisointi tapahtuu pC$C$sC$ikeessC$, mikC$ voi aiheuttaa UI-jankkia
(nykimistC$), jos dataa on paljon.
TC$ydellinen ratkaisu on siirtC$C$ tietokantaoperaatiot kokonaan Web
Workeriin. PC$C$sC$ie kommunikoi Workerin kanssa viestinvC$lityksellC$
(postMessage).
PC$C$sC$ie (UI/VC$ylC$):
export class BrowserWorkerAdapter {
constructor() {
// KC$ynnistetC$C$n taustasC$ie
this.worker = new Worker('storage-worker.js');
}
persist(collection, id, operation, data) {
// SiirretC$C$n serialisointi ja I/O pois pC$C$sC$ikeestC$.
// Structured Clone -algoritmi kopioi datan Workeriin salamannopeasti.
this.worker.postMessage({
type: 'MUTATION',
payload: { op: operation, c: collection, id, d: data } }); } }
Worker-sC$ie (storage-worker.js):
/ WorkerissC$ elC$vC$ IndexedDB-logiikka
let db;
/ ... (IndexedDB avausrutiinit)
/ Otetaan mutaatiot vastaan jonoon
const messageQueue = [];
let isProcessing = false;
self.onmessage = (event) => {
if (event.data.type === 'MUTATION') {
messageQueue.push(event.data.payload);
if (!isProcessing) processQueue();
}
};
async function processQueue() {
isProcessing = true;
// Avataan yksi IndexedDB-transaktio koko erC$lle suorituskyvyn
maksimoimiseksi
const tx = db.transaction('wal_store', 'readwrite');
const store = tx.objectStore('wal_store');
// SiirretC$C$n koko nykyinen jono IndexedDB:hen kerralla
const batch = messageQueue.splice(0, messageQueue.length);
for (const msg of batch) {
// Append-only tyylisesti lisC$tC$C$n uusi tietue (autoincrement ID)
store.put(msg);
}
tx.oncomplete = () => {
if (messageQueue.length > 0) {
processQueue(); // Puretaan seuraava erC$
} else {
isProcessing = false;
} }; }
Tiivistys ja Kompaktio (Snapshotting)
Koska WAL (JSONL tai IndexedDB log) on append-only, se kasvaa loputtomiin.
PitkC$ikC$isessC$ jC$rjestelmC$ssC$ tarvitaan kompaktioprosessi (Compaction):
Kun lokitiedosto saavuttaa tietyn koon (esim. 50 MB) tai aikarajan,
taustaprosessi aktivoituu.
JC$rjestelmC$ kirjoittaa nykyisen RAM-muistin tilan puhtaana AST-MD tiedostona
levylle.
Kun AST-MD -snapshot on tallennettu onnistuneesti, JSONL-loki tyhjennetC$C$n
ja append-only operaatiot jatkuvat uuteen, tyhjC$C$n lokiin.
TC$mC$ yhdistelmC$ takaa sen, ettC$ kC$ynnistyminen on nopeaa (luetaan
kompakti AST-MD snapshot, ja sen pC$C$lle ajetaan vain tuoreimmat
JSONL-muutokset), ja ajonaikainen suorituskyky pysyy C$C$rimmC$isenC$.