自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

多級(jí)緩存架構(gòu):深度解析與實(shí)踐

開發(fā) 架構(gòu)
常見的多級(jí)緩存架構(gòu)為本地緩存 + 分布式緩存(Redis)+ 數(shù)據(jù)庫(DB),足以滿足大部分場景;若需支撐更高量級(jí)查詢,可將緩存進(jìn)一步前置。

一、多級(jí)緩存架構(gòu)概述

  • 在互聯(lián)網(wǎng)電商場景中,由于讀、寫請(qǐng)求量級(jí)大且響應(yīng)時(shí)間(RT)要求低,多級(jí)緩存架構(gòu)旨在提升讀請(qǐng)求性能。
  • 緩存本質(zhì)是數(shù)據(jù)冗余,將數(shù)據(jù)逐層冗余放置在離用戶更近、速度更快但容量更小、價(jià)格更貴的存儲(chǔ)系統(tǒng)上,以提高系統(tǒng)訪問性能。
  • 常見的多級(jí)緩存架構(gòu)為本地緩存 + 分布式緩存(Redis)+ 數(shù)據(jù)庫(DB),足以滿足大部分場景;若需支撐更高量級(jí)查詢,可將緩存進(jìn)一步前置。

性能瓶頸

  • DB層:性能瓶頸在于磁盤讀取的磁盤IO,將數(shù)據(jù)移至內(nèi)存中的Redis可提升性能。
  • Redis層:性能受限于網(wǎng)絡(luò)IO,JVM應(yīng)用請(qǐng)求Redis時(shí)需發(fā)起IO請(qǐng)求,使用JVM本地緩存可減少此消耗。
  • JVM本地緩存層:性能受限于Tomcat服務(wù)器,單個(gè)服務(wù)器處理并發(fā)請(qǐng)求有限,可通過增加Tomcat服務(wù)數(shù)量(如K8s多Pod部署)或把緩存前置到Nginx提升性能。
  • Nginx層:Nginx是高性能Web服務(wù)器,單機(jī)處理并發(fā)請(qǐng)求量可達(dá)數(shù)萬,可選擇在Nginx本地內(nèi)存存儲(chǔ)部分?jǐn)?shù)據(jù)或通過Lua腳本訪問Redis數(shù)據(jù);使用兩層Nginx架構(gòu),接入層Nginx負(fù)責(zé)流量分發(fā),應(yīng)用層Nginx處理業(yè)務(wù)邏輯和熱點(diǎn)緩存讀取。
  • 緩存層次并非越多越好,需根據(jù)實(shí)際情況選擇,互聯(lián)網(wǎng)常用多級(jí)緩存為JVM本地緩存 + Redis緩存,對(duì)于極熱點(diǎn)數(shù)據(jù)可采用Nginx + JVM本地緩存 + Redis緩存三級(jí)架構(gòu)。
  • JVM緩存存放熱點(diǎn)數(shù)據(jù),避免其使Redis分片崩潰,但僅在查詢Redis數(shù)據(jù)時(shí)將數(shù)據(jù)放入本地緩存會(huì)導(dǎo)致命中率不高,通常需要熱點(diǎn)探測系統(tǒng)將熱點(diǎn)數(shù)據(jù)放入JVM本地緩存,許多大廠都有自研熱點(diǎn)探測系統(tǒng),如得物的Burning、京東的hotkey。

二、多級(jí)緩存請(qǐng)求流程

圖片圖片

  • 用戶請(qǐng)求進(jìn)入接入層Nginx后,會(huì)被負(fù)載均衡算法分發(fā)到各應(yīng)用層Nginx。
  • 在應(yīng)用層Nginx上,先讀取本地緩存,減少對(duì)后端服務(wù)沖擊;
  • 若未命中,則讀取Redis緩存,以減輕Tomcat集群壓力;
  • 若Redis緩存未命中,進(jìn)入JVM應(yīng)用層,先讀取JVM本地內(nèi)存;
  • 若JVM本地內(nèi)存未命中,訪問Redis集群;
  • 若Redis集群未命中,最后訪問DB。

