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

<!--category-- ASP.NET, Architecture, CQRS, Bot Detection, Caching, Systems Design -->
<datetime class="hidden">2025-12-09T12:00</datetime>

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](/blog/botdetection-introduction). Detta är också den minsta versionen av [DiSE-arkitekturen](/blog/dise-architecture-overview) - styrd evolution genom resurstryck

Om du' har läst min artikel på [CQRS och Event Sourcing](/blog/moderncqrsandeventsourcing), 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,

[TOC]

## 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

```csharp
// 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 **urvaltryck**Det 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:

```csharp
// 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:

```csharp
/// <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](/blog/moderncqrsandeventsourcing#part-2-the-half-assed-approach-cache-invalidation) - 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

```mermaid
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

```csharp
// 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

```csharp
// 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

```csharp
// 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.

```csharp
// 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. | ♫

```mermaid
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:

```csharp
// 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

```csharp
// 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

```mermaid
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

```csharp
// 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

```csharp
// 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

```csharp
// 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:

```csharp
// 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:

```sql
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](/blog/dise-architecture-overview) ä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](/blog/botdetection-introduction) - och [DiSE-Architekturserien](/blog/dise-architecture-overview) för den djupare filosofin

## Links

- [Bot-detektorintroduction](/blog/botdetection-introduction) - systemet som använder dessa mönster
- [DiSE arkitektur överblick](/blog/dise-architecture-overview) - kontrollerad evolution genom tryck
- [Modern CQRS och Event Sourcing](/blog/moderncqrsandeventsourcing) - det fulla mönstret  när du behöver det
- [GitHub: mestlucid.botdetection](https://github.com/scottgal/mostlylucid.nugetpackages) - full källkod
- [NuGet-paket](https://www.nuget.org/packages/mostlylucid.botdetection/) - installera den