面試題之java緩存總結(jié),從單機(jī)緩存到分布式緩存架構(gòu)

1、緩存定義

高速數(shù)據(jù)存儲層,提高程序性能

2、為什么要用緩存(讀多寫少,高并發(fā))

1、提高讀取吞吐量

2、提升應(yīng)用程序性能

3、降低數(shù)據(jù)庫成本

4、減少后端負(fù)載

5、消除數(shù)據(jù)庫熱點(diǎn)

6、可預(yù)測的性能

3、緩存分類

3.1、單機(jī)緩存(localCache)

實(shí)現(xiàn)方案

1、基于JSR107規(guī)范自研(了解即可):

1、Java Caching定義了5個(gè)核心接口,分別是CachingProvider, CacheManager, Cache, Entry 和 Expiry。

2、CachingProvider定義了創(chuàng)建、配置、獲取、管理和控制多個(gè)CacheManager。一個(gè)應(yīng)用可以在運(yùn)行期訪問多個(gè)CachingProvider。

3、CacheManager定義了創(chuàng)建、配置、獲取、管理和控制多個(gè)唯一命名的Cache,這些Cache存在于CacheManager的上下文中。一個(gè)CacheManager僅被一個(gè)CachingProvider所擁有。

4、Cache是一個(gè)類似Map的數(shù)據(jù)結(jié)構(gòu)并臨時(shí)存儲以Key為索引的值。一個(gè)Cache僅被一個(gè)CacheManager所擁有。

5、Entry是一個(gè)存儲在Cache中的key-value對。

每一個(gè)存儲在Cache中的條目有一個(gè)定義的有效期,即Expiry Duration。

一旦超過這個(gè)時(shí)間,條目為過期的狀態(tài)。一旦過期,條目將不可訪問、更新和刪除。緩存有效期可以通過ExpiryPolicy設(shè)置。

2、基于ConcurrentHashMap實(shí)現(xiàn)數(shù)據(jù)緩存

3.2、分布式緩存(redis、Memcached)

4、單機(jī)緩存

1、自己實(shí)現(xiàn)一個(gè)單機(jī)緩存

創(chuàng)建緩存類

/**

* @author yinfeng

* @description 本地緩存實(shí)現(xiàn):用map實(shí)現(xiàn)一個(gè)簡單的緩存功能

* @since 2022/2/8 13:54

*/

public class MapCacheDemo {

? ? /**

? ? * 在構(gòu)造函數(shù)中,創(chuàng)建了一個(gè)守護(hù)程序線程,每5秒掃描一次并清理過期的對象

? ? */

? ? private static final int CLEAN_UP_PERIOD_IN_SEC = 5;

? ? /**

? ? * ConcurrentHashMap保證線程安全的要求

? ? * SoftReference <Object>? 作為映射值,因?yàn)檐浺每梢员WC在拋出OutOfMemory之前,如果缺少內(nèi)存,將刪除引用的對象。

? ? */

? ? private final ConcurrentHashMap<String, SoftReference<CacheObject>> cache = new ConcurrentHashMap<>();

? ? public MapCacheDemo() {

? ? ? ? //創(chuàng)建了一個(gè)守護(hù)程序線程,每5秒掃描一次并清理過期的對象

? ? ? ? Thread cleanerThread = new Thread(() -> {

? ? ? ? ? ? while (!Thread.currentThread().isInterrupted()) {

? ? ? ? ? ? ? ? try {

? ? ? ? ? ? ? ? ? ? Thread.sleep(CLEAN_UP_PERIOD_IN_SEC * 1000);

? ? ? ? ? ? ? ? ? ? cache.entrySet().removeIf(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(CacheObject::isExpired).orElse(false));

? ? ? ? ? ? ? ? } catch (InterruptedException e) {

? ? ? ? ? ? ? ? ? ? Thread.currentThread().interrupt();

? ? ? ? ? ? ? ? }

? ? ? ? ? ? }

? ? ? ? });

? ? ? ? cleanerThread.setDaemon(true);

? ? ? ? cleanerThread.start();

? ? }

? ? public void add(String key, Object value, long periodInMillis) {

? ? ? ? if (key == null) {

? ? ? ? ? ? return;

? ? ? ? }

? ? ? ? if (value == null) {

? ? ? ? ? ? cache.remove(key);

? ? ? ? } else {

? ? ? ? ? ? long expiryTime = System.currentTimeMillis() + periodInMillis;

? ? ? ? ? ? cache.put(key, new SoftReference<>(new CacheObject(value, expiryTime)));

? ? ? ? }

? ? }

? ? public void remove(String key) {

? ? ? ? cache.remove(key);

? ? }

? ? public Object get(String key) {

? ? ? ? return Optional.ofNullable(cache.get(key)).map(SoftReference::get).filter(cacheObject -> !cacheObject.isExpired()).map(CacheObject::getValue).orElse(null);

? ? }

? ? public void clear() {

? ? ? ? cache.clear();

? ? }

? ? public long size() {

? ? ? ? return cache.entrySet().stream().filter(entry -> Optional.ofNullable(entry.getValue()).map(SoftReference::get).map(cacheObject -> !cacheObject.isExpired()).orElse(false)).count();

? ? }

? ? /**

? ? * 緩存對象value

? ? */

? ? private static class CacheObject {

? ? ? ? private Object value;

? ? ? ? private final long expiryTime;

? ? ? ? private CacheObject(Object value, long expiryTime) {

? ? ? ? ? ? this.value = value;

? ? ? ? ? ? this.expiryTime = expiryTime;

? ? ? ? }

? ? ? ? boolean isExpired() {

? ? ? ? ? ? return System.currentTimeMillis() > expiryTime;

? ? ? ? }

? ? ? ? public Object getValue() {

? ? ? ? ? ? return value;

? ? ? ? }

? ? ? ? public void setValue(Object value) {

? ? ? ? ? ? this.value = value;

? ? ? ? }

? ? }


}