多級(jí)緩存架構(gòu)應(yīng)用可根據(jù)實(shí)際情況逐步完善,初始使用DB,DB有瓶頸時(shí)添加Redis,Redis無法支撐時(shí)將熱數(shù)據(jù)放至JVM本地內(nèi)存,JVM內(nèi)存也無法支撐時(shí),將緩存前置到Nginx層。

簡單示例:

public class MultiLevelCache {
    private Cache<String, String> jvmCache;
    private Jedis redis;

    public MultiLevelCache() {
        jvmCache = Caffeine.newBuilder()
             .maximumSize(100)
             .build();
        redis = new Jedis("localhost", 6379);
    }

    public String getProductName(String productId) {
        // 首先嘗試從 JVM 本地緩存獲取
        String productName = jvmCache.getIfPresent("product:" + productId + ":name");
        if (productName == null) {
            // 若 JVM 本地緩存未命中,嘗試從 Redis 緩存獲取
            productName = redis.get("product:" + productId + ":name");
            if (productName!= null) {
                // 將 Redis 中命中的數(shù)據(jù)更新到 JVM 本地緩存
                jvmCache.put("product:" + productId + ":name", productName);
            } else {
                // 若 Redis 緩存未命中,從數(shù)據(jù)庫獲取
                productName = fetchFromDatabase(productId);
                if (productName!= null) {
                    // 將數(shù)據(jù)存儲(chǔ)到 Redis 緩存
                    redis.set("product:" + productId + ":name", productName);
                    // 同時(shí)存儲(chǔ)到 JVM 本地緩存
                    jvmCache.put("product:" + productId + ":name", productName);
                }
            }
        }
        return productName;
    }

    private String fetchFromDatabase(String productId) {
        // 模擬從數(shù)據(jù)庫讀取數(shù)據(jù),這里使用 MySQL 示例代碼
        MySQLDataAccess mySQLDataAccess = new MySQLDataAccess();
        return mySQLDataAccess.fetchProductName(productId);
    }

    public static void main(String[] args) {
        MultiLevelCache cache = new MultiLevelCache();
        String productName = cache.getProductName("1");
        System.out.println("Product Name: " + productName);
    }
}
class MySQLDataAccess {
    private static final String JDBC_URL = "jdbc:mysql://localhost:3306/your_database";
    private static final String JDBC_USER = "username";
    private static final String JDBC_PASSWORD = "password";

