Lära LRU - När du överträffar kapacitet gör ditt system bättre (Svenska (Swedish))

Lära LRU - När du överträffar kapacitet gör ditt system bättre

Tuesday, 09 December 2025

//

18 minute read

De flesta system försämras vid överbelastning. Minnet fylls upp, frågor saktar ner, användare klagar, servrar kraschar.

Några ovanliga får bättre.

Den här artikeln visar hur en LRU-baserat beteendeminne blir självoptimerande när det träffar kapacitet - och hur detta mönster driver inlärningssystemet i min Botdetekteringsmotor. Detta är också den minsta möjliga versionen av DiSE arkitektur - kontrollerad utveckling genom resurstryck.

Om du har läst min artikel om CQRS och HändelseförslitningDu känner igen några mönster, men det här är CQRS som är avskalad till benet - ingen händelsebutik, inga projektioner, ingen Marten, bara en minnescache, en bakgrundsarbetare och SQLite.

Det grundläggande idea - Beteendeminnet om en budget

Innan vi dyker in, låt mig definiera en term som jag kommer att använda hela: namnteckning. En signatur är någon stabil nyckel som representerar ett beteendemönster - en hash av IP + User-Agent, ett fingeravtryck av huvudkombinationer, en detektor klassificering av "den här begäran ser ut som X". cache lagrar dessa signaturer tillsammans med inlärda vikter som utvecklas över tiden.

Tänk om du kunde bygga ett system som:

  • Svar på mindre än 1 ms
  • Blockera aldrig på databasskriver
  • Självoptimering under tryck
  • Glömmer vad som inte spelar någon roll
  • Kommer ihåg vad som gör

Det är precis vad IMemoryCache med glidande utgång ger dig - om du förstår vad du bygger.

Vad en LRU Cache faktiskt gör

LRU (minst nyligen använda) caches vräk poster som inte har kommits åt nyligen. När cache fylls upp, de kallaste posterna kastas ut för att göra plats för heta.

De flesta utvecklare ser detta som en begränsning. "Åh nej, min cache är full, data går förlorad!"

Men för beteendesystem är detta ett drag.

// From WeightStore.cs - the bounded memory window
_cache = new MemoryCache(new MemoryCacheOptions
{
    SizeLimit = _cacheSize,           // e.g., 1000 entries
    CompactionPercentage = 0.25       // Remove 25% when limit reached
});

För detta ändamål ska följande gälla: SizeLimit Det är inte bara en minnesbegränsning. Urvalstryck. – Det avgör hur mycket systemet "kommer ihåg" och tvingar det att fokusera på vad som är viktigt.

Att glida ut - Klockan som glömmer

Kombinerat med glidande utgång, får du automatiskt glömma:

// From WeightStore.cs:254-259
private MemoryCacheEntryOptions GetCacheEntryOptions()
{
    return new MemoryCacheEntryOptions()
        .SetSlidingExpiration(_slidingExpiration)  // 30 minutes
        .SetSize(1);  // Each entry counts as 1 toward size limit
}

Om en signatur inte nås inom 30 minuter vräks den inte för att den är fel, för den är inte längre relevant.

Detta skapar naturliga glömska:

  • IP- data blir omplacerade
  • Botarna roterar signaturer
  • Trafikmönster förskjutning
  • Gårdagens attacker är inte dagens.

Statiska blocklists blir gamla. Sliding utgång håller minnet färskt.

ImoryCache mönster - liten CQRS utan att säga CQRS

Här är mönstret som får det att fungera. SqliteWeightStore klass:

/// <summary>
///     SQLite implementation of the weight store with sliding expiration memory cache.
///     Uses a CQRS-style pattern with write-behind:
///     - Reads: Hit memory cache first (fast path), fall back to SQLite on miss
///     - Writes: Update cache immediately, queue SQLite writes for background flush
///     Sliding expiration provides automatic LRU-like eviction behavior.
/// </summary>

public class SqliteWeightStore : IWeightStore, IAsyncDisposable
{
    // Memory cache with sliding expiration - auto-evicts least recently used entries
    private readonly MemoryCache _cache;