寫個(gè)main方法測試一下

public static void main(String[] args) throws InterruptedException {

? ? ? ? MapCacheDemo mapCacheDemo = new MapCacheDemo();

? ? ? ? mapCacheDemo.add("uid_10001", "{1}", 5 * 1000);

? ? ? ? mapCacheDemo.add("uid_10002", "{2}", 5 * 1000);

? ? ? ? System.out.println("從緩存中取出值:" + mapCacheDemo.get("uid_10001"));

? ? ? ? Thread.sleep(5000L);

? ? ? ? System.out.println("5秒鐘過后");

? ? ? ? // 5秒后數(shù)據(jù)自動清除了

? ? ? ? System.out.println("從緩存中取出值:" + mapCacheDemo.get("uid_10001"));

? ? }

2、谷歌guava cache緩存框架

2.1、簡介

Guava Cache是一個(gè)內(nèi)存緩存模塊,用于將數(shù)據(jù)緩存到j(luò)vm內(nèi)存中,是單個(gè)應(yīng)用運(yùn)行時(shí)的本地緩存,他不將數(shù)據(jù)放到文件或外部服務(wù)器。

2.2簡單使用

/**

* @author yinfeng

* @description guava測試,https://github.com/google/guava

* @since 2022/2/8 14:13

*/

public class GuavaCacheDemo {

? ? public static void main(String[] args) throws ExecutionException {

? ? ? ? //緩存接口這里是LoadingCache,LoadingCache在緩存項(xiàng)不存在時(shí)可以自動加載緩存

? ? ? ? LoadingCache<String, User> userCache

? ? ? ? ? ? ? ? //CacheBuilder的構(gòu)造函數(shù)是私有的,只能通過其靜態(tài)方法newBuilder()來獲得CacheBuilder的實(shí)例

? ? ? ? ? ? ? ? = CacheBuilder.newBuilder()

? ? ? ? ? ? ? ? //設(shè)置并發(fā)級別為8,并發(fā)級別是指可以同時(shí)寫緩存的線程數(shù)

? ? ? ? ? ? ? ? .concurrencyLevel(8)

? ? ? ? ? ? ? ? //設(shè)置寫緩存后8秒鐘過期

? ? ? ? ? ? ? ? .expireAfterWrite(8, TimeUnit.SECONDS)

? ? ? ? ? ? ? ? //設(shè)置寫緩存后1秒鐘刷新

? ? ? ? ? ? ? ? .refreshAfterWrite(1, TimeUnit.SECONDS)

? ? ? ? ? ? ? ? //設(shè)置緩存容器的初始容量為10

? ? ? ? ? ? ? ? .initialCapacity(10)

? ? ? ? ? ? ? ? //設(shè)置緩存最大容量為100,超過100之后就會按照LRU最近雖少使用算法來移除緩存項(xiàng)

? ? ? ? ? ? ? ? .maximumSize(100)

? ? ? ? ? ? ? ? //設(shè)置要統(tǒng)計(jì)緩存的命中率

? ? ? ? ? ? ? ? .recordStats()

? ? ? ? ? ? ? ? //設(shè)置緩存的移除通知

? ? ? ? ? ? ? ? .removalListener(notification -> System.out.println(notification.getKey() + " 被移除了,原因: " + notification.getCause()))

? ? ? ? ? ? ? ? //build方法中可以指定CacheLoader,在緩存不存在時(shí)通過CacheLoader的實(shí)現(xiàn)自動加載緩存

? ? ? ? ? ? ? ? .build(

? ? ? ? ? ? ? ? ? ? ? ? new CacheLoader<String, User>() {

? ? ? ? ? ? ? ? ? ? ? ? ? ? @Override

? ? ? ? ? ? ? ? ? ? ? ? ? ? public User load(String key) {

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println("緩存沒有時(shí),從數(shù)據(jù)庫加載" + key);

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // TODO jdbc的代碼~~忽略掉

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? return new User("yinfeng" + key, key);

? ? ? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? );

? ? ? ? // 第一次讀取

? ? ? ? for (int i = 0; i < 20; i++) {

? ? ? ? ? ? User user = userCache.get("uid" + i);

? ? ? ? ? ? System.out.println(user);

? ? ? ? }

? ? ? ? // 第二次讀取

? ? ? ? for (int i = 0; i < 20; i++) {

? ? ? ? ? ? User user = userCache.get("uid" + i);

? ? ? ? ? ? System.out.println(user);

? ? ? ? }

? ? ? ? System.out.println("cache stats:");

? ? ? ? //最后打印緩存的命中率等 情況

? ? ? ? System.out.println(userCache.stats().toString());

? ? }

? ? @Data

? ? @AllArgsConstructor

? ? public static class User implements Serializable {

? ? ? ? private String userName;

? ? ? ? private String userId;

? ? ? ? @Override

? ? ? ? public String toString() {

? ? ? ? ? ? return userId + " --- " + userName;

? ? ? ? }

? ? }

}

5、分布式緩存

5.1redis

5.1.1介紹

Redis是一個(gè)開源的使用C語言縮寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型,Key-Value數(shù)據(jù)庫,并提供多種語言的API。

本質(zhì)是客戶端-服務(wù)端應(yīng)用軟件程序。

特點(diǎn)是使用簡單,性能強(qiáng)悍,功能應(yīng)用場景豐富。

5.1.2通用命令

命令作用

DEL key用于在key存在是刪除key

DUMP key序列化給定的key,并返回給定的值

EXISTS key檢查給定key是否存在

EXPIRE key seconds為給定key設(shè)置過期時(shí)間,單位秒

TTL key以秒為單位,返回給定key的剩余生存時(shí)間

TYPE key返回key所存儲的值的類型

5.1.3數(shù)據(jù)結(jié)構(gòu)

1.String

定義

String數(shù)據(jù)結(jié)構(gòu)是簡單的key-value類型,value其實(shí)不僅是String,也可以是數(shù)字。

使用場景:微博數(shù),粉絲數(shù)(常規(guī)計(jì)數(shù))

常用命令

命令作用

Get獲取指定key的值

Set設(shè)置指定key的值

Incr將key中儲存的數(shù)字值增一

Decr將key中儲存的數(shù)字值減一

Mget獲取所有(一個(gè)或多個(gè))給定key的值

2. List

定義

List就是鏈表,依賴于鏈表結(jié)構(gòu)

使用場景:微博的關(guān)注列表,粉絲列表

常用命令

命令作用

Lpush將一個(gè)或多個(gè)值插入到列表頭部

Rpush在列表中添加一個(gè)或多個(gè)值

Lpop移出并獲取列表的第一個(gè)元素

Rpop移除列表的最后一個(gè)元素,返回值為移除的元素

Lrange獲取所有(一個(gè)或多個(gè))給定key的值

3. Set

定義

Set就是一個(gè)集合,集合的概念就是一堆不重復(fù)值的組合。利用Reds提供的Set數(shù)據(jù)結(jié)構(gòu),可以存儲一些集合性的數(shù)據(jù)。

使用場景:實(shí)現(xiàn)如共同關(guān)注,共同喜好,二度好友

常用命令

命令作用

Lpush向集合中添加一個(gè)或多個(gè)成員

Rpush移除并返回集合中的一個(gè)隨機(jī)元素

Lpop返回集合中的所有成員

Rpop返回所有給定集合的并集

4. Sorted set

定義

Sorted set的使用場景與set類似,區(qū)別是set不是自動有序的,而sorted set可以通過用戶額外提供一個(gè)優(yōu)先級(score)的參數(shù)來為成員排序,并且是插入有序的,即自動排序。

使用場景:排行榜、按照用戶投票和時(shí)間排序

常用命令

命令作用

Zadd向有序集合添加一個(gè)或多個(gè)成員,或者更新已存在成員的分?jǐn)?shù)

