偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

設(shè)計本地緩存、Redis與數(shù)據(jù)庫的三層架構(gòu):一致性協(xié)議與過期策略實踐

數(shù)據(jù)庫 Redis
實際實施中,需要根據(jù)具體業(yè)務(wù)特點調(diào)整策略參數(shù),如TTL時長、延遲刪除時間等。同時,完善的監(jiān)控和日志記錄對于排查問題和優(yōu)化系統(tǒng)至關(guān)重要。

在現(xiàn)代分布式系統(tǒng)中,為了平衡性能與數(shù)據(jù)一致性,采用本地緩存、分布式緩存(如Redis)和數(shù)據(jù)庫的三層架構(gòu)是一種常見方案。這種架構(gòu)能夠顯著降低數(shù)據(jù)庫壓力并提高響應(yīng)速度,但同時也帶來了數(shù)據(jù)一致性和過期策略的設(shè)計挑戰(zhàn)。本文將深入探討如何設(shè)計這樣一個系統(tǒng),確保數(shù)據(jù)在多層級之間保持一致性,并有效管理數(shù)據(jù)的生命周期。

1. 架構(gòu)概述與挑戰(zhàn)

在我們開始設(shè)計之前,先明確三層架構(gòu)的基本組成:

本地緩存:應(yīng)用進(jìn)程內(nèi)的緩存(如Caffeine、Ehcache),訪問速度最快,但無法跨進(jìn)程共享

分布式緩存(Redis):作為中央緩存層,被所有應(yīng)用實例共享,速度較快

數(shù)據(jù)庫:數(shù)據(jù)的持久化存儲,作為最終的數(shù)據(jù)源

這種架構(gòu)帶來的主要挑戰(zhàn)有:

  • 如何保證本地緩存與Redis之間的數(shù)據(jù)一致性?
  • 如何保證Redis與數(shù)據(jù)庫之間的數(shù)據(jù)一致性?
  • 如何設(shè)計有效的過期策略,避免陳舊數(shù)據(jù)提供服務(wù)?
  • 如何處理緩存穿透、擊穿和雪崩問題?

2. 一致性協(xié)議設(shè)計

2.1 寫操作的一致性保障

當(dāng)數(shù)據(jù)需要更新時,我們必須謹(jǐn)慎處理三層之間的數(shù)據(jù)同步。以下是推薦的寫操作流程:

public class DataService {
    private LocalCache localCache;
    private RedisClient redisClient;
    private Database db;
    
    public void updateData(String key, Object value) {
        // 1. 先更新數(shù)據(jù)庫(最終權(quán)威數(shù)據(jù)源)
        db.update(key, value);
        
        // 2. 刪除Redis中的緩存(而不是更新)
        redisClient.delete(key);
        
        // 3. 刪除本地緩存
        localCache.delete(key);
    }
}

為什么選擇刪除緩存而不是更新緩存?這基于一個簡單但重要的觀察:刪除操作是冪等的,而更新操作不是。在多實例環(huán)境中,多個應(yīng)用實例可能以不同的順序收到更新消息,直接更新緩存可能導(dǎo)致數(shù)據(jù)順序錯亂,而刪除操作確保了下次讀取時會從數(shù)據(jù)庫加載最新數(shù)據(jù)。

2.2 讀操作的一致性保障

讀操作需要遵循"緩存優(yōu)先"的原則,但要有適當(dāng)?shù)幕赝藱C(jī)制:

public Object readData(String key) {
    // 1. 首先嘗試從本地緩存獲取
    Object value = localCache.get(key);
    if (value != null) {
        return value;
    }
    
    // 2. 本地緩存未命中,嘗試從Redis獲取
    value = redisClient.get(key);
    if (value != null) {
        // 將數(shù)據(jù)存入本地緩存
        localCache.set(key, value, LOCAL_TTL);
        return value;
    }
    
    // 3. Redis未命中,從數(shù)據(jù)庫獲取
    value = db.query(key);
    if (value != null) {
        // 更新Redis緩存
        redisClient.set(key, value, REDIS_TTL);
        // 更新本地緩存
        localCache.set(key, value, LOCAL_TTL);
    }
    
    return value;
}

