搜尋
首頁資料庫Redis高並發技巧之Redis和本地快取使用技巧分享

這篇文章為大家帶來了關於Redis的相關知識,其中主要介紹的是分散式快取和本地快取的使用技巧,包括快取種類介紹,各種的使用場景,以及如何使用,最後再給實戰案例,下面一起來看一下,希望對大家有幫助。

推薦學習:Redis影片教學

#眾所周知,快取最主要的目的是加速訪問,緩解資料庫壓力。最常用的緩存就是分散式緩存,例如redis,在面對大部分並發場景或一些中小型公司流量沒有那麼高的情況,使用redis基本上都能解決了。但在流量較高的情況下可能得使用到本機快取了,例如guava的LoadingCache和快手開源的ReloadableCache。

三種快取的使用場景

這部分會介紹redis,像是guava的LoadingCache和快手開源的ReloadableCache的使用場景和限制,透過這部分的介紹就能知道在怎樣的業務場景下應該使用哪種緩存,以及為什麼。

Redis的使用場景和限制

如果寬泛的說redis何時使用,那麼自然就是用戶訪問量過高的地方使用,從而加速訪問,並且緩解資料庫壓力。如果細分的話,還得分為單節點問題和非單節點問題。

如果一個頁面使用者訪問量比較高,但是訪問的不是同一個資源。例如使用者詳情頁,訪問量比較高,但每個使用者的資料都是不一樣的,這種情況顯然只能用分散式快取了,如果使用redis,key為使用者唯一鍵,value則是使用者資訊。

redis導致的快取擊穿

但是要注意一點,一定要設定過期時間,而且不能設定到同一時間點過期。舉個例子,例如用戶又個活動頁,活動頁能看到用戶活動期間獲獎數據,粗心的人可能會設定用戶數據的過期時間點為活動結束,這樣會

#單(熱)點問題

單節點問題說的是redis的單一節點的並發問題,因為對於相同的key會落到redis集群的同一個節點上,那麼如果對這個key的訪問量過高,那麼這個redis節點就存在並發隱患,這個key就稱為熱key。

如果所有使用者存取的都是同一個資源,例如小愛同學app首頁對所有使用者展示的內容都一樣(初期),服務端給h5返回的是同一個大json,顯然得使用到緩存。首先我們考慮下用redis是否可行,由於redis存在單點問題,如果流量過大的話,那麼所有用戶的請求到達redis的同一個節點,需要評估該節點能否抗住這麼大流量。我們的規則是,如果單節點qps達到了千級就要解決單點問題了(即使redis號稱能抗住十萬級的qps),最常見的做法就是使用本地快取。顯然小愛app首頁流量不過百,使用redis是沒問題的。

LoadingCache的使用場景和限制

對於這上面說的熱key問題,我們最直接的做法就是使用本地緩存,例如你最熟悉的guava的LoadingCache,但是使用本地快取要求能夠接受一定的髒數據,因為如果你更新了首頁,本地快取是不會更新的,它只會根據一定的過期策略來重新加載緩存,不過在我們這個場景是完全沒問題的,因為一旦在後台推送了首頁後就不會再去改變了。即使改變了也沒問題,可以設定寫過期為半小時,超過半小時重新加載緩存,這種短時間內的髒數據我們是可以接受的。

LoadingCache導致的快取擊穿

雖然說本地快取和機器上強相關的,雖然程式碼層面寫的是半小時過期,但由於每台機器的啟動時間不同,導致快取的載入時間不同,過期時間也就不同,也就不會所有機器上的請求在同一時間快取失效後都去請求資料庫。但對於單一一台機器也是會導致快取穿透的,假如有10台機器,每台1000的qps,只要有一台快取過期就可能導致這1000個請求同時打到了資料庫。這個問題其實比較好解決,但是容易被忽略,也就是在設定LoadingCache的時候使用LoadingCache的load-miss方法,而不是直接判斷cache.getIfPresent()== null然後去請求db;前者會加虛擬機層面的鎖,保證只有一個請求打到資料庫去,從而完美的解決了這個問題。

但是,如果對於即時性要求較高的情況,例如有段時間要經常做活動,我要保證活動頁面能近實時更新,也就是運營在後台配置好了活動信息後,需要在C端近即時展示這次配置的活動訊息,此時使用LoadingCache肯定就不能滿足了。

ReloadableCache的使用場景和限制