Zrange通過索引區(qū)間返回有序集合中指定區(qū)間內(nèi)的成員

Zrem移除有序集合中的一個(gè)或多個(gè)成員

Zcard獲取有序集合的成員數(shù)

5. Hash

定義

Hash是一個(gè)sting類型的field和value的映射表

使用場景:存儲部分變更數(shù)據(jù),如用戶信息

常用命令

命令作用

Zadd獲取存儲在哈希表中指定字段的值

Zrange將哈希表key中的字段field的值設(shè)為value

Hgetall獲取在哈希表中指定key的所有字段和值

6. GEO

定義

GEO3.2版本開始對GEO(地理位置)的支持

使用場景:LBS應(yīng)用開發(fā)

常用命令

命令作用

GEOADD增加地理位置的坐標(biāo),可以批量添加地理位置

GEODIST獲取兩個(gè)地理位置的距離

GEOHASH獲取某個(gè)地理位置的geohash值

GEOPOS獲取指定位置的坐標(biāo),可以批量獲取多個(gè)地理位置的坐標(biāo)

GEORADIUS根據(jù)給定地理位置坐標(biāo)獲取指定范圍內(nèi)的地理位置集合(注意:該命令的中心點(diǎn)由輸入的經(jīng)度和結(jié)度決定)

GEORADIUSBYMEMBER根據(jù)給定成員的位置獲取指定范圍內(nèi)的位置信息集合(注意:該命令的中心點(diǎn)足由給定的位置元素決定)

7. Stream

定義

Stream5.0版本開始的新結(jié)構(gòu)“流”

使用場景:消費(fèi)者生產(chǎn)者場景(類似MO)

常用命令

命令作用

XADD增加地理位置的坐標(biāo),可以批量添加地理位置

XLENstream流中的消息數(shù)量

XDEL刪除流中的消息

XRANGE返回流中滿足給定ID范圍的消息

XREAD從一個(gè)或者多個(gè)流中讀取消息

XINFO檢索關(guān)于流和關(guān)聯(lián)的消費(fèi)者組的不同的信息

5.1.4持久化機(jī)制

1. 介紹

redis的數(shù)據(jù)都存放在內(nèi)存中,如果沒有配置持久化,重啟后數(shù)據(jù)就全丟失,于是需要開啟redis的持久化功能,將數(shù)據(jù)保存到磁盤上,當(dāng)redis重啟后,可以從磁盤中恢復(fù)數(shù)據(jù)

2. 持久化方式

RDB持久化:RDB持久化方式能夠在指定的時(shí)間間隔對你的數(shù)據(jù)進(jìn)行快照存儲

AOF持久化: AOF持久化方式記錄每次對服務(wù)器寫的操作,當(dāng)服務(wù)器重后的時(shí)候會重新執(zhí)行這些命令來恢復(fù)原始的數(shù)據(jù)

3.RDB方式

客戶端直接通過命令BGSAVE或者SAVE來創(chuàng)建一個(gè)內(nèi)存快照:

