緩存解決方案SpringDataRedis

學習目標

  • 掌握SpringDataRedis 的常用操作
  • 能夠理解並說出什麼是緩存穿透、緩存擊穿、緩存雪崩,以及對應的解決方案
  • 使用緩存預熱的方式實現商品分類導航緩存
  • 使用緩存預熱的方式實現廣告輪播圖緩存
  • 使用緩存預熱的方式實現商品價格緩存

1.SpringDataRedis

1.1 SpringDataRedis簡介

SpringDataRedis 屬於Spring Data 家族一員,用於對redis的操作進行封裝的框架 ,Spring Data : Spring 的一個子項目.Spring 官方提供一套數據層綜合解決方案,用 於簡化數據庫訪問,支持NoSQL和關係數據庫存儲。包括Spring Data JPA 、Spring Data Redis 、SpringDataSolr 、SpringDataElasticsearch 、Spring DataMongodb 等 框架。

1.2 SpringDataRedis入門

1.2.1 準備工作

(1)創建SpringDataRedisDemo工程,在pom.xml中配置相關依賴

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.qingcheng.springdataredis</groupId>
    <artifactId>SpringDataRedisDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.0.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.0.5.RELEASE</version>
        </dependency>
    </dependencies>

</project>

(2)在src/main/resources下創建properties文件redis-config.properties

redis.host=127.0.0.1
redis.port=6379
redis.pass=
redis.database=0
redis.maxIdle=300
redis.maxWait=3000

maxIdle :最大空閒數

maxWaitMillis: 連接時的最大等待毫秒數

(3)在src/main/resources下創建applicationContext-redis.xml

<?xml version="1.0" encoding="UTF-8"?> 
<beans xmlns="http://www.springframework.org/schema/beans"      
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
   
   
   <context:property-placeholder location="classpath:redis-config.properties" />
   
   <!-- redis 相關配置 --> 
   <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">  
     <property name="maxIdle" value="${redis.maxIdle}" />   
     <property name="maxWaitMillis" value="${redis.maxWait}" />  
   </bean>  
  
   <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
       p:host-name="${redis.host}" p:port="${redis.port}" p:password="${redis.pass}" p:pool-config-ref="poolConfig"/>  
   
   <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">  
    	<property name="connectionFactory" ref="jedisConnectionFactory" />
   </bean>


</beans>  

1.2.2 值類型操作

package com.qingcheng.springdataredis.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestValue {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 存值 / 修改(當key相同,value不同,把原來覆蓋掉)
     */
    @Test
    public void setValue(){
        redisTemplate.boundValueOps("name").set("qingcheng");
    }

    /**
     * 取值
     */
    @Test
    public void getValue(){
        String str = (String) redisTemplate.boundValueOps("name").get();
        System.out.println(str);
    }

    /**
     * 刪除
     */
    @Test
    public void deleteValue(){
        Boolean name = redisTemplate.delete("name");
        
    }
}

1.2.3 Set類型操作

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.Set;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestSet {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void set(){
        redisTemplate.boundSetOps("names").add("曹操");
        redisTemplate.boundSetOps("names").add("劉備");
        redisTemplate.boundSetOps("names").add("孫權");
    }
    @Test
    public void get(){
        //獲取所有
        Set names = redisTemplate.boundSetOps("names").members();
        System.out.println(names);
    }

    @Test
    public void deleteValue(){
        Long remove = redisTemplate.boundSetOps("names").remove("曹操");
        System.out.println(remove);
    }

    @Test
    public void deleteAll(){
        Boolean isDelete = redisTemplate.delete("names");
        System.out.println(isDelete);
    }
}

1.2.3 List類型操作

