Back to "Att lära sig LRUs - När överskott gör ditt system bättre"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Architecture ASP.NET Bot Detection Caching CQRS Systems Design

Att lära sig LRUs - När överskott gör ditt system bättre

Tuesday, 09 December 2025

De flesta system bryts ned när de är överbelade. minnet fylls upp , sökningar saktar ner

Några ovanliga får bättre.

Detta artikel visar hur en LRU-baserade beteendemässig minne blir självoptimerande när det når kapaciteten - och hur detta mönster driver lärandesystemet i mitt bot-detektor. Detta är också den minsta versionen av DiSE-arkitekturen - styrd evolution genom resurstryck

Om du' har läst min artikel på CQRS och Event Sourcing, ni ' kommer att känna igen några av mönsterna här . Men det här är CQRS som är utplånat till benet. ♫ - inga händelserlära, ,, inga projiktioner, M SK5, inget Marten, ., bara ett minneslapp, ,, en bakgrundsarbetare, MSC8, och SQLite,

Den grundläggande idén - Behavioural Memory on a Budget

Innan vi dyker in i handtekening. En signering är vilken stabil nyckel som representerar ett beteendemönster - en hasch av IP

Tänk om du kunde bygga ett system som

  • Svar under 1ms
  • Never blocks on database writes
  • Self-optimerar under tryck
  • glömmer vad som inte spelar någon roll
  • Kommer ihåg vad som händer

Det är precis vad IMemoryCache med glittrande utgång ger dig - om du förstår vad du ska bygga

Vad en LRU-kas gör egentligen

LRU ( minst nyligen användad ) kaffern tar bort inträg som inte har varit öppnat ' inte blivit tillträdda nyligen M SK3 När kasken fyller upp sig MSC4 så släpper de kallaste inträgen ut för att göra plats för de varma

De flesta utvecklare ser detta som en begränsning " Åh nej , min dataskiva är full

Men för beteendemässiga system är detta en egenskap

// 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
});

Det SizeLimit är "'" inte bara ett minnesbegränsande, det är urvaltryckDet bestämmer hur mycket systemet minns och tvingar det att fokusera på vad som är viktigt.

Sliding Expiration - En klocka som glömmer

kombinerad med ruttande utgångsdatum, 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 signering inte är tillgänglig within 30 minutes , så är den rejektionerad.

Det skapar naturligt glömma:

  • IPs blir omskiljade
  • Bots roterar kännetecken
  • Trafikmönster förändras
  • Igår attacker var

Statiska blocklistar går döda. Sliding expiration keeps the memory fresh.

IMemoryCache-pattern - Tiny CQRS utan att säga CQ RS

Här är mönstret som gö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 det tråkiga cache-invalidenationen . Istället för att invalidera cache-Einträg efter skrivandet, kaset är skrivandets modell. SQLite är bara den hållbara ledgern

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

Nyckelinsikten: läser och skriver går till minnet. Databasen är i slutändan konsekvent - och det är okt

Read Path - Cache först , alltid

När en detektor behöver en lärd vikt

// 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
}

Heißa vägar aldrig träffa databasen. Cache är sanningens källa för läsande . SQLite är bara ett reservsystem

Att skriva vägen - Cache omedelbart , Fortsätt senare

När systemet lär sig något nytt

// 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;
}