BGSAVE調(diào)用fork來創(chuàng)建一個(gè)子進(jìn)程,子進(jìn)程負(fù)責(zé)將快照寫入磁盤,而父進(jìn)程仍然繼續(xù)處理命令。

SAVE執(zhí)行SAVE命令過程中,不再響應(yīng)其他命令。

在redis.conf中調(diào)整save配置選項(xiàng),當(dāng)在規(guī)定的時(shí)間內(nèi),redis發(fā)生了寫操作的個(gè)數(shù)滿足條件會觸發(fā)BGSAVE命令

#900秒之內(nèi)至少一次寫操作

save 900 1

#300秒之內(nèi)至少發(fā)生10次寫操作

save 300 10

優(yōu)缺點(diǎn)

優(yōu)點(diǎn)缺點(diǎn)

對性能影響最小同步時(shí)丟失數(shù)據(jù)

RDB文件進(jìn)行數(shù)據(jù)恢復(fù)比使用AOF要快很多如果數(shù)據(jù)集非常大且CPU不夠強(qiáng)(比如單核CPU),Redis在fork子進(jìn)程時(shí)可能會消耗相對較長的時(shí)間,影響RediS對外提供服務(wù)的能力

4. AOF持久化方式

**開啟AOF持久化 **

appendonty yes

AOF策略調(diào)整

#每次有數(shù)據(jù)修改發(fā)生時(shí)都會寫入AOF文件

appendfsync always

#每秒鐘同步一次,該策略為AOF的默認(rèn)策略

appendfsync everysec

#從不同步。高效但是數(shù)據(jù)不會被持久化

appendfsync no

優(yōu)點(diǎn)

優(yōu)點(diǎn)缺點(diǎn)

最安全文件體積大

容災(zāi)性能消耗比RDB高

易讀,可修改數(shù)據(jù)恢復(fù)速度比RDB慢

5.1.5 內(nèi)存管理

1、內(nèi)存分配

不同數(shù)據(jù)類型的大小限制:

Strings類型:一個(gè)Strings類型的Value最大可以存儲512M。

Lists類型:list的元素個(gè)數(shù)最多為2^32-1個(gè)

Sets類型:元素個(gè)數(shù)最多為2^32-1個(gè)

Hashes類型:鍵值對個(gè)數(shù)最多為2^32-1個(gè)

最大內(nèi)存控制:

maxmemory 最大內(nèi)存閾值

maxmemory-policy 到達(dá)閾值的執(zhí)行策略

2、內(nèi)存壓縮

#配置字段最多512個(gè)

hash-max-zipmap-entries 512

#配置value最大為64字節(jié)

hash-max-zipmap-value 64

#配置元素個(gè)數(shù)最多512個(gè)

lst-max-zipmap-entries 512

#配置value最大為64字節(jié)

list-max-zipmap-value 64

#配置元素個(gè)數(shù)最多512個(gè)

set-max-zipmap-entries 512

大小超出壓縮范圍,溢出后redis將自動將其轉(zhuǎn)換為正常大小

3、過期數(shù)據(jù)的處理策略

主動處理(redis主動觸發(fā)檢測key足否過期)每秒抗行10次。過程如下:

從具有相關(guān)過期的key集合中測試20個(gè)隨機(jī)key

刪除找到的所有已過期key

如果超過25%的key已過期,請從步驟1重新開始

被動處理:

每次訪問key的時(shí)候,發(fā)現(xiàn)超時(shí)后被動過期,清理掉

數(shù)據(jù)恢復(fù)階段過期數(shù)據(jù)的處理策略:

RDB方式:過期的Key不會被持久化到文件中。載入時(shí)過期的key,會通過redis的主動和被動方式清理掉。

AOF方式:每次遇到過期的key,redis會追加一條DEL命令到AOF文件,也就是說只要我們順序載入執(zhí)行AOF命令文件就會刪除過期的key

注意:過期數(shù)據(jù)的計(jì)算和計(jì)算機(jī)本身的時(shí)間是有直接聯(lián)系的!

Redis內(nèi)存回收策略:

配置文件中設(shè)置:maxmemory-poIicy noeviction

命令動態(tài)調(diào)整:config set maxmemory-policy noeviction

回收策略說明

noeviction客戶端嘗試執(zhí)行會讓更多內(nèi)存被使用的命令直接報(bào)錯(cuò)

allkeys-lru在所有key里執(zhí)行LRU算法清除

volatile-lru在所有已經(jīng)過期的key里執(zhí)行LRU算法清除

allkeys-lfu在所有key里執(zhí)行LFU算法清除

volatile-lfu在所有已經(jīng)過期的key里執(zhí)行LFU算法清除

allkeys-random在所有key里隨機(jī)回收

volatile-random在已經(jīng)過期的key里隨機(jī)回收

volatile-ttl回收已經(jīng)過期的key,并且優(yōu)先回收存活時(shí)間(TTL)較短的key

4、LRU算法

LRU(最近最少使用):根據(jù)數(shù)據(jù)的歷史訪問記錄來進(jìn)行溝汰數(shù)據(jù)

核心思想:如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高。

注意:Redis的LRU算法并非完整的實(shí)現(xiàn),完整的LRU實(shí)現(xiàn)需要太多的內(nèi)存。

方法:通過對少量keys進(jìn)行取樣(50%),然后回收其中一個(gè)最好的key。

配置方式:maxmemory-samples 5

5、LFU算法

LFU:根據(jù)數(shù)據(jù)的歷史訪問頻率來溝汰數(shù)據(jù)