package com.qingcheng.springdataredis.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;
import java.util.Set;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestList {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 右壓棧 (後添加的排在後面)
     *
     */
    @Test
    public void setRightValue(){
        redisTemplate.boundListOps("names").rightPush("劉備");
        redisTemplate.boundListOps("names").rightPush("關羽");
        redisTemplate.boundListOps("names").rightPush("張飛");
    }


    /**
     * 左壓棧(後添加的在前面)
     */
    @Test
    public void setLeftValue(){
        redisTemplate.boundListOps("nams_").leftPush("劉備");
        redisTemplate.boundListOps("nams_").leftPush("關羽");
        redisTemplate.boundListOps("nams_").leftPush("張飛");
    }

    /**
     * range(start,end)
     * start:開始位置
     * end 查詢多少個 ,如果是"-1" 表示查詢所有
     */
    @Test
    public void seachAll(){
        List names = redisTemplate.boundListOps("nams_").range(0, -1);
        System.out.println(names);
    }

    @Test
    public void searchByIndex(){
        Object name = redisTemplate.boundListOps("nams_").index(2);
        System.out.println(name);
    }

    /**
     * 移除集合中某個元素
     * List集合可以重複
     * remove(count,Object) 第一個參數表示移除個數 第二個參數表示移除那個元素
     * 總之就是移除相同元素的個數
     */
    @Test
    public void remove(){
        redisTemplate.boundListOps("nams_").remove(2,"關羽");
    }
    @Test
    public void deleteAll(){
        redisTemplate.delete("nams_");
    }
}

1.2.4 Hash類型操作

類似於Java中的Map

package com.qingcheng.springdataredis.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;
import java.util.Set;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class TestHash {
    @Autowired
    private RedisTemplate redisTemplate;

     @Test
    public void setHashValue(){
         redisTemplate.boundHashOps("hashName").put("a","唐僧");
         redisTemplate.boundHashOps("hashName").put("s","孫悟空");
         redisTemplate.boundHashOps("hashName").put("z","豬八戒");
     }

     @Test
    public void getHashKeys(){
         Set keys = redisTemplate.boundHashOps("hashName").keys();
         System.out.println(keys);
     }

     @Test
    public void getHashValues(){
         List values = redisTemplate.boundHashOps("hashName").values();
         System.out.println(values);
     }

    /**
     * 根據key獲取value
     */
    @Test
    public void getValueByKey(){
        Object o = redisTemplate.boundHashOps("hashName").get("a");
        System.out.println(o);
    }

    @Test
    public void deleteByKey(){
        redisTemplate.boundHashOps("hashName").delete("a");
    }

    @Test
    public void delete(){
        redisTemplate.delete("hashName");
    }
}

1.2.5 zset類型操作

zset是set的升級版本,它在set的基礎上增加了格順序屬性,這屬性在添加元素

的同時可以指定,每次指定後,zset會自動重新按照新的值調整順序。可以理解爲有兩列 的mysql表,列存儲value,列存儲分值。

比如主播的人氣榜、富豪榜

/*
package com.qingcheng.springdataredis.test;*/
package com.qingcheng.springdataredis.test;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;
import java.util.Set;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext-redis.xml")
public class Testzset {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testSetValue(){
        redisTemplate.boundZSetOps("nameszset").add("曹操",1000);
        redisTemplate.boundZSetOps("nameszset").add("劉備",100);
        redisTemplate.boundZSetOps("nameszset").add("孫權",10);
    }

    /**
     * 默認是由低到高
     */
    @Test
    public void testGetValue(){
        Set nameszset = redisTemplate.boundZSetOps("nameszset").range(0, -1);
        System.out.println(nameszset);
    }

    /**
     * 前兩位富豪
     */

    @Test
    public void testTuHaoBang(){
        Set nameszset = redisTemplate.boundZSetOps("nameszset").reverseRange(0, 1);
        System.out.println(nameszset);
    }

    @Test
    public void addScort(){
        redisTemplate.boundZSetOps("nameszset").incrementScore("孫權",200);
    }
    //查詢分數和值
    //TypeTuple類型的對象
    @Test
    public void getValueAndScore(){
        Set<ZSetOperations.TypedTuple> nameszset = redisTemplate.boundZSetOps("nameszset").reverseRangeWithScores(0, 9);
        for (ZSetOperations.TypedTuple typedTuple : nameszset) {
            System.out.println(typedTuple.getValue()+"----"+typedTuple.getScore());
        }

    }
}

