Download Game! Currently 78 players and visitors. Last logged in:GrizztRobinhoodJanoQumni

Blitzer's Blog >> 71953

Back to blogs index
Posted: 03 Sep 2026 14:18 [ permalink ]
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$.