核心思想:如果數(shù)據(jù)過去被訪問多次,那么將來被訪問的頻率也更高。

啟用LFU算法后,可以使用熱點(diǎn)數(shù)據(jù)分析功能。

5.1.6 主從復(fù)制

1、介紹

為什么要主從復(fù)制

redis-server單點(diǎn)故障

單節(jié)點(diǎn)QPS有限

應(yīng)用場景分析

讀寫分離場景,規(guī)避redis單機(jī)瓶頸

故障切換,master出問題后還有slave節(jié)點(diǎn)可以使用

2、搭建主從復(fù)制

主Redis Server以普通模式啟動,主要是啟動從服務(wù)器的方式

命令行

#連接需要實(shí)現(xiàn)從節(jié)點(diǎn)的rediS,執(zhí)行下面的命令

slaveof [ip] [port]

redis.conf配置文件

#配置文件中增加

slaveof [ip] [port]

#從服務(wù)器是否只讀(默認(rèn)yes)

slave-read-only yes

退出主從集群的方式

slaveof no one

3、檢查主從復(fù)制

#redis客戶端執(zhí)行

info replication

4、主從復(fù)制流程

從服務(wù)器通過psync命令發(fā)送服務(wù)器已有的同步進(jìn)度(同步源ID,同步進(jìn)度offset)

master收到請求,同步源為當(dāng)前master,則根據(jù)偏移量增量同步

同步源非當(dāng)前master,則進(jìn)入全量同步:maser生成rdb,傳輸?shù)絪lave,加載到slave內(nèi)存

5、主從復(fù)制核心知識

Redis默認(rèn)使用異步復(fù)制,slave和master之間異步地確認(rèn)處理的數(shù)據(jù)量

一個(gè)master可以擁有多個(gè)slave

Slave可以接受其他slave的連接。slave可以有下級sub slave

主從同步過程在master側(cè)是非阻塞的

slave初次同步需要?jiǎng)h除舊數(shù)據(jù),加載新數(shù)據(jù),會阻塞到來的連接請求

6、應(yīng)用場景

主從復(fù)制可以用來支持讀寫分離

slave服務(wù)器設(shè)定為只讀,可以用在數(shù)據(jù)安全的場景下。

可以使用主從復(fù)制來避免master持久化造成的開銷。master關(guān)閉持久化,slave設(shè)置為不定期保存或開啟AOF

注意:重新啟動的master程序?qū)囊粋€(gè)空數(shù)據(jù)集開始,如果一個(gè)slave試圖與它同步,那么這個(gè)slave也會被清空。

7、注意事項(xiàng)

讀寫分離場景:

數(shù)據(jù)復(fù)制延時(shí)導(dǎo)致讀到過期數(shù)據(jù)或者讀不到數(shù)據(jù)(網(wǎng)絡(luò)原因,slave阻塞)

從節(jié)點(diǎn)故障(多個(gè)client如何遷移)

全量復(fù)制情況下:

第一次建立主從關(guān)系或者runid不匹配會導(dǎo)致全量復(fù)制

故障轉(zhuǎn)移的時(shí)候也會出現(xiàn)全量復(fù)制

復(fù)制風(fēng)暴:

master故障重啟,如果slave節(jié)點(diǎn)過多,所有slave都要復(fù)制,對服務(wù)器的性能,網(wǎng)絡(luò)的壓力都有很大影響。

如果一個(gè)機(jī)器部署了多個(gè)master

寫能力有限

主從復(fù)制還是只有一臺master,提供的寫服務(wù)能力有限

master故障情況下:

如果是mater無持久化,Slave開啟持久化來保留數(shù)據(jù)的場展,建議不要配置redis自動重啟。

啟動redis自動重啟,master啟動后,無備份數(shù)據(jù),可能導(dǎo)致集群數(shù)據(jù)丟失的情況

帶有效期的key:

Slave不會讓key過期,而是等待master讓key過期

在Lua腳本執(zhí)行期間,不執(zhí)行任何key過期操作

5.1.7 哨兵模式

1、哨兵(Sentinel)機(jī)制核心作用

客戶端詢問主redis地址> redis哨兵監(jiān)控、提醒、故障轉(zhuǎn)移(主從切換)> 主redis(master)主從復(fù)制關(guān)系> 從redis(slave)

2、核心運(yùn)作流程

服務(wù)發(fā)現(xiàn)和健康檢查流程

搭建redis主從集群 ==> 啟動哨兵(客戶端通過哨兵發(fā)現(xiàn)Redis實(shí)例信息) ==> 哨兵通過連接master發(fā)現(xiàn)主從集群內(nèi)的所有實(shí)例信息 ==> 哨兵監(jiān)控redis實(shí)例的健康狀況

故障切換流程

哨兵一旦發(fā)現(xiàn)master不能正常提供服務(wù),則通知給其他哨兵 ==> 當(dāng)一定數(shù)量的哨兵都認(rèn)為master掛了 ==> 選舉一個(gè)哨兵作為故障轉(zhuǎn)移的執(zhí)行者 ==> 執(zhí)行者在slave中選取一個(gè)作為新的master ==> 將其他slave重新設(shè)定為新master的從屬

3、哨兵如何知道Redis主從信息

哨兵配置文件中,保存著主從集群中master的信息,可以通過info replication命令,進(jìn)行主從信息自動發(fā)現(xiàn)。

4、什么是主觀下線(sdown)

主觀下線:單個(gè)哨兵自身認(rèn)為redis實(shí)例已經(jīng)不能提供服務(wù)

檢測機(jī)制:哨兵向redis發(fā)送ping請求,+PONG,-LOADING,-MASTERDOWN三種情況視為正常,其他回復(fù)均視為無效