1.2.6 設置過期時間

    @Test
    public void setTimeOut(){
        //第一個參數表示設置過期的數字
        //第二個參數表示數字的單位, seconds 表示秒
        redisTemplate.boundValueOps("name").expire(10, TimeUnit.SECONDS);
    }

2.緩存穿透、擊穿、雪崩

2.1 緩存穿透

緩存穿透是指緩存和數據庫中都沒有的數據,而用戶不斷髮起請求,如發起爲id

爲“1”的數據或id爲特別大不存在的數據。這時的用戶很可能是攻擊者,攻擊會導致數據 庫壓力過大。如下面這段代碼就存在緩存穿透的問題

public Integer findPrice(Long id) { 
    //從緩存中查詢 
    Integer sku_price = (Integer)redisTemplate.boundHashOps("sku_price").get(id); 	     
    if(sku_price==null){ 
        //緩存中沒有,從數據庫查詢 
        Sku sku = skuMapper.selectByPrimaryKey(id); 
       
        if(sku!=null){ 
            //如果數據庫有此對象 
            sku_price = sku.getPrice();                       		      
            redisTemplate.boundHashOps("sku_price").put(id,sku_price); 
        } 
    }
    return sku_price; 
}

解決方案:

1.接口層增加校驗,如用戶鑑權校驗,id做基礎校驗,id<=0的直接攔截;

2.從緩存取不到的數據,在數據庫中也沒有取到,這時也可以將key-value對寫爲 key-0。這樣可以防止攻擊用戶反覆用同一個id暴力攻擊。

代碼舉例:

public Integer findPrice(Long id) { 
    //從緩存中查詢 
    Integer sku_price = (Integer)redisTemplate.boundHashOps("sku_price").get(id); 	     
    if(sku_price==null){ 
        //緩存中沒有,從數據庫查詢 
        Sku sku = skuMapper.selectByPrimaryKey(id); 
        if(sku!=null){ 
            //如果數據庫有此對象 
            sku_price = sku.getPrice();                       		      
            redisTemplate.boundHashOps("sku_price").put(id,sku_price); 
        }else{
           redisTemplate.boundHashOps("sku_price").put(id,0);  
        } 
    }
    return sku_price; 
}

  1. 使用緩存預熱 ,緩存預熱就是將數據提前加入到緩存中,當數據發生變更,再將最新的數據更新到緩

存。後邊我們就用緩存預熱的方式實現對分類導航、廣告輪播圖等數據的緩存。

2.2 緩存擊穿

緩存擊穿是指緩存中沒有但數據庫中有的數據。這時由於併發用戶特別多,同時讀

緩存沒讀到數據,又同時去數據庫去取數據,引起數據庫壓力瞬間增大,造成過大壓 力。

以下代碼可能會產生緩存擊穿:

@Autowired 
private RedisTemplate redisTemplate;
public List<Map> findCategoryTree() {
    //從緩存中查詢 
    List<Map> categoryTree= (List<Map>)redisTemplate.boundValueOps("categoryTree").get(); 
    if(categoryTree==null){ 
        Example example=new Example(Category.class); 
        Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("isShow","1");
        //顯示 
        List<Category> categories = categoryMapper.selectByExample(example); 
        categoryTree=findByParentId(categories,0); 
        redisTemplate.boundValueOps("categoryTree").set(categoryTree); 
        //過期時間設置 ...... 
    }
    return categoryTree; 
}

主要是緩存過期時間造成的。

解決方案:

1.設置熱點數據永遠不過期。

2.緩存預熱

2.3 緩存雪崩

緩存雪崩是指緩存數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過

大甚至down機。和緩存擊穿不同的是,緩存擊穿指併發查同一條數據緩存雪崩是不同

數據都過期了,很多數據都查不到從而查數據庫

解決方案:

1.緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。

2.設置熱點數據永遠不過期。

3.使用緩存預熱

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