Redis緩存使用詳解

Mybatis Redis 緩存

1-1.mybatis一級,二級緩存

一級緩存:

	一級緩存基於sqlSession默認開啓,在操作數據庫時需要構造SqlSession對象,在對象中有一個HashMap用於存儲緩存數據。不同的SqlSession之間的緩存數據區域是互相不影響的

二級緩存:

	二級緩存的作用域是mapper的同一個namespace。不同的sqlSession兩次執行相同的namespace下的sql語句,且向sql中傳遞的參數也相同,即最終執行相同的sql語句,則第一次執行完畢會將數據庫中查詢的數據寫到緩存,第二次查詢會從緩存中獲取數據,不再去底層數據庫查詢,從而提高效率,多表聯合查詢時會造成髒讀的情況	 

1-2.mybatis存在的問題

緩存在自己的服務器上

1.不能做分佈式的緩存

2.佔用服務器的資源

2.使用緩存存在的問題

2.1緩存穿透

概念:

	是指查詢數據庫中一定不存在的數據。先在緩存中查詢,如果key不存在或者key已經過期,再對數據庫進行查詢,並把查詢到的對象,放進緩存中。如果數據庫查詢對象空,則不放進緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,造成緩存穿透。

解決辦法:

	1.布隆過濾

	最常見的則是採用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的bitmap中,對所有可能查詢的參數以hash形式存儲,在控制層先進行校驗,不符合則丟棄,一個一定不存在的數據會被bitmap攔截掉從而避免了對底層存儲系統的查詢壓力。

	2.強而有力

		訪問key未在DB查詢到的空值寫進緩存,設置較短過期時間

2.2緩存雪崩

概念:

	大量的key設置了相同的過期時間,導致在緩存在同一時刻全部失效,造成瞬時DB請求量大、壓力驟	增,引起雪崩。

解決辦法:

	可以給緩存設置過期時間時加上一個隨機值時間,使得每個key的過期時間分佈開來,不會集中在同一時	刻失效。

Redis

1.簡介

1.1簡介

Redis 是完全開源免費的,遵守BSD協議,是一個高性能的key-value數據庫。

Redis 與其他 key - value 緩存產品有以下三個特點:
  • Redis支持數據的持久化,可以將內存中的數據保存在磁盤中,重啓的時候可以再次加載進行使用。
  • Redis不僅僅支持簡單的key-value類型的數據,同時還提供string,list,set,zset,hash等數據結構的存儲。
  • Redis支持數據的備份,即master-slave模式的數據備份。

1.2Redis的5種數據類型及其適用場景

(1)String:可以包含任何數據,比如jpg圖片或者序列化的對象。

(2)List:鏈表(雙向鏈表),增刪快,提供了操作某一段元素的API。適用於:最新消息排行等功能;消息隊列。

(3)Set:集合。哈希表實現,元素不重複,爲集合提供了求交集、並集、差集等操作。適用於:共同好友;利用唯一性,統計訪問網站的所有獨立ip;好友推薦時,根據tag求交集,大於某個閾值就可以推薦。

(4)Hash 鍵值對集合,即編程語言中的Map類型。適合存儲對象,並且可以像數據庫中update一個屬 性一樣只修改某一項屬性值。適用於:存儲、讀取、修改用戶屬性。

(5)Sorted Set:有序集合。將Set中的元素增加一個權重參數score,元素按score有序排列。數據插入集合時,已經進行天然排序。適用於:排行榜;帶權重的消息隊列。

1.3優勢

  • 性能極高 – Redis能讀的速度是110000次/s,寫的速度是81000次/s 。
  • 豐富的數據類型 – Redis支持二進制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 數據類型操作。
  • 原子 – Redis的所有操作都是原子性的,意思就是要麼成功執行要麼失敗完全不執行。單個操作是原子性的。多個操作也支持事務,即原子性.
  • 豐富的特性 – Redis還支持 publish/subscribe, 通知, key 過期等等特性。