對應(yīng)配置文件的配置項(xiàng):sentinel down-after-milliseconds mymaster 1000

5、什么是客觀下線(odown)

客觀下線:一定數(shù)量值的哨兵認(rèn)為master已經(jīng)下線。

檢測機(jī)制:當(dāng)哨兵主觀認(rèn)為maser下線后,則會通過SENTINEL is-master-down-by-addr命令詢問其他哨兵是否認(rèn)為master已經(jīng)下線,如果達(dá)成共識(達(dá)到quorum個(gè)數(shù)),就會認(rèn)為master節(jié)點(diǎn)客觀下線,開始故障轉(zhuǎn)移流程

對應(yīng)配置文件的配置項(xiàng):sentinel monitor mymaster 1.0.0.1 6380 2

6、哨兵之間如何通信

哨兵之間的自動發(fā)現(xiàn):發(fā)布自己的信息,訂閱其他哨兵消息(pub/sub)

哨兵之間通過命令進(jìn)行通信:直連發(fā)送命令

哨兵之間通過訂閱發(fā)布進(jìn)行通信:相互訂閱指定主題(pub/sub)

7、哨兵領(lǐng)導(dǎo)選舉機(jī)制

基于Raft算法實(shí)現(xiàn)的選舉機(jī)制,流程簡述如下:

拉票階段:每個(gè)哨兵節(jié)點(diǎn)希望自己成為領(lǐng)導(dǎo)者;

Sentinel節(jié)點(diǎn)收到拉票命令后,如果沒有收到或同意過其他sentinel節(jié)點(diǎn)的請求,就同意該sentinel節(jié)點(diǎn)的請求(每個(gè)sentinel只持有一個(gè)同意票數(shù))

如果sentinel節(jié)點(diǎn)發(fā)現(xiàn)自己的票數(shù)已經(jīng)超過一半的數(shù)值,那么它將成為領(lǐng)導(dǎo)者,去執(zhí)行故障轉(zhuǎn)移

投票結(jié)束后,如果超過failover-timeout的時(shí)間內(nèi),沒進(jìn)行實(shí)際的故障轉(zhuǎn)移操作,則重新拉票選舉。

8、slave選舉方案

slave節(jié)點(diǎn)狀態(tài) > 優(yōu)先級 > 數(shù)據(jù)同步情況 > 最小的run id

9、最終主從切換的過程

針對即將成為master的slave節(jié)點(diǎn),將其撒出主從集群,自動執(zhí)行:slaveof NO ONE

針對其他slave節(jié)點(diǎn),使它們成為新master的從屬,自動執(zhí)行:slaveof new_master_host new_master_port

10、哨兵服務(wù)部署方案

不推薦:一主一從,兩個(gè)哨兵

推薦:一主兩從,三個(gè)哨兵

redis集群非強(qiáng)一致:一主兩從,網(wǎng)絡(luò)分區(qū)下可能出現(xiàn)數(shù)據(jù)不一致或丟失。

5.1.8 redis集群分片存儲

1、為什么要分片存儲

redis的內(nèi)存需求可能超過機(jī)器的最大內(nèi)存。(一臺機(jī)器不夠用)

2、官方集群方案

redis cluster是redis的分布式集科解決方案,在3.0版本推出后有效地解決了redis分布式分面的需求,實(shí)現(xiàn)了數(shù)據(jù)在多個(gè)Redis節(jié)點(diǎn)之間自動分片,故障自動轉(zhuǎn)移,擴(kuò)容機(jī)制等功能。

主要基于CRC16(key) % 16384 計(jì)算出每個(gè)key對應(yīng)的slot,然后根據(jù)redis集群中實(shí)例的預(yù)設(shè)槽slot(16384個(gè))進(jìn)行對應(yīng)的操作,slot不存儲數(shù)據(jù),僅僅用來做片區(qū)劃分。

3、搭建集群

準(zhǔn)備6個(gè)獨(dú)立的redis服務(wù)

通過redis-cli工具創(chuàng)建集群

檢驗(yàn)集群

故障轉(zhuǎn)移測試

集群擴(kuò)容

集群節(jié)點(diǎn)刪除

4、集群關(guān)心的問題

增加了slot槽的計(jì)算,是不是比單機(jī)性能差?

不是的,為了避免每次都需要服務(wù)器計(jì)算重定向,優(yōu)秀的Java客戶端都實(shí)現(xiàn)了本地計(jì)算,并且緩存服務(wù)器slots分配,有變動時(shí)再更新本地內(nèi)容,從而避免了多次重定向帶來的性能損耗。

redis集群大小,到底可以裝多少數(shù)據(jù)?

理論是可以做到16384個(gè)槽,每個(gè)槽對應(yīng)一個(gè)實(shí)例,但是redis宮方建議是最大1000個(gè)實(shí)例,因?yàn)榇鎯σ呀?jīng)足夠大了。

集群節(jié)點(diǎn)間是怎么通信的?

每個(gè)Redis群集節(jié)點(diǎn)都有一個(gè)額外的TCP端口,每個(gè)節(jié)點(diǎn)使用TCP連接與每個(gè)其他節(jié)點(diǎn)連接。檢測和故障轉(zhuǎn)移這些步驟基本和哨兵模式類似。

ask和moved重定向的區(qū)別

重定向包括兩種情況

若確定slot不屬于當(dāng)前節(jié)點(diǎn),redis會返回moved。

若當(dāng)前redis節(jié)點(diǎn)正在處理slot遷移,則代表此處請求對應(yīng)的key暫時(shí)不在此節(jié)點(diǎn),返回ask,告訴客戶端本次請求重定向。

