文章目錄
學習目標
- 掌握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;
}
- 使用緩存預熱 ,緩存預熱就是將數據提前加入到緩存中,當數據發生變更,再將最新的數據更新到緩
存。後邊我們就用緩存預熱的方式實現對分類導航、廣告輪播圖等數據的緩存。
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.使用緩存預熱