    // Write-behind queue for batched SQLite persistence
    private readonly ConcurrentDictionary<string, PendingWrite> _pendingWrites = new();
    private readonly Timer _flushTimer;
    private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500);

Det här är Informella CQRS - men utan den tråkiga cacheinvalideringen. Istället för att ogiltigförklara cacheposter efter att de skrivits, cache är skrivmodellen. SQLite är bara den hållbara liggaren.

flowchart LR
    subgraph Cache["In-Memory Behaviour Store"]
        A[Hot Signatures] --- B[Sliding Expiry]
    end
    subgraph DB["SQLite Ledger"]
        C[(Durable Write-Behind)]
    end

    A --Periodic Flush--> C
    B --Eviction--> D[Forgotten]

    style Cache fill:none,stroke:#10b981,stroke-width:2px
    style DB fill:none,stroke:#6366f1,stroke-width:2px

Nyckelinsikt: läser och skriver går till minnet. Databasen är så småningom konsekvent - och det är bra.

Läsvägen - cache först, alltid

När en detektor behöver en inlärd vikt träffar den cache:

// From WeightStore.cs:421-472
public async Task<double> GetWeightAsync(
    string signatureType,
    string signature,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Check cache first (fast path - no DB access)
    if (_cache.TryGetValue(key, out LearnedWeight? cached) && cached != null)
    {
        _metrics?.RecordCacheHit(signatureType);
        return cached.Weight * cached.Confidence;
    }

    _metrics?.RecordCacheMiss(signatureType);

    // Cache miss - load from DB
    await EnsureInitializedAsync(ct);

    await using var conn = new SqliteConnection(_connectionString);
    await conn.OpenAsync(ct);

    var sql = $@"
        SELECT weight, confidence, observation_count, first_seen, last_seen
        FROM {TableName}
        WHERE signature_type = @type AND signature = @sig
    ";

    // ... execute query ...

    if (await reader.ReadAsync(ct))
    {
        // Cache the result for future reads
        var learnedWeight = new LearnedWeight { /* ... */ };
        _cache.Set(key, learnedWeight, GetCacheEntryOptions());

        return weight * confidence;
    }

    return 0.0;  // No learned weight exists
}

Varma stigar aldrig träffa databasen. Cachen är källan till sanningen för läsningar. SQLite är bara backup lagring.

Skrivvägen - cache omedelbart, beständig senare

När systemet lär sig något nytt, uppdaterar det cachen omedelbart och köar databasen skriver:

// From WeightStore.cs:551-582
public Task UpdateWeightAsync(
    string signatureType,
    string signature,
    double weight,
    double confidence,
    int observationCount,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Update cache immediately (source of truth for reads)
    var learnedWeight = new LearnedWeight
    {
        SignatureType = signatureType,
        Signature = signature,
        Weight = weight,
        Confidence = confidence,
        ObservationCount = observationCount,
        FirstSeen = DateTimeOffset.UtcNow,
        LastSeen = DateTimeOffset.UtcNow
    };
    _cache.Set(key, learnedWeight, GetCacheEntryOptions());

    // Queue for async SQLite persistence (write-behind)
    QueueWrite(signatureType, signature, weight, confidence, observationCount);

    return Task.CompletedTask;
}

Meddelande: UpdateWeightAsync returer Task.CompletedTask Skrivet är köat, inte utfört.

  • Undermillionär skrivlatens
  • Ingen blockering på I/O
  • Skriver slås samman (sista skrivvinster)

Bakgrunden Flusher - 500ms av tråkig magi

Varje 500 m, väntande skriver spolas till SQLite i en enda sats:

// From WeightStore.cs:274-357
public async Task FlushPendingWritesAsync(CancellationToken ct = default)
{
    if (_pendingWrites.IsEmpty) return;

    // Only one flush at a time
    if (!await _flushLock.WaitAsync(0, ct)) return;

    try
    {
        await EnsureInitializedAsync(ct);

        // Snapshot and clear pending writes atomically
        var writes = new List<PendingWrite>();
        foreach (var key in _pendingWrites.Keys.ToList())
        {
            if (_pendingWrites.TryRemove(key, out var write))
            {
                writes.Add(write);
            }
        }

        if (writes.Count == 0) return;

        await using var conn = new SqliteConnection(_connectionString);
        await conn.OpenAsync(ct);
        await using var transaction = await conn.BeginTransactionAsync(ct);

        try
        {
            var sql = $@"
                INSERT INTO {TableName}
                    (signature_type, signature, weight, confidence,
                     observation_count, first_seen, last_seen)
                VALUES (@type, @sig, @weight, @conf, @count, @now, @now)
                ON CONFLICT(signature_type, signature) DO UPDATE SET
                    weight = @weight,
                    confidence = @conf,
                    observation_count = @count,
                    last_seen = @now
            ";

            foreach (var write in writes)
            {
                await using var cmd = new SqliteCommand(sql, conn, transaction);
                // ... add parameters and execute ...
            }

            await transaction.CommitAsync(ct);
            _logger.LogDebug("Flushed {Count} pending writes in {Duration:F1}ms",
                writes.Count, sw.ElapsedMilliseconds);
        }
        catch
        {
            await transaction.RollbackAsync(ct);
            throw;
        }
    }
    finally
    {
        _flushLock.Release();
    }
}

Det här är händelse-sourcing-ljus. Du får:

  • Batched skriver (effektiv I/O)
  • Transaktionskonsistens
  • Kolade uppdateringar (om samma signatur uppdateras 10 gånger på 500 ms, skrivs endast det slutliga värdet)
  • SQLite är helt nöjd med detta tillträde mönster

EMA Uppdateringar - Lärande med exponentiella glidande medelvärden

När en ny observation kommer, använder systemet exponentiellt rörliga medelvärden för att uppdatera vikter:

// From WeightStore.cs:584-635
public Task RecordObservationAsync(
    string signatureType,
    string signature,
    bool wasBot,
    double detectionConfidence,
    CancellationToken ct = default)
{
    var key = CacheKey(signatureType, signature);

    // Calculate new weight using EMA in memory
    var alpha = 0.1;  // Learning rate
    var weightDelta = wasBot ? detectionConfidence : -detectionConfidence;

    double newWeight;
    double newConfidence;
    int newObservationCount;

    if (_cache.TryGetValue(key, out LearnedWeight? existing) && existing != null)
    {
        // Apply EMA: new_weight = old_weight * (1-α) + delta * α
        newWeight = existing.Weight * (1 - alpha) + weightDelta * alpha;
        newConfidence = Math.Min(1.0, existing.Confidence + detectionConfidence * 0.01);
        newObservationCount = existing.ObservationCount + 1;
    }
    else
    {
        // First observation
        newWeight = weightDelta;
        newConfidence = detectionConfidence;
        newObservationCount = 1;
    }

    // Update cache immediately
    var learnedWeight = new LearnedWeight
    {
        SignatureType = signatureType,
        Signature = signature,
        Weight = newWeight,
        Confidence = newConfidence,
        ObservationCount = newObservationCount,
        FirstSeen = existing?.FirstSeen ?? DateTimeOffset.UtcNow,
        LastSeen = DateTimeOffset.UtcNow
    };
    _cache.Set(key, learnedWeight, GetCacheEntryOptions());

    // Queue for persistence
    QueueWrite(signatureType, signature, newWeight, newConfidence, newObservationCount);

    return Task.CompletedTask;
}

EMA-formeln underlättar inlärningen: new_weight = old_weight × (1 - α) + new_value × α

Med α = 0,1:

  • Nya bevis bidrar med 10 %
  • Historiska bevis bidrar med 90 %
  • Detta förhindrar vilda gungor från enstaka observationer

Varför överflödet gör systemet Bättre

Här är nyckelinsikten som de flesta saknar.

När cachen fylls upp:

  • Lågfrekventa signaturer faller ut
  • Endast heta (ofta tillgängliga) signaturer stannar i minnet
  • Databasen släpar efter med en spolcykel - och det är bra
  • Systemet fokusering under tryck

Tänk på det: om 50.000 unika signaturer träffar din bot detektor, men du bara har minne för 10.000, vilka signaturer materia?