1.4其他key-value存儲有什麼不同?

  • Redis有着更爲複雜的數據結構並且提供對他們的原子性操作,這是一個不同於其他數據庫的進化路徑。Redis的數據類型都是基於基本數據結構的同時對程序員透明,無需進行額外的抽象。
  • Redis運行在內存中但是可以持久化到磁盤,所以在對不同數據集進行高速讀寫時需要權衡內存,因爲數據量不能大於硬件內存。在內存數據庫方面的另一個優點是,相比在磁盤上相同的複雜的數據結構,在內存中操作起來非常簡單,這樣Redis可以做很多內部複雜性很強的事情。同時,在磁盤格式方面他們是緊湊的以追加的方式產生的,因爲他們並不需要進行隨機訪問。

1.5Redis的特點

  • Redis是一個高性能key/value內存型數據庫

  • Redis支持豐富的數據類型如(String,list,set,zset,hash)

          key(string) value(String list hash zset set)
    
  • Redis支持持久化

  • Redis單線程,效率高 (redis 單線程效率發揮極致)

  • (Memcache key value (String Object) 多線程 併發操作同一個數據 內部線程鎖 c)

1.6基於aop,現實通用的緩存

aop的機制:

AOP是Spring框架面向切面的編程思想,AOP採用一種稱爲“橫切”的技術,將涉及多業務流程的通用功能抽取並單獨封裝,形成獨立的切面,在合適的時機將這些切面橫向切入到業務流程指定的位置中。

切入點表達式

語法結構: execution( 方法修飾符 方法返回值 方法所屬類 匹配方法名 ( 方法中的形參表 ) 方法申明拋出的異常 )

eg:

execution(* com.baizhi.service…query(…))

自定義註解

@redisCaChe

2.下載

  1. redis主頁: www.redis.io
  2. 下載redis的最新資料包
  3. 將下載redis資料包上傳到Linux中將壓縮包在linux系統中解壓
  4. 解壓完成後進入解壓目錄直接的目錄中執行make即可

3.安裝,啓動

1.創建虛擬機

2.安裝linux鏡像

3.啓動虛擬機設置密碼相關參數

4.在linux系統中設置自動獲取ip地址

	vi /etc/sysconfig/network-scripts/ifcfg-ens33

	修改:ONBOOT=YES

5.重啓虛擬機 reboot

2.克隆虛擬機

3.CRT連接虛擬機

1.安裝gcc運行環境

		yum -y install gcc  #redis是C語言寫的

2.vim的安裝

        yum install -y vim*  #//在線安裝vim

3.上傳redis的資料包

		在CRT中 alt+p , 直接拖進

4.解壓redis的壓縮包

		tar -zxvf xxx.zip

5.安裝包不能直接用,需要進入redis包執行指令編譯redis

		make
   		#如果遇到致命錯誤:執行    
   		make MALLOC=libc

6.編譯完成之後安裝redis

		make install PREFIX=/usr/redis

7.啓動redis bin目錄下執行

		./redis-server 
		#默認執行配置文件  /path/to/redis.conf
		
	
		#將配置文件拷貝到 /usr/redis目錄    進入到家目錄解壓完成之後的redis中執行
		cp redis.conf /usr/redis
		#指定配置文件啓動  usr/bin目錄下執行
		./redis-server  ../redis.conf

8.修改啓動端口

	#將redis.conf中的默認端口修改爲指定端口
	
	port 6379    port 7000

9.使用redis的客戶端連接redis服務 bin目錄下

		#默認連接
		./redis-cli
		
		#指定端口連接
		./redis-cli –p 6379 

10.守護模式模式啓動

		#將redis.conf中的默認端口修改爲指定端口
		daemonize no    改爲 daemonize yes

11.關閉防火牆

		service iptables stop

		systemctl stop firewalld

12.查看進程

		ps aux|grep redis
		
		#殺死進程
		kill -9 8989