數(shù)據(jù)傾斜和訪問傾斜的問題

傾斜導(dǎo)致集群中部分節(jié)點(diǎn)數(shù)據(jù)多,壓力大。解決方案分為前期和后期:

前期是業(yè)務(wù)層面提前預(yù)測,哪些key是熱點(diǎn),在設(shè)計(jì)的過程中規(guī)避。

后期是slot遷移,盡量將壓力分?jǐn)?slot調(diào)整有自動rebalance、reshard和手動)。

slot手動遷移怎么做?

在遷移目的節(jié)點(diǎn)執(zhí)行cluster setslot?IMPORTING?命令,指明需要遷移的slot和遷移源節(jié)點(diǎn)。

在遷移源節(jié)點(diǎn)執(zhí)行cluster setslot?MIGRATING?命令,指明需要遷移的slot和遷移目的節(jié)點(diǎn)。

在遷移源節(jié)點(diǎn)執(zhí)行cluster getkeysinslot獲取該slot的key列表

在遷移源節(jié)點(diǎn)執(zhí)行對每個(gè)key執(zhí)行migrate命令,該命令會同步把該key遷移到目的節(jié)點(diǎn)。

在遷移源節(jié)點(diǎn)反復(fù)執(zhí)行cluster getkeysinslo命令,直到該slot的列表為空。

在遷移源節(jié)點(diǎn)和目的節(jié)點(diǎn)執(zhí)行cluster setslot?NODE?,完成遷移操作。

節(jié)點(diǎn)之間會交換信息,傳遞的消息包括槽的信息,帶來帶寬消耗。注意:避免使用大的一個(gè)集群,可以分多個(gè)集群。

Pub/Sub發(fā)布訂閱機(jī)制:對集群內(nèi)任意的一個(gè)節(jié)點(diǎn)執(zhí)行pubish發(fā)布消息,這個(gè)消息會在集群中進(jìn)行傳播,其他節(jié)點(diǎn)都接收到發(fā)布的消息。

讀寫分離:

redis-cluster默認(rèn)所有從節(jié)點(diǎn)上的讀寫,都會重定向到key對應(yīng)槽的主節(jié)點(diǎn)上。

可以通過readonly設(shè)置當(dāng)前連接可讀,通過readwrite取消當(dāng)前連接的可讀狀態(tài)。

注意:主從節(jié)點(diǎn)依然存在數(shù)據(jù)不一致的問題

5.1.9 redis監(jiān)控

1、monitor命令

monitor是一個(gè)調(diào)試命令,返回服務(wù)器處理的每個(gè)命令。對于發(fā)現(xiàn)程序的錯(cuò)誤非常有用。出于安全考慮,某些特殊管理命令CONFIG不會記錄到MONITOR輸出。

注意:運(yùn)行一個(gè)MONITOR命令能夠降低50%的吞吐量,運(yùn)行多個(gè)MONITOR命令降低的吞吐量更多。

2、info命令

INFO命令以一種易于理解和閱讀的格式,返回關(guān)于Redis服務(wù)器的各種信息和統(tǒng)計(jì)數(shù)值。

info命令返回信息

serverRedis服務(wù)器的一般信息

clients客戶端的連接部分

memory內(nèi)存消耗相關(guān)信息

persistence持久化相關(guān)信息

stats一般統(tǒng)計(jì)

replication主/從復(fù)制信息

cpu統(tǒng)計(jì)CPU的消耗

commandstatsRedis命令統(tǒng)計(jì)

clusterRedis集群信息

keyspace數(shù)據(jù)庫的相關(guān)統(tǒng)計(jì)

可以通過section返回部分信息,如果沒有使用任何參數(shù)時(shí),默認(rèn)為detault。

3、圖形化監(jiān)控工具: Redis-Live

5.2 memcached入門

由于memcached慢慢淡出了人們的視野,使用的公司越來越少,所以這里只是做個(gè)入門介紹。

1、簡介

是一個(gè)免費(fèi)開源的、高性能的、具有分布式內(nèi)存對象的緩存系統(tǒng),它通過減輕數(shù)據(jù)庫負(fù)載加速動態(tài)web應(yīng)用。

本質(zhì)上就是一個(gè)內(nèi)存key-Value緩存

協(xié)議簡單,使用的是基于文本行的協(xié)議

不支持?jǐn)?shù)據(jù)的持久化,服務(wù)器關(guān)閉之后數(shù)據(jù)全部丟失

Memcached簡潔而強(qiáng)大,便于快速開發(fā),上手較為容易

沒有安全機(jī)制

2、設(shè)計(jì)理念

簡單的鍵/值存儲:服務(wù)器不關(guān)心你的數(shù)據(jù)是什么樣的,只管數(shù)據(jù)存儲

服務(wù)端功能簡單,很多邏輯依賴客戶端實(shí)現(xiàn)

客戶端專注如何選擇讀取或?qū)懭氲姆?wù)器,以及無法聯(lián)系服務(wù)器時(shí)要執(zhí)行的操作。

服務(wù)器專注如何存儲和管理何時(shí)清除或重用內(nèi)存

Memcached實(shí)例之間沒有通信機(jī)制

每個(gè)命令的復(fù)雜度為0(1):慢速機(jī)器上的查詢應(yīng)該在1ms以下運(yùn)行。高端服務(wù)器的吞吐量可以達(dá)到每秒數(shù)百萬

緩存自動清除機(jī)制

緩存失效機(jī)制

3、常用命令

分組命令描述