De hetaste 10 000 - som normalt utgör 99 % av den faktiska trafiken.

I produktionen ser jag ungefär 40.000 engångssignaturer per dag (skrapor som försöker en gång, slumpmässiga sonder, legitima användare som aldrig återvänder) och kanske 5-10 000 som återkommer hela tiden. De 5-10 000 är där 99% av riskerna lever. Långstjärtssignaturer? Buller. Att vräka dem skadar inte detektion noggrannhet - det kan till och med förbättra det genom att minska falska positiva från lågt förtroende mönster.

flowchart TB
    subgraph Input["50,000 Unique Signatures"]
        Hot[Hot Signatures\n~10,000]
        Cold[Cold Signatures\n~40,000]
    end

    subgraph Cache["Bounded Cache (10,000)"]
        Kept[Kept in Memory]
    end

    subgraph Evicted["Evicted"]
        Lost[Forgotten\nNoise Traffic]
    end

    Hot --> Kept
    Cold --> Lost

    style Hot fill:none,stroke:#10b981,stroke-width:2px
    style Cold fill:none,stroke:#94a3b8,stroke-width:2px
    style Kept fill:none,stroke:#10b981,stroke-width:2px
    style Lost fill:none,stroke:#ef4444,stroke-width:2px

Överflöde vässar beteendeminnet. Systemet är självoptimerat.

När överflödet inte hjälper

Det finns kantfall där LRU pressar dig:

  • Cache för liten: Om din cache bara innehåller 100 poster men du har 1000 verkligt viktiga signaturer, kommer du churn ständigt och förlora användbara mönster innan de ackumuleras tillräckligt bevis. Storlek din cache för att bekvämt hålla din "hot set".

  • Enhetlig trafik: Om du kör på ett litet internt system där nästan allt är "hett" (få unika signaturer, alla återkommande), överflöd ger dig mindre nytta. Urvalstrycket har inget att välja mot.

  • Problem med kallstart: Ett nyutplacerat system har inga inlärda vikter. Allt är lika kallt. De första timmarna kommer att ha högre falska positiva hastigheter tills de heta signaturerna etablerar sig.

Mönstret fungerar bäst när du har hög kardinalitet med makt-lag distribution - Många unika signaturer, men en liten undergrupp som dominerar trafiken.

Behålla cache och databas i synkronisering - Tag-based invalidation

Gömman är källan till sanningen för läsning, men databasen är den hållbara liggaren. Vad händer när de driver?

Avveckling av synkronisering

När databasen vikter förfall, måste cache följa. DecayOldWeightsAsync metod hanterar båda:

// From WeightStore.cs:725-766
public async Task DecayOldWeightsAsync(TimeSpan maxAge, double decayFactor, CancellationToken ct = default)
{
    await EnsureInitializedAsync(ct);

    await using var conn = new SqliteConnection(_connectionString);
    await conn.OpenAsync(ct);

    var cutoff = DateTimeOffset.UtcNow.Subtract(maxAge).ToString("O");

    // Decay old weights in the database
    var sql = $@"
        UPDATE {TableName}
        SET weight = weight * @decay,
            confidence = confidence * @decay
        WHERE last_seen < @cutoff
    ";

    await using var cmd = new SqliteCommand(sql, conn);
    cmd.Parameters.AddWithValue("@decay", decayFactor);
    cmd.Parameters.AddWithValue("@cutoff", cutoff);

    var updated = await cmd.ExecuteNonQueryAsync(ct);

    // Delete weights that have decayed below threshold
    var deleteSql = $@"
        DELETE FROM {TableName}
        WHERE confidence < 0.01 OR (ABS(weight) < 0.01 AND observation_count < 5)
    ";

    await using var deleteCmd = new SqliteCommand(deleteSql, conn);
    var deleted = await deleteCmd.ExecuteNonQueryAsync(ct);

    if (updated > 0 || deleted > 0)
    {
        _logger.LogInformation(
            "Weight decay: {Updated} decayed, {Deleted} deleted",
            updated, deleted);

        // Compact cache to remove stale entries
        _cache.Compact(0.25);
    }
}