4.redis常用指令

說明 : 使用redis的默認配置器動redis服務後,默認會存在16個庫,編號從0-15

可以使用select 庫的編號 來選擇一個redis的庫

切換庫  select 庫名   select 1
查看值的類型  type key
設置一個key/value      set   
根據key獲得對應的value  get  
清空當前庫    flushdb
清空全部庫    flushall

5.使用

6.Redis中的持久化機制

說明 : Redis提供了兩種不同的持久化方法來將數據存儲到硬盤裏面

  • 快照(snapshotting)

這種方式可以將某一時刻的所有數據都寫入硬盤中,當然這也是redis的默認持久化方式,保存的文件是以.rdb形式結尾的文件因此這種方式也稱之爲RDB方式

  • AOF(append only file)只追加文件

這種方式可以將所有客戶端執行的寫命令記錄到日誌文件中

6.1快照持久化

a) 快照持久化也是redis中的默認開啓的持久化方案, 根據redis.conf中的配置,快照將被寫入dbfilename指定的文件裏面(默認是dump.rdb文件中)

b) 根據redis.conf中的配置,快照將保存在dir選項指定的路徑上

c) 創建快照的幾種方式

  • 客戶端可以使用BGSAVE命令來創建一個快照,當接收到客戶端的BGSAVE命令時,redis會調用fork¹來創建一個子進程,然後子進程負責將快照寫入磁盤中,而父進程則繼續處理命令請求
  • 客戶端還可以使用SAVE命令來創建一個快照,接收到SAVE命令的redis服務器在快照創建完畢之前將不再響應任何其他的命令

注意 : SAVE命令並不常用,使用SAVE命令在快照創建完畢之前,redis處於阻塞狀態,無法對外服務

名詞解釋 : fork當一個進程創建子進程的時候,底層的操作系統會創建該進程的一個副本,在類unix系統中創建子進程的操作會進行優化:在剛開始的時候,父子進程共享相同內存,直到父進程或子進程對內存進行了寫之後,對被寫入的內存的共享纔會結束服務

  • 如果用戶在redis.conf中設置了save配置選項,redis會在save選項條件滿足之後自動觸發一次BGSAVE命令,如果設置多個save配置選項,當任意一個save配置選項條件滿足,redis也會觸發一次BGSAVE命令
  • 當redis通過shutdown指令接收到關閉服務器的請求時,會執行一個save命令,阻塞所有的客戶端,不再執行客戶端執行發送的任何命令,並且在save命令執行完畢之後關閉服務器

6.2AOF持久化機制

6.2.1.在redis的默認配置中AOF持久化機制 是沒有開啓的,AOF持久化會將被執行的寫命令寫到AOF的文件末尾,以此來記錄數據發生的變化,因此只要redis從頭到尾執行一次AOF文件所包含的所有寫命令,就可以恢復AOF文件的記錄的數據集.

6.2.2.開啓AOF持久化機制,需要修改redis.conf的配置文件,

  • 通過修改redis.conf配置中appendonly yes來開啓AOF持久化
  • 通過appendfilename指定日誌文件名字(默認爲:appendonly.aof)
  • 通過appendfsync指定日誌記錄頻率

6.2.3.AOF 日誌記錄頻率的選項

6.2.3.1.可選項

選項 同步頻率
always 每個redis寫命令都要同步寫入硬盤,嚴重降低redis速度
everysec 每秒執行一次同步顯式的將多個寫命令同步到磁盤
no 由操作系統決定何時同步

6.2.3.2.三種日誌記錄頻率的詳細分析 :

  • 如果用戶使用了always選項,那麼每個redis寫命令都會被寫入硬盤,從而將發生系統崩潰時出現的數據丟失減到最少;遺憾的是,因爲這種同步策略需要對硬盤進行大量的寫入操作,所以redis處理命令的速度會受到硬盤性能的限制;