    public String fetchProductName(String productId) {
        String productName = null;
        try (Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
            String query = "SELECT name FROM products WHERE id =?";
            try (PreparedStatement preparedStatement = connection.prepareStatement(query)) {
                preparedStatement.setInt(1, Integer.parseInt(productId));
                try (ResultSet resultSet = preparedStatement.executeQuery()) {
                    if (resultSet.next()) {
                        productName = resultSet.getString("name");
                    }
                }
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return productName;
    }
}

三、負(fù)載均衡算法的選擇

  • 常用負(fù)載均衡算法:

輪詢:優(yōu)勢在于負(fù)載均衡,但相同請(qǐng)求會(huì)被轉(zhuǎn)發(fā)到不同節(jié)點(diǎn),節(jié)點(diǎn)增多時(shí)緩存命中率會(huì)降低。

一致性哈希:相同請(qǐng)求會(huì)路由到同一臺(tái)機(jī)器,節(jié)點(diǎn)宕機(jī)僅會(huì)使少量緩存數(shù)據(jù)失效,但會(huì)導(dǎo)致大量請(qǐng)求集中在某臺(tái)機(jī)器。

  • 算法選擇依據(jù):負(fù)載較低時(shí),更追求緩存命中率,可使用一致性哈希;負(fù)載較高時(shí),對(duì)熱點(diǎn)數(shù)據(jù)訪問多,更希望請(qǐng)求平均分散,不建議使用一致性哈希,可選擇輪詢,避免如Redis Cluster中使用一致性哈希時(shí)節(jié)點(diǎn)掛掉引發(fā)的雪崩問題。

四、應(yīng)用層Nginx本地緩存實(shí)現(xiàn)

圖片圖片

  • 可使用Lua Shared Dict實(shí)現(xiàn),這是OpenResty提供的功能,OpenResty集成了Nginx和Lua,可開發(fā)業(yè)務(wù)邏輯進(jìn)行熱點(diǎn)數(shù)據(jù)的查詢和獲取。
  • 通過Nginx + Lua可從Nginx本地內(nèi)存或遠(yuǎn)程Redis獲取數(shù)據(jù)。
  • 應(yīng)用層Nginx處理第一層數(shù)據(jù)并存儲(chǔ)熱點(diǎn)數(shù)據(jù),會(huì)將請(qǐng)求上報(bào)到熱點(diǎn)發(fā)現(xiàn)系統(tǒng)進(jìn)行統(tǒng)計(jì),熱點(diǎn)數(shù)據(jù)還可放在JVM本地緩存。
  • 互聯(lián)網(wǎng)公司的熱點(diǎn)數(shù)據(jù)探測系統(tǒng),如京東的hotkey,主要用于探測MySQL、Redis中頻繁訪問數(shù)據(jù),避免因惡意攻擊、爬蟲請(qǐng)求、機(jī)器人導(dǎo)致的熱點(diǎn)數(shù)據(jù)使Redis分片癱瘓。以往使用JVM本地緩存 + Redis緩存的二級(jí)緩存方式,對(duì)于熱點(diǎn)數(shù)據(jù)命中率較低,因此需要統(tǒng)一熱點(diǎn)key探測方案,將熱點(diǎn)數(shù)據(jù)推送到JVM本地緩存。
  • 京東 hotkey:https://mp.weixin.qq.com/s/xOzEj5HtCeh_ezHDPHw6Jw
  • 得物 Burning:https://tech.dewu.com/article?id=23

簡單示例:

-- 在 Nginx 配置文件中
http {
    lua_shared_dict my_cache 10m;  -- 定義一個(gè) 10MB 的共享字典

    server {
        location /cache {
            content_by_lua_block {
                local cache = ngx.shared.my_cache
                local key = ngx.var.arg_key
                local value = cache:get(key)
                if value == nil then
                    -- 從 Redis 獲取數(shù)據(jù)
                    local redis = require "resty.redis"
                    local red = redis:new()
                    red:connect("localhost", 6379)
                    value = red:get(key)
                    red:close()
                    if value ~= nil then
                        cache:set(key, value)  -- 將數(shù)據(jù)存儲(chǔ)到本地緩存
                    end
                end
                ngx.say(value)
            }
        }
    }
}

五、數(shù)據(jù)緩存優(yōu)化

  • 緩存數(shù)據(jù)過期時(shí)間:

設(shè)置過期時(shí)間:適合熱點(diǎn)、易更新數(shù)據(jù),如庫存數(shù)據(jù),可短時(shí)間允許不一致。

不設(shè)置過期時(shí)間:適合非熱點(diǎn)、長期訪問數(shù)據(jù),如用戶信息、店鋪信息等。

  • 緩存數(shù)據(jù)淘汰:
  • 對(duì)于設(shè)置過期時(shí)間的數(shù)據(jù)到期自動(dòng)刪除,不設(shè)置過期時(shí)間的數(shù)據(jù),當(dāng)緩存空間滿時(shí),需通過淘汰策略刪除。
  • 淘汰策略有LRU(根據(jù)訪問時(shí)間淘汰最久未訪問數(shù)據(jù),但大批量數(shù)據(jù)訪問會(huì)使命中率下降)、LFU(根據(jù)訪問頻率淘汰最不常訪問數(shù)據(jù),數(shù)據(jù)訪問內(nèi)容變化大時(shí)命中率會(huì)下降)、ARC(結(jié)合LRU和LFU優(yōu)點(diǎn))。
  • 緩存加載和更新:可使用緩存旁路模式,先寫數(shù)據(jù)庫,再寫緩存;更新時(shí)先更新數(shù)據(jù)庫,再刪除緩存。
  • 增量化緩存重建:對(duì)于復(fù)雜數(shù)據(jù),緩存重建成本高,可通過維度劃分和根據(jù)增量數(shù)據(jù)變更僅重建對(duì)應(yīng)維度緩存,降低重建成本,如對(duì)商品的不同維度信息(基礎(chǔ)信息、圖片等)進(jìn)行劃分,按更新維度重建緩存。

六、緩存數(shù)據(jù)一致性保證

  • 多級(jí)緩存的特性差異:分布式緩存Redis中一份數(shù)據(jù)只存儲(chǔ)一次,可存儲(chǔ)較多數(shù)據(jù);JVM緩存中熱點(diǎn)數(shù)據(jù)在每個(gè)節(jié)點(diǎn)存儲(chǔ),且本地緩存容量小,存儲(chǔ)少量數(shù)據(jù),因此更新策略不同。
  • Redis數(shù)據(jù)一致性:

同步刪除緩存數(shù)據(jù) - 旁路緩存策略:讀場景:先從緩存讀取,命中則返回,未命中從數(shù)據(jù)庫讀取;寫場景:先更新數(shù)據(jù)庫,再失效緩存,能滿足大部分場景的數(shù)據(jù)一致性,但在極端情況(讀寫操作時(shí)序錯(cuò)亂)下會(huì)出現(xiàn)數(shù)據(jù)不一致問題。

同步刪除緩存數(shù)據(jù) - 延時(shí)雙刪:兩次刪除緩存,第一次快速達(dá)到最終一致性,第二次延時(shí)刪除可能的臟數(shù)據(jù);需考慮延時(shí)時(shí)間設(shè)置(大于讀操作時(shí)間 + 數(shù)據(jù)庫主從同步延時(shí)時(shí)間),可使用異步線程執(zhí)行第二次刪除,但存在延時(shí)時(shí)間不準(zhǔn)確和性能消耗問題。

異步刪除緩存數(shù)據(jù) - 基于binlog實(shí)現(xiàn):如阿里巴巴的Canal監(jiān)聽binlog實(shí)現(xiàn)緩存更新,Canal模擬MySQL從庫,接收主庫binlog,Server解析存儲(chǔ),客戶端與Server通信獲取binlog;

Canal 1.1.1版本后支持將binlog投遞至MQ,可多客戶端消費(fèi),實(shí)現(xiàn)大量數(shù)據(jù)的緩存更新;但對(duì)于大多數(shù)場景,使用緩存旁路策略已足夠,引入Canal會(huì)使架構(gòu)復(fù)雜且增加維護(hù)成本。

簡單示例:

public class RedisCacheConsistency {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        String key = "product:1:price";
        // 先更新數(shù)據(jù)庫(這里省略數(shù)據(jù)庫更新代碼)
        // 立即刪除緩存
        jedis.del(key);
        // 延時(shí) 500 毫秒后再次刪除緩存,確保臟數(shù)據(jù)清除
        new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
                jedis.del(key);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        jedis.close();
    }
}
  • 本地緩存數(shù)據(jù)一致性:

對(duì)于少量熱點(diǎn)數(shù)據(jù),可采用主動(dòng)更新 + 過期時(shí)間的方式刷新本地緩存。

對(duì)于不同更新頻率的數(shù)據(jù),可設(shè)置不同過期時(shí)間并配合定時(shí)任務(wù)刷新,如不易變化的數(shù)據(jù)可設(shè)置過期時(shí)間為30min,配合1min更新一次的定時(shí)任務(wù);變化頻繁的數(shù)據(jù)可設(shè)置過期時(shí)間為1min,配合30ms更新一次的定時(shí)任務(wù)。

可使用本地雙緩存策略,創(chuàng)建兩份過期時(shí)間不同的本地緩存,第一份寫后30min過期,第二份讀寫后40min過期;讀寫操作以第一份緩存為主,第一份過期后可從第二份讀取,同時(shí)更新第一份緩存,第二份作為備用。