對於上面說的LoadingCache不能解決的即時問題,可以考慮使用ReloadableCache,這是快手開源的一個本機快取框架,最大的特點是支援多機器同時更新緩存,假設我們修改了首頁信息,然後請求打到的是A機器,這個時候重新加載ReloadableCache,然後它會發出通知,監聽了同一zk節點的其他機器收到通知後重新更新緩存。使用這個緩存一般的要求是將全量資料載入到本地緩存,所以如果資料量過大肯定會對gc造成壓力,這種情況就不能使用了。由於小愛同學首頁這個首頁是帶有狀態的,一般online狀態的就那麼兩個,所以完全可以使用ReloadableCache來只裝載online狀態的首頁。

小結

到這裡三種快取基本上都介紹完了,做個小結:

  • 對於非熱點的數據訪問,例如用戶維度的數據,直接使用redis即可;
  • 對於熱點數據的訪問,如果流量不是很高,無腦使用redis即可;
  • 對於熱點數據,如果允許一定時間內的髒數據,使用LoadingCache即可;
  • 對於熱點數據,如果一致性要求較高,同時數據量不大的情況,使用ReloadableCache即可;
##小技巧

不管哪種本地快取雖然都帶有虛擬機器層面的加鎖來解決擊穿問題,但是意外總有可能以你意想不到的方式發生,保險起見你可以使用兩級緩存的方式即本地緩存redis db 。

快取使用的簡單介紹

這裡redis的使用就不再多說了,相信很多人對api的使用比我還熟悉

LoadingCache的使用

這個是guava提供的網上一抓一大把,但是給兩點注意事項

    要使用load-miss的話, 要么使用
  • V get(K key, Callable extends V> loader);要嘛使用build的時候使用的是build(CacheLoader super K1, V1> loader)這個時候可以直接使用get( )了。另外建議使用load-miss,而不是getIfPresent==null的時候再去查資料庫,這可能會導致快取擊穿;
  • 使用load-miss是因為這是執行緒安全的,如果快取失效的話,多個執行緒呼叫get的時候只會有一個執行緒去db查詢,其他執行緒需要等待,也就是說這是執行緒安全的。
  • LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                    .maximumSize(1000L)
                    .expireAfterAccess(Duration.ofHours(1L)) // 多久不访问就过期
                    .expireAfterWrite(Duration.ofHours(1L))  // 多久这个key没修改就过期
                    .build(new CacheLoader<String, String>() {
                        @Override
                        public String load(String key) throws Exception {
                            // 数据装载方式,一般就是loadDB
                            return key + " world";
                        }
                    });
    String value = cache.get("hello"); // 返回hello world

reloadableCache的使用

導入三方依賴

<dependency>
  <groupId>com.github.phantomthief</groupId>
  <artifactId>zknotify-cache</artifactId>
  <version>0.1.22</version>
</dependency>

需要看文檔,不然無法使用,有興趣自己寫一個也行的。

public interface ReloadableCache<T> extends Supplier<T> {

    /**
     * 获取缓存数据
     */
    @Override
    T get();

    /**
     * 通知全局缓存更新
     * 注意:如果本地缓存没有初始化,本方法并不会初始化本地缓存并重新加载
     *
     * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
     */
    void reload();

    /**
     * 更新本地缓存的本地副本
     * 注意:如果本地缓存没有初始化,本方法并不会初始化并刷新本地的缓存
     *
     * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
     */
    void reloadLocal();
}

老生常談的快取擊穿/穿透/雪崩問題

這三個真的是亙古不變的問題,如果流量大確實需要考慮。

快取擊穿

簡單說就是快取失效,導致大量請求同一時間打到了資料庫。對於快取擊穿問題上面已經給了許多解決方案了。

    例如使用本機快取
  • 本機快取使用load-miss方法
  • #使用第三方服務來載入快取
1.2和都說過,主要來看3。假如業務願意只能使用redis而無法使用本地緩存,例如資料量過大,即時性需求比較高。那麼當快取失效的時候就得想辦法保證只有少量的請求打到資料庫。很自然的就想到了使用分散式鎖,理論上是可行的,但實際上有隱憂。我們的分散式鎖相信很多人都是使用redis lua的方式實現的,並且在while中進行了輪訓,這樣請求量大,數據多的話會導致無形中讓redis成了隱患,並且佔了太多業務線程,其實只是引入了分散式鎖就加大了複雜度,我們的原則就是能不用就不用。