注意 : 轉盤式硬盤在這種頻率下200左右個命令/s ; 固態硬盤(SSD) 幾百萬個命令/s;

警告 : 使用SSD用戶請謹慎使用always選項,這種模式不斷寫入少量數據的做法有可能會引發嚴重的寫入放大問題,導致將固態硬盤的壽命從原來的幾年降低爲幾個月

  • 爲了兼顧數據安全和寫入性能,用戶可以考慮使用everysec選項,讓redis每秒一次的頻率對AOF文件進行同步;redis每秒同步一次AOF文件時性能和不使用任何持久化特性時的性能相差無幾,而通過每秒同步一次AOF文件,redis可以保證,即使系統崩潰,用戶最多丟失一秒之內產生的數據(推薦使用這種方式)

  • 最後使用no選項,將完全有操作系統決定什麼時候同步AOF日誌文件,這個選項不會對redis性能帶來影響但是系統崩潰時,會丟失不定數量的數據

  • 另外如果用戶硬盤處理寫入操作不夠快的話,當緩衝區被等待寫入硬盤數據填滿時,redis會處於阻塞狀態,並導致redis的處理命令請求的速度變慢(不推薦使用)

7.AOF文件的重寫

aof 的方式也同時帶來了另一個問題。持久化文件會變的越來越大。例如我們調用incr test命令100次,文件中必須保存全部的100條命令,其實有99條都是多餘的。因爲要恢復數據庫的狀態其實文件中保存一條set test 100就夠了。爲了壓縮aof的持久化文件Redis提供了AOF重寫機制

7.1.重寫 aof 文件的兩種方式

  • 執行BGREWRITEAOF命令
  • 配置redis.conf中的auto-aof-rewrite-percentage選項

7.2.BGREWRITEAOF 方式

收到此命令redis將使用與快照類似的方式將內存中的數據 以命令的方式保存到臨時文件中,最後替換原來的文件。具體過程如下
  • redis調用fork ,現在有父子兩個進程子進程根據內存中的數據庫快照,往臨時文件中寫入重建數據庫狀態的命令
  • 父進程繼續處理client請求,除了把寫命令寫入到原來的aof文件中。同時把收到的寫命令緩存起來。這樣就能保證如果子進程重寫失敗的話並不會出問題。
  • 當子進程把快照內容寫入已命令方式寫到臨時文件中後,子進程發信號通知父進程。然後父進程把緩存的寫命令也寫入到臨時文件。
  • 現在父進程可以使用臨時文件替換老的aof文件,並重命名,後面收到的寫命令也開始往新的aof文件中追加。

注意 : 重寫aof文件的操作,並沒有讀取舊的aof文件,而是將整個內存中的數據庫內容用命令的方式重寫了一個新的aof文件,替換原有的文件這點和快照有點類似。(AOF重寫過程完成後會刪除舊的AOF文件,刪除一個體積達幾十GB大的舊的AOF文件可能會導致系統隨時掛起 )

7.3. 配置redis.conf中的auto-aof-rewrite-percentage選項

  • AOF重寫也可以使用auto-aof-rewrite-percentage 100
    和auto-aof-rewrite-min-size 64mb來自動執行BGREWRITEAOF.

說明: 如果設置auto-aof-rewrite-percentage值爲100和auto-aof-rewrite-min-size 64mb,並且啓用的AOF持久化時,那麼當AOF文件體積大於64M,並且AOF文件的體積比上一次重寫之後體積大了至少一倍(100%)時,會自動觸發,如果重寫過於頻繁,用戶可以考慮將auto-aof-rewrite-percentage設置爲更大

8.兩種持久化方案的總結

  • AOF持久化既可以將丟失的數據的時間降低到1秒(甚至不丟失任何數據),那麼我們還有什麼理由不是用AOF呢?

注意 :