Efter sönderfallande databas register, ringer vi _cache.Compact(0.25) - Detta tvingar MemoryCache att vräka 25% av sina poster, prioritera den senast använda. Nästa läsning kommer att ladda nya värden från databasen.

Taggbaserad vräkning

Ibland måste du ogiltigförklara en hel kategori av cachade poster - t.ex. vid omskolning av en detektor eller när externa data ändras:

// From WeightStore.cs:768-777
/// <summary>
///     Evicts all cached entries for a specific signature type (tag-based eviction).
/// </summary>

public void EvictByTag(string signatureType)
{
    // MemoryCache doesn't natively support tag-based eviction, but we can compact
    // For now, just compact - sliding expiration will handle stale entries
    _cache.Compact(0.1);
    _logger.LogDebug("Compacted cache for signature type: {SignatureType}", signatureType);
}

.NET:s MemoryCache inte har infödda tagg-baserad vräkning som Redis, men komprimering uppnår samma effekt: tvinga ut gamla poster, låt läser återbefolka från databasen.

Synkroniseringsstrategin

Den viktigaste insikten är att perfekt synkning är inte nödvändigt. Systemet tolererar drift eftersom:

  1. Cache missar att ladda om från DB - om en post är vräkt, nästa läsa hämtar färsk data
  2. Glidande utgångshandtag föråldrande - poster inte nås inom 30 minuter auto-evct
  3. Kompakteringskrafter förnyelse - periodisk komprimering skjuter ut gamla poster
  4. Uppdateringar av skrivbakom sammansmältningen - flera snabba uppdateringar blir en DB skriva

Det här är den slutliga konsekvensen som görs rätt. Kachen förblir "nära nog" till databasen utan att kräva komplex invalideringslogik.

flowchart TB
    subgraph Sync["Cache-Database Synchronisation"]
        D[Database Decay] --> C[Cache Compact]
        E[Tag Eviction] --> C
        S[Sliding Expiration] --> M[Cache Miss]
        M --> R[Reload from DB]
    end

    style Sync fill:none,stroke:#6366f1,stroke-width:2px

Inget cacheinvalideringshelvete. Ingen komplex pub/sub. Bara komprimering och naturligt utgångsdatum.

Gå djupare: Reputationssystemet

Anmärkning: Om du bara ville LRU + skriva-bakom mönster, kan du sluta här. Resten av denna artikel visar hur jag tillämpar samma idéer på fullt mönster rykte - tillstånd maskiner, hysteres, och tid förfall. Det är "extra mile" för dem som bygger adaptiva system.

Hysteresis och dekay

För mönster rykte (följa om en signatur är bot eller mänsklig över tid), samma principer gäller men med ytterligare sofistikering:

// From PatternReputation.cs:42-108
public record PatternReputation
{
    public required string PatternId { get; init; }
    public required string PatternType { get; init; }
    public required string Pattern { get; init; }

    /// <summary>Current bot probability [0,1]. 0 = human, 1 = bot, 0.5 = neutral</summary>

    public double BotScore { get; init; } = 0.5;

    /// <summary>Effective sample count - decays over time, increases with observations</summary>

    public double Support { get; init; } = 0;

    /// <summary>Current reputation state - determines fast-path behavior</summary>

    public ReputationState State { get; init; } = ReputationState.Neutral;

    // Computed properties
    public double Confidence => Math.Min(1.0, Support / 100.0);

    public bool CanTriggerFastAbort =>
        State is ReputationState.ConfirmedBad or ReputationState.ManuallyBlocked;

    public bool CanTriggerFastAllow =>
        State is ReputationState.ConfirmedGood or ReputationState.ManuallyAllowed;
}

Tillståndsövergångar med hysteres

Mönster inte vända direkt från Neutral till BekräftadBad. Det finns hysteres för att förhindra fladdrande:

// From PatternReputation.cs:367-421 - simplified
public PatternReputation EvaluateStateChange(PatternReputation reputation)
{
    if (reputation.IsManual)
        return reputation;

    var newState = reputation.State;
    var score = reputation.BotScore;
    var support = reputation.Support;

    switch (reputation.State)
    {
        case ReputationState.Neutral:
            // Can promote to Suspect or ConfirmedGood
            if (score >= 0.6 && support >= 10)
                newState = ReputationState.Suspect;
            else if (score <= 0.1 && support >= 100)
                newState = ReputationState.ConfirmedGood;
            break;

        case ReputationState.Suspect:
            // Can promote to ConfirmedBad or demote to Neutral
            if (score >= 0.9 && support >= 50)
                newState = ReputationState.ConfirmedBad;
            else if (score <= 0.4 || support < 10)
                newState = ReputationState.Neutral;
            break;

        case ReputationState.ConfirmedBad:
            // Can demote to Suspect (requires MORE evidence to forgive)
            if (score <= 0.7 && support >= 100)
                newState = ReputationState.Suspect;
            break;
    }

    // ... log state change and return ...
}

Observera asymmetrin: det är lättare att bli blockerad än oblockerad. ConfirmedBad → Suspect kräver 100 stöd, medan Neutral → Suspect Det är svårare att förlåta än att misstänka.

Tidsfördröjning - avslöjande glömska

När mönster tystnar förfaller de mot neutralitet:

// From PatternReputation.cs:334-361
public PatternReputation ApplyTimeDecay(PatternReputation reputation)
{
    if (reputation.IsManual)
        return reputation;

    var hoursSinceLastSeen = (DateTimeOffset.UtcNow - reputation.LastSeen).TotalHours;

    if (hoursSinceLastSeen < 1)
        return reputation;  // Too recent to decay

    // Score decay toward prior (0.5 = neutral)
    // new_score = old_score + (prior - old_score) × (1 - e^(-Δt/τ))
    var scoreDecayFactor = 1 - Math.Exp(-hoursSinceLastSeen / _options.ScoreDecayTauHours);
    var newScore = reputation.BotScore + (0.5 - reputation.BotScore) * scoreDecayFactor;

    // Support decay
    // new_support = old_support × e^(-Δt/τ)
    var supportDecayFactor = Math.Exp(-hoursSinceLastSeen / _options.SupportDecayTauHours);
    var newSupport = reputation.Support * supportDecayFactor;

    return reputation with
    {
        BotScore = Math.Clamp(newScore, 0, 1),
        Support = newSupport
    };
}

Förvald tid konstanter:

  • Score förfall τ: 168 timmar (7 dagar) - poäng flytta 63% mot neutral efter en vecka av inaktivitet
  • Stöd för nedbrytning τ: 336 timmar (14 dagar) - självförtroende sjunker 63% efter två veckor

Detta innebär en bekräftad dålig IP som går tyst i en månad kommer så småningom att falla tillbaka till Neutral. Inte för att det reformeras - eftersom dess bevis blev gamla.

Servicen för bakgrundsunderhåll

och ReputationMaintenanceService utför tre periodiska uppgifter:

// From ReputationMaintenanceService.cs:48-129
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation("Reputation maintenance service starting");

    // Load persisted reputations on startup
    await _cache.LoadAsync(stoppingToken);

    var decayInterval = TimeSpan.FromMinutes(60);    // Hourly decay sweep
    var gcInterval = TimeSpan.FromHours(24);          // Daily garbage collection
    var persistInterval = TimeSpan.FromMinutes(5);   // Persist every 5 minutes

    var lastDecay = DateTimeOffset.UtcNow;
    var lastGc = DateTimeOffset.UtcNow;
    var lastPersist = DateTimeOffset.UtcNow;

    while (!stoppingToken.IsCancellationRequested)
    {
        await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        var now = DateTimeOffset.UtcNow;

        // Decay sweep: push stale scores toward neutral
        if (now - lastDecay >= decayInterval)
        {
            await _cache.DecaySweepAsync(stoppingToken);
            lastDecay = now;
        }

        // Garbage collection: remove old neutral patterns
        if (now - lastGc >= gcInterval)
        {
            await _cache.GarbageCollectAsync(stoppingToken);
            lastGc = now;

            var stats = _cache.GetStats();
            _logger.LogInformation(
                "Reputation stats: {Total} patterns, {Bad} bad, {Suspect} suspect",
                stats.TotalPatterns, stats.ConfirmedBadCount, stats.SuspectCount);
        }

        // Persistence: save to SQLite
        if (now - lastPersist >= persistInterval)
        {
            await _cache.PersistAsync(stoppingToken);
            lastPersist = now;
        }
    }

    // Final persist on shutdown
    await _cache.PersistAsync(CancellationToken.None);
}