             本地雙緩存刷新通過定時(shí)任務(wù)完成,檢查緩存數(shù)據(jù)失效后從遠(yuǎn)程獲取數(shù)據(jù)并放入雙緩存。

  • 緩存最終一致性的保證:設(shè)置緩存過期時(shí)間,最終會(huì)刪除緩存,達(dá)到最終一致性;軟件工程中無法保證絕對(duì)一致性,如Linux的PageCache在服務(wù)器異常關(guān)機(jī)時(shí)會(huì)有數(shù)據(jù)丟失,軟件層面更難避免數(shù)據(jù)不一致。

簡單示例:

public class LocalCacheConsistency {
    // 本地緩存
    private Cache<String, String> cache;
    // 模擬的數(shù)據(jù)庫訪問服務(wù)
    private DatabaseService databaseService;
    // 定時(shí)任務(wù)執(zhí)行器
    private ScheduledExecutorService executorService;

    public LocalCacheConsistency() {
        // 初始化本地緩存,設(shè)置最大容量和過期時(shí)間
        cache = Caffeine.newBuilder()
              .maximumSize(100)
              .expireAfterWrite(1, TimeUnit.MINUTES)
              .build();
        databaseService = new DatabaseService();
        // 創(chuàng)建定時(shí)任務(wù)執(zhí)行器
        executorService = Executors.newSingleThreadScheduledExecutor();
        // 啟動(dòng)定時(shí)刷新任務(wù)
        startCacheRefreshTask();
    }