Anmärkning: UpdateWeightAsync återkommer Task.CompletedTask - det ' är i grund och botten synkroniserad. skrivandet är upprept

  • Millisekundlig skriv latens
  • Ingen blockering på I/O
  • Reglerna är sammanlagda (ste skrivar vinnerM SK1

Hintergrundflussorn - 500 ms av boring magic

Alla 500ms, ända skrivar skickas till SQLite i en enda paket

// 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 event-sourcing-light. Du får

  • Batch skriver (effektivt I /O)
  • Transaktionskonsistens
  • Koalationerade uppdaterade ( om samma undertecknande är uppdaterad 10 gånger i ♫ 500 ♫ ms ♫ МSK3 ♫ bara den slutgiltiga värden är skriven
  • SQLite är helt nöjd med denna tillgångsmönster

EMA-aktualiseringar - Att lära sig med exponentella förändringsmått

När en ny observation kommer fram, ,, använder systemet exponentiella rörliga medel 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-formulären underlättar lärande: new_weight = old_weight × (1 - α) + new_value × α

Med α = 0.1:

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

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

Här är den viktigaste insikten som de flesta missar

När kaset fyller upp

  • Low-frequency signatures fall out
  • Bara varma ( oftare tillgängliga )-signaturer stannar i minnet
  • Databasen lagrar med en strömningscykel - och att ' är bra
  • Systemet fokuserar under tryck

Tänk på det.

Den varmaste 10,000 - som vanligtvis representerar 99% av den faktiska trafiken

I produktionen ser jag ungefär 40,000, en, -, bort signaturer per dag. ♫ (, krapper som försöker en gång, ♫ ♫ slumpmässiga sondröjningar, ♪ ♫ ,, legitima användare som aldrig kommer tillbaka. ♪ ), och kanske 5–10,000, som återhämtar sig hela tiden. | ♫

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ödet täcker den beteendemässiga minnet. Systemet själv - optimerar

När överflödet inte hjälper

Det här är inte "'", det är inte magi, det finns kanterna där LRU-trycket fungerar mot dig.

  • Kaset är för litet: Om din dataskiva bara innehåller | 100 | inskrywings men du har |1,000 | verkligt viktiga kännetecken | , | så kommer du |' | att ständigt stöta och förlora användbara mönster innan de samlar tillräckligt med bevis |

  • Enkel trafik: Om du driver på ett litet inre system där nästan allting är varmt | " |

  • Cold-startproblem: Ett nyimplementerat system har inga lärde vikter . Allt är lika kallt ♫ . ♫ De första timmarna kommer att ha högre falska positiva värden tills de varma kännetecken stabiliserar sig ♫

Pattern fungerar bäst när du har hög cardinalitet med kraft- lagfördelning - massor av unika kännetecken , men en liten undergrupp som dominerar trafiken

Att behålla kaset och databasen i synkronisering - Tag-Based Invalidation

Cache är källan till sanningen för läsarna, men databasen är den hållbara ledgern. Vad händer när de driftar?

Förlustssynkronisering

När databasernas vikter förrutsnar, måste kaset följa DecayOldWeightsAsync metod hanterar både:

// 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 att nedbrytna dataregistern, så ringer vi _cache.Compact(0.25) - detta tvingar MemoryCache att dra bort 25% ur inskrywingarna, och ge prioritering till de minst nyligen använta . Nästa läsning kommer att ladda upp nyvärder från databasen

Tag-Based Eviction

Ibland behöver du avskaffa en hel kategori med kasserade inträg - till exempel, när detektorn blir omtränad eller när yttre 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 har inte "'", har ingen native tag , eller en Redis-baserad utskövning, eller en , men kompaktering får samma effekt, eller ett : , tvingar ut städa inskrywings, eller något sådant, eller så får läsarna att återuppstå från databasen.

Synkroniseringsstrategin

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

  1. Kaset missar att ladda om från DB - om en inskrywing är bortskrävd , tar nästa läsande fram ny data
  2. Sliding expiration handleder ångest - uppgifter inte tillgängliga inom 30 minuter automatiskt
  3. Kompakteringskrafter förnyelse - periodic compaction trycker ut gamla inskrywings
  4. skriva-behind coalesces updates - flera snabba uppdaterade versioner blir ett DB skriv

Detta är till slut en jämlikhet gjord rätt

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

Ingen cache-invalidering hell. Inget komplext pub/subM SK2 Bara kompaktion och naturlig utplåning .

Att gå djupare: Reputationssystemet

Nota: Om du bara ville ha LRU: n så kan du stanna här.

Hysteresi och förlust

För mönsterreputation ( att spåra om en kännetecken är robot eller människa över tid

// 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;
}

Stadsövergångar med hysteresi

Patterns don't flip directly from Neutral to ConfirmedBad

// 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 ...
}

Lägg märke till att asymmetryn. ConfirmedBad → Suspect kräver 100 stöd Neutral → Suspect bara behöver 10. Det här är avsiktligt - det är svårare att förlåta än att misstänka

Tidnedgång - Exponentiell försvinning

När mönster blir tysta de bryts ned mot neutrala

// 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
    };
}

