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喲