那我們是不是可以設​​計一個類似分散式鎖定,但更可靠的rpc服務呢?當呼叫get方法的時候這個rpc服務保證相同的key打到同一個節點,並且使用synchronized來進行加鎖,之後完成資料的載入。在快手提供了一個叫cacheSetter的框架。下面提供一個簡易版,自己寫也很容易實現。

import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

/**
 * @Description 分布式加载缓存的rpc服务,如果部署了多台机器那么调用端最好使用id做一致性hash保证相同id的请求打到同一台机器。
 **/
public abstract class AbstractCacheSetterService implements CacheSetterService {

    private final ConcurrentMap<String, CountDownLatch> loadCache = new ConcurrentHashMap<>();

    private final Object lock = new Object();

    @Override
    public void load(Collection<String> needLoadIds) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return;
        }
        CountDownLatch latch;
        Collection<CountDownLatch> loadingLatchList;
        synchronized (lock) {
            loadingLatchList = excludeLoadingIds(needLoadIds);

            needLoadIds = Collections.unmodifiableCollection(needLoadIds);

            latch = saveLatch(needLoadIds);
        }
        System.out.println("needLoadIds:" + needLoadIds);
        try {
            if (CollectionUtils.isNotEmpty(needLoadIds)) {
                loadCache(needLoadIds);
            }
        } finally {
            release(needLoadIds, latch);
            block(loadingLatchList);
        }

    }

    /**
     * 加锁
     * @param loadingLatchList 需要加锁的id对应的CountDownLatch
     */
    protected void block(Collection<CountDownLatch> loadingLatchList) {
        if (CollectionUtils.isEmpty(loadingLatchList)) {
            return;
        }
        System.out.println("block:" + loadingLatchList);
        loadingLatchList.forEach(l -> {
            try {
                l.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 释放锁
     * @param needLoadIds 需要释放锁的id集合
     * @param latch 通过该CountDownLatch来释放锁
     */
    private void release(Collection<String> needLoadIds, CountDownLatch latch) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return;
        }
        synchronized (lock) {
            needLoadIds.forEach(id -> loadCache.remove(id));
        }
        if (latch != null) {
            latch.countDown();
        }
    }

    /**
     * 加载缓存,比如根据id从db查询数据,然后设置到redis中
     * @param needLoadIds 加载缓存的id集合
     */
    protected abstract void loadCache(Collection<String> needLoadIds);

    /**
     * 对需要加载缓存的id绑定CountDownLatch,后续相同的id请求来了从map中找到CountDownLatch,并且await,直到该线程加载完了缓存
     * @param needLoadIds 能够正在去加载缓存的id集合
     * @return 公用的CountDownLatch
     */
    protected CountDownLatch saveLatch(Collection<String> needLoadIds) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return null;
        }
        CountDownLatch latch = new CountDownLatch(1);
        needLoadIds.forEach(loadId -> loadCache.put(loadId, latch));
        System.out.println("loadCache:" + loadCache);
        return latch;
    }

    /**
     * 哪些id正在加载数据,此时持有相同id的线程需要等待
     * @param ids 需要加载缓存的id集合
     * @return 正在加载的id所对应的CountDownLatch集合
     */
    private Collection<CountDownLatch> excludeLoadingIds(Collection<String> ids) {
        List<CountDownLatch> loadingLatchList = Lists.newArrayList();
        Iterator<String> iterator = ids.iterator();
        while (iterator.hasNext()) {
            String id = iterator.next();
            CountDownLatch latch = loadCache.get(id);
            if (latch != null) {
                loadingLatchList.add(latch);
                iterator.remove();
            }
        }
        System.out.println("loadingLatchList:" + loadingLatchList);
        return loadingLatchList;
    }
}

業務實作

import java.util.Collection;
public class BizCacheSetterRpcService extends AbstractCacheSetterService {
    @Override
    protected void loadCache(Collection<String> needLoadIds) {
        // 读取db进行处理
   	// 设置缓存
    }
}

快取穿透

#簡單來說就是請求的資料在資料庫中不存在,導致無效請求打穿資料庫。

解法也很簡單,從db取得資料的方法(getByKey(K key))一定要給個預設值。

例如我有個獎金池,金額上限是1W,用戶完成任務的時候給他發筆錢,並且使用redis記錄下來,並且落表,用戶在任務頁面能實時看到獎池剩餘金額,在任務開始的時候顯然獎池金額是不變的,redis和db裡面都沒有發放金額的記錄,這就導致每次必然都去查db,對於這種情況,從db沒查出來數據應該緩存個值0到緩存。