Standardtimekonstanter:

  • Punktförlust τ: 168 timmar (7 dagar) ♫ ♫ Punktzahl rör sig ♫ 63% ♫ mot neutralitet efter en vecka av inaktivitet
  • Trägningsförlust τ: 336 timmar (14 dagar ) - självförtroende minskar 63% efter två veckor

Det betyder att en bekräftad, dålig IP som håller tyst i en månad kommer i slutändan falla tillbaka till neutral, inte för att den har reformerats, utan för att beviset har blivit borta.

Background Maintenance Service

Den 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);
}

Müllsamlare tar bort mönster som är:

  • 90+ dagar gammal
  • Ondersteun ≤ 1.0
  • I neutrala tillstånd

Detta förhindrar att ordboken i minnet inte växer fast utan att bevara värdefulla lärde mönster.

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

Många utvecklare når reflexivt efter PostgreSQL eller Redis

  1. skriva-behind eliminerar bottlenecks - SQLite
  2. Lokala lager - ingen nätverkande latens , inga kopplingspooler
  3. Noll Konfiguration - bara en filepath
  4. Perfekt för randanvändning - kör på en Raspberry Pi
  5. Portable - databasen är bara en file som du kan kopiera runt

Skematet ä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, ,, byta till PostgreSQL, m sk1, om du behöver ha eller replikera, m sck2, byte till Redis eller en distribuerad datakälla, mcsk3, Architekturen ändrar inte, msck4, mSck5 bara kopplingsstringen, mck6, SQLite är standard för gränssnittployment, ,, inte religion, mspk8

DiSE-knytet-In - misslyckades som evolution

Om DiSE är den fulla evolutionära maskinen.

Detta mönster implementerar DiSE-principer på den minsta nivån

  • Begränsning av resurser → Väljningstryck
  • LRU utfånganing → Naturell urval
  • Tidsdödning → Att glömma möjliggör anpassning
  • EMA-updater → Mutation genom observation
  • Hysteresi → Stabilitet genom resistens mot förändring

Ett fullt kas är inte ett misslyckande. evolutionära tryck. Systemet samarbetar sig själv - optimerar : varma kännetecken stannar, , kalla känneteken flyter bort,, och beteendemässiga minnen förknippas mot vad som faktiskt betyder något

Inget ML-utbildning . Inga externa modeller. Bara en arkitektur som beter sig som ett levande system

slutsats - Enkela strukturer , Kriget beteende

Hela mönstret boils ner till:

  1. Cache är sanningens källa för läsarna - sub- millisekundlig tillgång
  2. skriver upp cachet omedelbart , förblir kvar senare - inga blockeringar IM SK1O
  3. Sliding utgångserfordrar automatisk LRU - inbyggd i .NET
  4. Avgränsad storlek skapar urvaltryck - överflöde skarpar fokus
  5. Hintergrundfluss håller SQLite i synkronisering - slutgiltiga konsistens är bra
  6. Tidsstörning gör det möjligt att glömma - föråldrade bevis försvinner

Din lilla beteendemässiga affär beter sig mer som ett levande system än CRUD.

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

Om du vill se detta i praktiken, kolla ut mestlucid.botdetection - och DiSE-Architekturserien för den djupare filosofin

logo

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