存儲命令set用于將value存儲在指定的key中。key已經(jīng)存在,更新該key所對應(yīng)的原來的數(shù)據(jù)。

add用于將value存儲在指定的key中,存在則不更新。

replace替換已存在的key的Value,不存在,則替換失敗。

append用于向已存在key的value后面追加數(shù)據(jù)

prepend向已存在key的value前面追加數(shù)據(jù)

cas比較和替換,比對后,沒有被其他客戶端修改的情況下才能寫入。

檢索命令get獲取存儲在key中的value,不存在,則返回空。

gets獲取帶有CAS令牌存的value,若key不存在,則返回為空

刪除delete刪除已存在的key

計(jì)算incr/decr對已存在的key的數(shù)字值進(jìn)行自增或自減操作

統(tǒng)計(jì)stats返回統(tǒng)計(jì)信息如PID(進(jìn)程號)、版本號、連接數(shù)等

stats items顯示各個(gè)slab中item的數(shù)目和存儲時(shí)長(最后一次訪問距離現(xiàn)在的秒數(shù))

stats slabs顯示各個(gè)slab的信息,包括chunk的大小、數(shù)目、使用情況等。

stats sizes顯示所有item的大小和個(gè)數(shù)

清除flush_all清除所有內(nèi)容

4、客戶端使用

客戶端支持的特性:集群下多服務(wù)器選擇,節(jié)點(diǎn)權(quán)重配置,失敗/故障轉(zhuǎn)移,數(shù)據(jù)壓縮,連接管理

5、服務(wù)端配置

命令行參數(shù)

查看memcached-h或man memcached獲取最新文檔

init腳本

如果通過yum應(yīng)用商店安裝,可以使用/etc/sysconfig/memcached文件進(jìn)行參數(shù)配置

檢查運(yùn)行配置

stats settings查看運(yùn)行中的memcached的配置

6、memcached性能

Memcached性能的關(guān)鍵是硬件,內(nèi)部實(shí)現(xiàn)是hash表,讀寫操作都是0(1)。硬件好,幾百萬的OPS都是沒問題的。

最大連接數(shù)限制:內(nèi)部基于事件機(jī)制(類似JAVA NIO)所以這個(gè)限制和nio類似,只要內(nèi)存,操作系統(tǒng)參數(shù)進(jìn)行調(diào)整,輕松幾十萬。

集群節(jié)點(diǎn)數(shù)量限制:理論是沒限制的,但是節(jié)點(diǎn)越多,客戶端需要建立的連接就會越多。

注意:memcached服務(wù)端沒有分布式的功能,所以不論是集群還是主從備份,都需要第三方產(chǎn)品支持。

7、服務(wù)器硬件需要

CPU要求:CPU占用率低,默認(rèn)為4個(gè)工作線程

內(nèi)存要求:

memcached內(nèi)容存在內(nèi)存里面,所有內(nèi)存使用率高。

建議memcached實(shí)例獨(dú)占服務(wù)器,而不是混用。

建議每個(gè)memcached實(shí)例內(nèi)存大小都足一致的,如果不一致則需要進(jìn)行權(quán)重調(diào)整

網(wǎng)絡(luò)要求:

根據(jù)項(xiàng)目傳輸?shù)膬?nèi)容來定,網(wǎng)絡(luò)越大越好,雖然通常10M就夠用了

建議:項(xiàng)目往memcached傳輸?shù)膬?nèi)容保持盡可能的小

8、Memcached應(yīng)用場景

數(shù)據(jù)查詢緩存:將數(shù)據(jù)庫中的數(shù)據(jù)加載到memcached,提供程序的訪問速度

計(jì)數(shù)器的場景:通過incr/decr命令實(shí)現(xiàn)評論數(shù)量、點(diǎn)擊數(shù)統(tǒng)計(jì),操作次數(shù)等等場景。

樂觀鎖實(shí)現(xiàn):例如計(jì)劃任務(wù)多實(shí)例部暑的場景下,通過CAS實(shí)現(xiàn)不重復(fù)執(zhí)行

防止重復(fù)處理:CAS命令

5.3 互聯(lián)網(wǎng)高并發(fā)緩存架構(gòu)

5.3.1 緩存架構(gòu)分析圖

5.3.2 緩存雪崩

定義:因?yàn)榫彺娣?wù)掛掉或者熱點(diǎn)緩存失效,從而導(dǎo)致所有請求都去查數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫連接不夠用或者數(shù)據(jù)庫處理不過來,從而導(dǎo)致整個(gè)系統(tǒng)不可用。

常用解決方案:

緩存數(shù)據(jù)的過期時(shí)間設(shè)置隨機(jī),防止同一時(shí)間大量數(shù)據(jù)過期現(xiàn)象發(fā)生。

緩存降級,直接返回錯(cuò)誤碼;

加鎖實(shí)現(xiàn)防止大量請求堆到數(shù)據(jù)庫。

設(shè)置熱點(diǎn)數(shù)據(jù)永遠(yuǎn)不過期,防止了自動失效的情況,通過其他后臺檢查程序,防止緩存數(shù)據(jù)和數(shù)據(jù)庫長期不同步

5.3.2 緩存擊穿

定義:查詢必然不存在的數(shù)據(jù),請求透過Redis,直擊數(shù)據(jù)庫。

常用解決方案:

用戶內(nèi)容預(yù)生成。

訪問頻率限制。

緩存中無數(shù)據(jù),也不查詢數(shù)據(jù)庫,直接返回錯(cuò)誤碼。

布隆過濾器

?需要大廠面試資料的可以私信我扣666喲

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容