這個問題實際上並沒有這麼簡單,因爲redis會不斷將執行的寫命令記錄到AOF文件中,所以隨着redis運行,AOF文件的體積會不斷增大,在極端情況下甚至會用完整個硬盤,還有redis重啓重新執行AOF文件記錄的所有寫命令的來還原數據集,AOF文件體積非常大,會導致redis執行恢復時間過長

兩種持久化方案既可以同時使用,又可以單獨使用,在某種情況下也可以都不使用,具體使用那種持久化方案取決於用戶的數據和應用決定

無論使用AOF還是快照機制持久化,將數據持久化到硬盤都是有必要的,除了持久化外,用戶還應該對持久化的文件進行備份(最好備份在多個不同地方)

9.基於SpringData設計redis通用緩存

1.導入jar包

<!--springData操作redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--aop-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.配置redis

spring:
  redis:
    host: 192.168.199.131   #ip地址
    port: 7000   #端口號
    database: 3   #操作的庫

3.使用redisTemplate操作

3.1.下表是具體的操作視圖接口類介紹:

Key類型操作
opsForValue Redis String/Value 操作
opsForList Redis List 操作
opsForSet Redis Set 操作
opsForZSet Redis Sort Set 操作
opsForHash Redis Hash 操作

4.配置切面類

@Configuration
@Aspect
public class RedisCache {

    @Resource
    private RedisTemplate redisTemplate;

    @Resource
    private StringRedisTemplate stringRedisTemplate;
}

5-1.創建通用緩存(string,string)

 /*
    * 添加緩存
    * */
@Around("execution(* com.baizhi.service.*.query*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{

    System.out.println("==環繞通知==");

    /*解決緩存亂碼*/
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    redisTemplate.setKeySerializer(stringRedisSerializer);
    redisTemplate.setHashKeySerializer(stringRedisSerializer);

    //獲取切面切的方法
    MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
    Method method = signature.getMethod();

    //判斷該方法上是否有添加緩存的註解
    boolean annotationPresent = method.isAnnotationPresent(AddCache.class);

    if(annotationPresent){

        StringBuilder sb = new StringBuilder();

        //設計一個key(類名+方法名+實參)   value(查詢的結果)

        //獲取類的全限定名
        String clazzName = proceedingJoinPoint.getTarget().getClass().getName();
        sb.append(clazzName);

        //獲取方法名
        String methodName = proceedingJoinPoint.getSignature().getName();
        sb.append(methodName);

        //返回方法的實參
        Object[] args = proceedingJoinPoint.getArgs();
        for (Object arg : args) {
            sb.append(arg);
        }

        //獲取key
        String key = sb.toString();
        //String 類型的操作
        ValueOperations valueOperations = redisTemplate.opsForValue();

        //判斷key是否存在
        Boolean aBoolean = redisTemplate.hasKey(key);

        Object result = null;
        //判斷緩存中對否存在
        if(aBoolean){
            //存在 查詢redis 返回結果
            result = valueOperations.get(key);
        }else{
            result = proceedingJoinPoint.proceed();
            //不存在  納入redis緩存
            valueOperations.set(key,result);
        }

        return result;
    }else{
        //沒有該註解直接放行
        Object proceed = proceedingJoinPoint.proceed();
        return proceed;
    }
}

5-2.清除緩存

/*
 * 清空緩存
* */
@After("execution(* com.baizhi.service.*.*(..)) && !execution(* com.baizhi.service.*.query*(..)) ")
public void after(JoinPoint joinPoint){

    System.out.println("==清空緩存==");
    //類的全限定名
    String className = joinPoint.getTarget().getClass().getName();

    //獲取所有的key
    Set<String> keys = stringRedisTemplate.keys("*");
    //遍歷所有的key
    for (String key : keys) {

        //判斷符合條件的key
        if(key.startsWith(className)){
            //清除
            stringRedisTemplate.delete(key);
        }
    }
}

5-3.自定義註解

package com.baizhi.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AddCache {

    //String value() default "";
    //String name();
}

6-1.創建通用緩存(string,hash)

/*
    * 添加緩存
    * */