2.3 數(shù)據(jù)庫與Redis的最終一致性

為了確保數(shù)據(jù)庫與Redis之間的最終一致性,可以考慮使用以下額外機(jī)制:

2.3.1 數(shù)據(jù)庫binlog監(jiān)聽

對于重要數(shù)據(jù),可以通過監(jiān)聽數(shù)據(jù)庫的binlog變化來觸發(fā)緩存失效:

public class BinlogListener {
    public void onDataUpdate(String table, String key, Object newValue) {
        // 當(dāng)數(shù)據(jù)庫更新時,刪除相關(guān)緩存
        redisClient.delete(key);
        // 發(fā)送消息通知所有實例清除本地緩存
        messageQueue.send(new CacheEvictMessage(key));
    }
}

2.3.2 延遲雙刪策略

在高并發(fā)場景下,即使先更新數(shù)據(jù)庫再刪除緩存,仍可能存在短暫的數(shù)據(jù)不一致窗口。延遲雙刪策略可以緩解這個問題:

public void updateDataWithDoubleDelete(String key, Object value) {
    // 第一次刪除緩存
    redisClient.delete(key);
    localCache.delete(key);
    
    // 更新數(shù)據(jù)庫
    db.update(key, value);
    
    // 延遲指定時間后再次刪除緩存
    scheduledExecutor.schedule(() -> {
        redisClient.delete(key);
        // 發(fā)送消息通知所有實例清除本地緩存
        messageQueue.send(new CacheEvictMessage(key));
    }, 1000, TimeUnit.MILLISECONDS); // 延遲1秒
}

延遲時間需要根據(jù)實際業(yè)務(wù)讀寫耗時調(diào)整,通常略大于一次讀操作耗時。

3. 過期策略設(shè)計

合理的過期策略是保證數(shù)據(jù)新鮮度和系統(tǒng)性能的關(guān)鍵。

3.1 本地緩存過期策略

本地緩存應(yīng)當(dāng)設(shè)置較短的TTL(Time-To-Live),建議在1-5分鐘之間,這可以在數(shù)據(jù)一致性和性能之間取得良好平衡。

// 使用Caffeine配置本地緩存
Cache<String, Object> localCache = Caffeine.newBuilder()
    .expireAfterWrite(2, TimeUnit.MINUTES) // 寫入2分鐘后過期
    .maximumSize(10000) // 限制最大容量
    .build();

短TTL的優(yōu)勢在于:

  1. 1. 保證數(shù)據(jù)相對新鮮
  2. 2. 即使出現(xiàn)不一致,也會在較短時間內(nèi)自動修復(fù)
  3. 3. 避免本地緩存占用過多內(nèi)存

3.2 Redis緩存過期策略

Redis緩存可以設(shè)置較長的TTL,建議在30分鐘到24小時之間,具體取決于業(yè)務(wù)需求和數(shù)據(jù)變更頻率。

// 設(shè)置Redis緩存,30分鐘過期
redisClient.setex(key, 30 * 60, value);

對于不常變更的數(shù)據(jù),可以設(shè)置更長的過期時間,甚至考慮使用"永久"緩存,通過主動刪除管理生命周期。

3.3 主動刷新策略

對于熱點數(shù)據(jù),可以采用主動刷新策略,在緩存過期前異步刷新數(shù)據(jù):

public class CacheWarmUpScheduler {
    public void scheduleRefresh() {
        scheduledExecutor.scheduleAtFixedRate(() -> {
            // 獲取熱點key列表
            Set<String> hotKeys = getHotKeys();
            for (String key : hotKeys) {
                // 異步刷新
                CompletableFuture.runAsync(() -> {
                    Object value = db.query(key);
                    if (value != null) {
                        redisClient.set(key, value, REDIS_TTL);
                    }
                });
            }
        }, 0, 5, TimeUnit.MINUTES); // 每5分鐘執(zhí)行一次
    }
}

3.4 分級過期策略