快取雪崩

就是大量快取集中失效打到了db,當然肯定都是一類的業務緩存,歸根到底是程式碼寫的有問題。可以將快取失效的過期時間打散,別讓其集中失效就可以了。

推薦學習:Redis影片教學

以上是高並發技巧之Redis和本地快取使用技巧分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:脚本之家。如有侵權,請聯絡admin@php.cn刪除
REDIS:超越SQL- NOSQL的觀點REDIS:超越SQL- NOSQL的觀點May 08, 2025 am 12:25 AM

Redis超越SQL數據庫的原因在於其高性能和靈活性。 1)Redis通過內存存儲實現極快的讀寫速度。 2)它支持多種數據結構,如列表和集合,適用於復雜數據處理。 3)單線程模型簡化開發,但高並發時可能成瓶頸。

REDIS:與傳統數據庫服務器的比較REDIS:與傳統數據庫服務器的比較May 07, 2025 am 12:09 AM

Redis在高並發和低延遲場景下優於傳統數據庫,但不適合複雜查詢和事務處理。 1.Redis使用內存存儲,讀寫速度快,適合高並發和低延遲需求。 2.傳統數據庫基於磁盤,支持複雜查詢和事務處理,數據一致性和持久性強。 3.Redis適用於作為傳統數據庫的補充或替代,但需根據具體業務需求選擇。

REDIS:功能強大的內存數據存儲的簡介REDIS:功能強大的內存數據存儲的簡介May 06, 2025 am 12:08 AM

Redisisahigh-performancein-memorydatastructurestorethatexcelsinspeedandversatility.1)Itsupportsvariousdatastructureslikestrings,lists,andsets.2)Redisisanin-memorydatabasewithpersistenceoptions,ensuringfastperformanceanddatasafety.3)Itoffersatomicoper

Redis主要是數據庫嗎?Redis主要是數據庫嗎?May 05, 2025 am 12:07 AM

Redis主要是一個數據庫,但它不僅僅是數據庫。 1.作為數據庫,Redis支持持久化,適合高性能需求。 2.作為緩存,Redis提升應用響應速度。 3.作為消息代理,Redis支持發布-訂閱模式,適用於實時通信。

REDIS:數據庫,服務器還是其他?REDIS:數據庫,服務器還是其他?May 04, 2025 am 12:08 AM

redisisamultifaceTedToolThatServesAsAdatabase,server和more.itfunctionsasanin-memorydatastrustore,supportsvariousDataStructures,and CanbeusedAsacache,MessageBroker,sessionStorage,sessionStorage,sessionstorage,andford forderibedibedlocking。

REDIS:揭示其目的和關鍵應用程序REDIS:揭示其目的和關鍵應用程序May 03, 2025 am 12:11 AM

Redisisanopen-Source,內存內部的庫雷斯塔氏菌,卡赫和梅斯吉級,excellingInsPeedAndVersatory.itiswidelysusedforcaching,Real-Timeanalytics,Session Management,Session Managements,and sessighterboarderboarderboardobboardotoitsssupportfortfortfortfortfortfortfortfortorvortfortfortfortfortfortforvortfortforvortforvortforvortfortforvortforvortforvortforvortdatastherctuct anddatataCcessandcessanddataaCces

REDIS:鍵值數據存儲的指南REDIS:鍵值數據存儲的指南May 02, 2025 am 12:10 AM

Redis是一個開源的內存數據結構存儲,用作數據庫、緩存和消息代理,適合需要快速響應和高並發的場景。 1.Redis使用內存存儲數據,提供微秒級的讀寫速度。 2.它支持多種數據結構,如字符串、列表、集合等。 3.Redis通過RDB和AOF機制實現數據持久化。 4.使用單線程模型和多路復用技術高效處理請求。 5.性能優化策略包括LRU算法和集群模式。

REDIS:緩存,會話管理等REDIS:緩存,會話管理等May 01, 2025 am 12:03 AM

Redis的功能主要包括緩存、會話管理和其他功能:1)緩存功能通過內存存儲數據,提高讀取速度,適用於電商網站等高頻訪問場景;2)會話管理功能在分佈式系統中共享會話數據,並通過過期時間機制自動清理;3)其他功能如發布-訂閱模式、分佈式鎖和計數器,適用於實時消息推送和多線程系統等場景。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

熱工具

SublimeText3 英文版

SublimeText3 英文版

推薦:為Win版本,支援程式碼提示!

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器