Sopuppsamlaren tar bort mönster som är:

  • 90+ dagar gamla
  • Stöd ≤ 1,0
  • I neutralt tillstånd

Detta hindrar den in-minne ordboken från att växa fritt samtidigt bevara värdefulla lärda mönster.

SQLite är inte ett skämt - Det är perfekt här

Många utvecklare sträcker sig reflexivt efter PostgreSQL eller Redis. Men för detta mönster är SQLite idealisk:

  1. Skriv-bakom eliminerar flaskhalsar - SQLites begränsning av single-writer spelar ingen roll när man packar ihop texter.
  2. Lokal lagring - ingen nätverkslatens, inga anslutningspooler
  3. Nollinställning - bara en filsökväg
  4. Perfekt för kantutplacering - körs på en Raspberry Pi
  5. Bärbar - databasen är bara en fil du kan kopiera runt

Schemat är minimalt:

CREATE TABLE IF NOT EXISTS learned_weights (
    signature_type TEXT NOT NULL,
    signature TEXT NOT NULL,
    weight REAL NOT NULL,
    confidence REAL NOT NULL,
    observation_count INTEGER NOT NULL DEFAULT 1,
    first_seen TEXT NOT NULL,
    last_seen TEXT NOT NULL,
    PRIMARY KEY (signature_type, signature)
);

CREATE INDEX IF NOT EXISTS idx_signature_type ON learned_weights(signature_type);
CREATE INDEX IF NOT EXISTS idx_confidence ON learned_weights(confidence);
CREATE INDEX IF NOT EXISTS idx_last_seen ON learned_weights(last_seen);

Om du behöver större skala, byt till PostgreSQL. Om du behöver HA eller replikering, byt till Redis eller en distribuerad cache. Arkitekturen ändrar inte - bara anslutningssträngen. SQLite är standardvärdet för utplacering av kanter, inte en religion.

Den DiSE Tie-in - misslyckande som evolution

Om SYFTE är den fulla evolutionsmotorn, detta cachemönster är mitokondrierna - den minsta biten som fortfarande beter sig som evolution under tvång.

Detta mönster implementerar DiSE principer på den mest minimala nivån:

  • Resursbegränsningar → Valtryck
  • LRU-avhysning → Naturligt urval (överlevare är bäst)
  • Tidsförfall → Glömma möjliggör anpassning
  • Uppdateringar av EMA → Mutation genom observation
  • Hysteres → Stabilitet genom motstånd mot förändring

En fullständig cache är inget misslyckande. Utvecklingstryck. – Systemet är självoptimerat: varma signaturer stannar kvar, kalla signaturer vräks och beteendeminnet konvergerar mot det som faktiskt betyder något.

Ingen ML-utbildning, inga externa modeller, bara arkitektur som beter sig som ett levande system.

Slutsats - enkla strukturer, ett framträdande beteende

Hela mönstret består av:

  1. Cache är sanningens källa för läsningar - tillträde under millisekund
  2. Skriver uppdateringscache omedelbart, fortsätter senare - ingen blockering av I/O
  3. Slidande utgång ger automatisk LRU - inbyggd i .NET
  4. Gränsad storlek skapar urvalstryck - spillvässar fokus
  5. Bakgrundsspolning håller SQLite i synk - eventuell konsekvens är bra
  6. Tidsförfall gör det möjligt att glömma - gamla bevis försvinner

Din lilla beteendebutik agerar mer som ett levande system än CRUD. Den minns vad som betyder något, glömmer det som inte gör det och blir bättre under press.

Minimal arkitektur → framträdande korrekthet. Överflöde → bättre fokus. Tryck → stabilitet.

Om du vill se detta i handling, kolla in mestadels lucid.botdetektion - och DiSE arkitekturserie för den djupare filosofin.

Länkar

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.