    // 從本地緩存獲取數(shù)據(jù)
    public String get(String key) {
        return cache.getIfPresent(key);
    }

    // 存儲(chǔ)數(shù)據(jù)到本地緩存
    public void put(String key, String value) {
        cache.put(key, value);
    }

    // 從數(shù)據(jù)庫獲取數(shù)據(jù)并更新本地緩存
    public void refreshCache(String key) {
        String value = databaseService.fetchData(key);
        if (value!= null) {
            cache.put(key, value);
        }
    }

    // 啟動(dòng)定時(shí)刷新緩存任務(wù)
    private void startCacheRefreshTask() {
        executorService.scheduleAtFixedRate(() -> {
            // 這里可以根據(jù)實(shí)際情況選擇要刷新的緩存鍵,例如全部刷新或只刷新部分熱點(diǎn)鍵
            // 這里假設(shè)我們要刷新所有緩存鍵,你可以修改為更具體的邏輯
            refreshAllCacheKeys();
        }, 0, 30, TimeUnit.SECONDS); // 每 30 秒刷新一次
    }

    // 刷新所有緩存鍵
    private void refreshAllCacheKeys() {
        // 假設(shè)緩存鍵的前綴是 "product:",這里可以根據(jù)具體業(yè)務(wù)進(jìn)行修改
        for (int i = 1; i <= 100; i++) {
            String key = "product:" + i;
            refreshCache(key);
        }
    }

    // 模擬的數(shù)據(jù)庫服務(wù)
    private static class DatabaseService {
        public String fetchData(String key) {
            // 這里可以實(shí)現(xiàn)具體的數(shù)據(jù)庫查詢邏輯,這里僅作模擬
            System.out.println("Fetching data from database for key: " + key);
            return"Value for " + key;
        }
    }
}

七、最后

在實(shí)際使用中,需從業(yè)務(wù)場景、數(shù)據(jù)不一致時(shí)間、實(shí)現(xiàn)成本和維護(hù)成本等多方面評(píng)估并選擇合適方案,通過壓測和實(shí)驗(yàn)對(duì)比優(yōu)劣。

責(zé)任編輯:武曉燕 來源: 一安未來
相關(guān)推薦

2022-06-13 10:23:34

Helios緩存服務(wù)端

2024-08-30 09:53:17

Java 8編程集成

2024-09-19 08:49:13

2013-04-07 17:57:16

SDN網(wǎng)絡(luò)架構(gòu)

2024-05-06 00:00:00

GAC代碼緩存

2023-12-04 16:18:30

2016-08-23 10:50:50

WebJavascript緩存

2010-11-25 09:37:14

MySQL查詢緩存機(jī)制

2019-10-21 09:32:48

緩存架構(gòu)分層

2023-05-05 06:13:51

分布式多級(jí)緩存系統(tǒng)

2024-07-08 07:30:47

2025-01-02 10:19:18

2024-12-30 08:55:09

2024-12-20 16:46:22

Spring三級(jí)緩存

2024-11-18 16:15:00

2024-12-24 14:01:10

2023-09-28 08:34:26

Docker微服務(wù)

2025-02-25 10:21:15

2024-10-12 14:18:21

C++OOP函數(shù)重載

2009-07-30 09:23:53

Java JNI
點(diǎn)贊
收藏

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