@Around("execution(* com.baizhi.service.*.query*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{

    System.out.println("==環繞通知==");

    /*解決緩存亂碼*/
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    redisTemplate.setKeySerializer(stringRedisSerializer);
    redisTemplate.setHashKeySerializer(stringRedisSerializer);

    //獲取切面切的方法
    MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
    Method method = signature.getMethod();

    //判斷該方法上是否有添加緩存的註解
    boolean annotationPresent = method.isAnnotationPresent(AddCache.class);

    if(annotationPresent){

        //創建可變長字符串
        StringBuilder sb = new StringBuilder();

        //設計一個key(類名+方法名+實參)   value(查詢的結果)

        //獲取類的全限定名
        String clazzName = proceedingJoinPoint.getTarget().getClass().getName();

        //獲取方法名
        String methodName = proceedingJoinPoint.getSignature().getName();
        sb.append(methodName);

        //返回方法的實參
        Object[] args = proceedingJoinPoint.getArgs();
        for (Object arg : args) {
            sb.append(arg);
        }

        //獲取key
        String key = sb.toString();
        //hash 類型的操作
        HashOperations hashOperations = redisTemplate.opsForHash();
        //判斷key是否存在
        Boolean aBoolean = redisTemplate.hasKey(key);

        Object result = null;
        //判斷緩存中對否存在
        if(aBoolean){
            //存在 查詢redis 返回結果
            result = hashOperations.get(clazzName,key);
        }else{
            result = proceedingJoinPoint.proceed();
            //不存在  納入redis緩存  KEY  key value
            hashOperations.put(clazzName,key,result);
        }
        return result;
    }else{
        //沒有該註解直接放行
        Object proceed = proceedingJoinPoint.proceed();
        return proceed;
    }
}

6-2.清除緩存

/*
    * 清空緩存
    * */
@After("execution(* com.baizhi.service.*.*(..)) && !execution(* com.baizhi.service.*.query*(..)) ")
public void after(JoinPoint joinPoint){

    System.out.println("==清空緩存==");
    //類的全限定名
    String className = joinPoint.getTarget().getClass().getName();

    //獲取所有的key
    stringRedisTemplate.delete(className);
}

10.Java源註解

1、@Documented:

用於標記在生成javadoc時是否將註解包含進去,可以看到這個註解和@Override一樣,註解中空空如也,什麼東西都沒有。

2、@Target

用於定義註解可以在什麼地方使用,默認可以在任何地方使用,也可以指定使用的範圍,開發中將註解用。

TYPE : 類、接口或enum聲明

FIELD: 域(屬性)聲明

METHOD: 方法聲明

PARAMETER: 參數聲明

CONSTRUCTOR: 構造方法聲明

LOCAL_VARIABLE:局部變量聲明

ANNOTATION_TYPE:註釋類型聲明

PACKAGE: 包聲明

示例:

@Target({ElementType.PARAMETER, ElementType.METHOD}) //參數聲明、方法聲明

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface Servicelock {

 String description()  default "";

}

3、@Inherited

允許子類繼承父類中的註解,可以通過反射獲取到父類的註解

4、@Constraint

用於校驗屬性值是否合法

5、@Retention

註解的聲明週期,用於定義註解的存活階段,可以存活在源碼級別、編譯級別(字節碼級別)、運行時級別。

SOURCE:源碼級別,註解只存在源碼中,一般用於和編譯器交互,用於檢測代碼。如@Override, @SuppressWarings。

CLASS:字節碼級別,註解存在於源碼和字節碼文件中,主要用於編譯時生成額外的文件,如XML,Java文件等,但運行時無法獲得。 如mybatis生成實體和映射文件,這個級別需要添加JVM加載時候的代理(javaagent),使用代理來動態修改字節碼文件。

RUNTIME:運行時級別,註解存在於源碼、字節碼、java虛擬機中,主要用於運行時,可以使用反射獲取相關的信息。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章