不同重要性的數(shù)據(jù)可以采用不同的過期策略:

  • 極高重要性數(shù)據(jù)(如商品價格):短TTL(1-5分鐘)+ 主動刷新 + 實時失效
  • 一般重要性數(shù)據(jù)(如用戶信息):中等TTL(30-60分鐘)+ 延遲雙刪
  • 低重要性數(shù)據(jù)(如文章內(nèi)容):長TTL(數(shù)小時至數(shù)天)+ 懶刷新

4. 特殊情況處理

4.1 緩存穿透

緩存穿透是指查詢一個不存在的數(shù)據(jù),由于緩存中不命中,導(dǎo)致每次請求都直達(dá)數(shù)據(jù)庫。

解決方案:

  • 緩存空值:對于查詢結(jié)果為null的key,也進(jìn)行緩存,但設(shè)置較短的TTL(1-5分鐘)
  • 布隆過濾器:在緩存層之前使用布隆過濾器判斷key是否存在
public Object readDataWithProtection(String key) {
    // 使用布隆過濾器判斷key是否存在
    if (!bloomFilter.mightContain(key)) {
        return null; // 肯定不存在
    }
    
    // 正常緩存查詢流程
    Object value = localCache.get(key);
    if (value != null) {
        if (value instanceof NullValue) { // 空值標(biāo)記
            return null;
        }
        return value;
    }
    
    // ... 其余流程同上
    
    if (value == null) {
        // 緩存空值,防止穿透
        localCache.set(key, NullValue.INSTANCE, NULL_TTL);
        redisClient.setex(key, NULL_TTL, NullValue.INSTANCE);
    }
    
    return value;
}

4.2 緩存擊穿

緩存擊穿是指熱點key在緩存過期的瞬間,大量請求直接訪問數(shù)據(jù)庫。

解決方案:

  • 互斥鎖:當(dāng)緩存失效時,使用分布式鎖保證只有一個請求可以訪問數(shù)據(jù)庫
  • 永不過期:對極熱點數(shù)據(jù)設(shè)置永不過期,通過后臺任務(wù)定期更新
public Object readDataWithMutex(String key) {
    Object value = localCache.get(key);
    if (value != null) {
        return value;
    }
    
    // 嘗試獲取分布式鎖
    String lockKey = "LOCK:" + key;
    boolean locked = redisClient.acquireLock(lockKey, 3, TimeUnit.SECONDS);
    
    if (locked) {
        try {
            // 再次檢查緩存,可能已被其他線程更新
            value = redisClient.get(key);
            if (value != null) {
                localCache.set(key, value, LOCAL_TTL);
                return value;
            }
            
            // 查詢數(shù)據(jù)庫
            value = db.query(key);
            if (value != null) {
                redisClient.set(key, value, REDIS_TTL);
                localCache.set(key, value, LOCAL_TTL);
            } else {
                // 緩存空值防止穿透
                redisClient.setex(key, NULL_TTL, NullValue.INSTANCE);
            }
            
            return value;
        } finally {
            // 釋放鎖
            redisClient.releaseLock(lockKey);
        }
    } else {
        // 未獲取到鎖,短暫等待后重試
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return readData(key); // 重試
    }
}

4.3 緩存雪崩

緩存雪崩是指大量緩存同時過期,導(dǎo)致所有請求直達(dá)數(shù)據(jù)庫。

解決方案:

  • 隨機(jī)TTL:為緩存過期時間添加隨機(jī)值,避免同時過期
  • 多層緩存:使用本地緩存作為Redis緩存的緩沖層
  • 熱點數(shù)據(jù)永不過期:對極熱點數(shù)據(jù)設(shè)置永不過期,通過后臺更新
// 為TTL添加隨機(jī)值,避免同時過期
private int getRandomTtl(int baseTtl) {
    Random random = new Random();
    int randomOffset = random.nextInt(300); // 0-5分鐘的隨機(jī)偏移
    return baseTtl + randomOffset;
}

5. 監(jiān)控與降級

任何緩存系統(tǒng)都需要完善的監(jiān)控和降級機(jī)制:

5.1 監(jiān)控指標(biāo)

  • 緩存命中率:本地緩存和Redis的命中率
  • 緩存操作耗時:讀取各層緩存的平均耗時
  • 數(shù)據(jù)庫壓力:QPS、連接數(shù)等
  • 系統(tǒng)資源:內(nèi)存使用率、網(wǎng)絡(luò)帶寬等

5.2 降級策略

當(dāng)緩存系統(tǒng)出現(xiàn)故障時,需要有降級方案:

  • 本地緩存降級:當(dāng)Redis不可用時,可以適當(dāng)延長本地緩存TTL
  • 讀操作降級:直接訪問數(shù)據(jù)庫,但需要限制頻率防止數(shù)據(jù)庫過載
  • 寫操作降級:將寫操作排隊異步執(zhí)行,或使用本地隊列暫存
public Object readDataWithFallback(String key) {
    try {
        // 正常緩存讀取流程
        return readData(key);
    } catch (CacheException e) {
        // 緩存系統(tǒng)異常,降級直接查詢數(shù)據(jù)庫
        log.warn("Cache system unavailable, fallback to DB", e);
        metrics.counter("cache.fallback").increment();
        
        // 但需要限制頻率,防止數(shù)據(jù)庫壓力過大
        if (rateLimiter.tryAcquire()) {
            return db.query(key);
        } else {
            throw new ServiceUnavailableException("System busy, please try again later");
        }
    }
}

6. 總結(jié)

設(shè)計本地緩存、Redis和數(shù)據(jù)庫的三層架構(gòu)需要在性能和數(shù)據(jù)一致性之間找到平衡點。本文提出了一套綜合解決方案:

  • 寫操作采用"先更新數(shù)據(jù)庫,再刪除緩存"的策略,結(jié)合延遲雙刪提高一致性
  • 讀操作遵循緩存優(yōu)先原則,逐層回退
  • 過期策略采用分級TTL設(shè)計,結(jié)合主動刷新和被動失效
  • 特殊場景使用布隆過濾器、互斥鎖和隨機(jī)TTL應(yīng)對
  • 監(jiān)控降級確保系統(tǒng)在異常情況下仍能提供服務(wù)

實際實施中,需要根據(jù)具體業(yè)務(wù)特點調(diào)整策略參數(shù),如TTL時長、延遲刪除時間等。同時,完善的監(jiān)控和日志記錄對于排查問題和優(yōu)化系統(tǒng)至關(guān)重要。

通過合理設(shè)計一致性協(xié)議和過期策略,三層緩存架構(gòu)能夠顯著提升系統(tǒng)性能,同時保證數(shù)據(jù)的正確性和新鮮度,為高并發(fā)場景下的應(yīng)用提供強(qiáng)有力的支撐。

責(zé)任編輯:武曉燕 來源: 程序員秋天
相關(guān)推薦

2021-06-11 09:21:58

緩存數(shù)據(jù)庫Redis

2025-08-08 07:09:58

2018-09-11 10:46:10

緩存數(shù)據(jù)庫一致性

2024-05-28 00:50:00

RedisMySQL緩存

2021-04-24 16:58:03

數(shù)據(jù)庫工具技術(shù)

2025-04-27 08:52:21

Redis數(shù)據(jù)庫緩存

2020-09-03 09:45:38

緩存數(shù)據(jù)庫分布式

2017-05-19 15:00:05

session架構(gòu)web-server

2024-04-11 13:45:14

Redis數(shù)據(jù)庫緩存

2024-10-28 12:41:25

2022-03-29 10:39:10

緩存數(shù)據(jù)庫數(shù)據(jù)

2023-09-24 14:35:43

Redis數(shù)據(jù)庫

2023-08-15 09:31:01

分布式緩存

2024-12-26 15:01:29

2020-05-12 10:43:22

Redis緩存數(shù)據(jù)庫

2024-11-07 22:57:30

2022-12-14 08:23:30

2024-12-24 14:26:47

2020-09-04 06:32:08

緩存數(shù)據(jù)庫接口

2017-07-25 14:38:56

數(shù)據(jù)庫一致性非鎖定讀一致性鎖定讀
點贊
收藏

51CTO技術(shù)棧公眾號