緩存預(yù)熱
緩存預(yù)熱就是系統(tǒng)啟動(dòng)前,提前將相關(guān)的緩存數(shù)據(jù)直接加載到緩存系統(tǒng)。避免在用戶請(qǐng)求的時(shí)候,先查詢數(shù)據(jù)庫(kù),然后再將數(shù)據(jù)緩存的問(wèn)題!用戶直接查詢事先被預(yù)熱的緩存數(shù)據(jù)。
可以通過(guò)@PostConstruct初始化白名單數(shù)據(jù)
緩存雪崩
發(fā)生?
- redis主機(jī)掛了,Redis?全盤崩潰,偏硬件運(yùn)維
- redis中有大量key同時(shí)過(guò)期大面積失效,偏軟件開發(fā)
預(yù)防+解決
- redis中key設(shè)置為永不過(guò)期?or?過(guò)期時(shí)間錯(cuò)開
- redis緩存集群實(shí)現(xiàn)高可用(主從+哨兵、Redis?Cluster、開啟Redis持久化機(jī)制aof/rdb,盡快恢復(fù)緩存集群)
- 多緩存結(jié)合預(yù)防雪崩(ehcache本地緩存?+?redis緩存)
- 服務(wù)降級(jí)(Hystrix或者阿里sentinel限流&降級(jí))
? - “人民幣玩家” :阿里云-云數(shù)據(jù)庫(kù)Redis版
緩存穿透
是什么
請(qǐng)求去查詢一條記錄,先查redis無(wú),后查mysql無(wú),都查詢不到該條記錄,但是請(qǐng)求每次都會(huì)打到數(shù)據(jù)庫(kù)上面去,導(dǎo)致后臺(tái)數(shù)據(jù)庫(kù)壓力暴增,這種現(xiàn)象稱為緩存穿透,這個(gè)redis變成了一個(gè)擺設(shè)。。。。。。
簡(jiǎn)單說(shuō)就是本來(lái)無(wú)一物,兩庫(kù)都沒有。既不在Redis緩存庫(kù),也不在mysql,數(shù)據(jù)庫(kù)存在被多次暴擊風(fēng)險(xiǎn)。
解決
一圖
方案1:空對(duì)象緩存或者缺省值?
一般OK
第一種解決方案,回寫增強(qiáng)
如果發(fā)生了緩存穿透,我們可以針對(duì)要查詢的數(shù)據(jù),在Redis里存一個(gè)和業(yè)務(wù)部門商量后確定的缺省值(比如,零、負(fù)數(shù)、defaultNull等)。
比如,鍵uid:abcdxxx,值defaultNull作為案例的key和value
先去redis查鍵uid:abcdxxx沒有,再去mysql查沒有獲得 ,這就發(fā)生了一次穿透現(xiàn)象。
but,可以增強(qiáng)回寫機(jī)制
mysql也查不到的話也讓redis存入剛剛查不到的key并保護(hù)mysql。
第一次來(lái)查詢uid:abcdxxx,redis和mysql都沒有,返回null給調(diào)用者,但是增強(qiáng)回寫后第二次來(lái)查uid:abcdxxx,此時(shí)redis就有值了。
可以直接從Redis中讀取default缺省值返回給業(yè)務(wù)應(yīng)用程序,避免了把大量請(qǐng)求發(fā)送給mysql處理,打爆mysql。
但是,此方法架不住黑客的惡意攻擊,有缺陷......,只能解決key相同的情況
But
黑客或者惡意攻擊
黑客會(huì)對(duì)你的系統(tǒng)進(jìn)行攻擊,拿一個(gè)不存在的id去查詢數(shù)據(jù),會(huì)產(chǎn)生大量的請(qǐng)求到數(shù)據(jù)庫(kù)去查詢??赡軙?huì)導(dǎo)致你的數(shù)據(jù)庫(kù)由于壓力過(guò)大而宕掉。
key相同打你系統(tǒng)
第一次打到mysql,空對(duì)象緩存后第二次就返回defaultNull缺省值,避免mysql被攻擊,不用再到數(shù)據(jù)庫(kù)中去走一圈了。
key不同打你系統(tǒng)
由于存在空對(duì)象緩存和緩存回寫(看自己業(yè)務(wù)不限死),redis中的無(wú)關(guān)緊要的key也會(huì)越寫越多(記得設(shè)置redis過(guò)期時(shí)間)
方案2:Google布隆過(guò)濾器Guava解決緩存穿透
Guava?中布隆過(guò)濾器的實(shí)現(xiàn)算是比較權(quán)威的,所以實(shí)際項(xiàng)目中我們可以直接使用Guava布隆過(guò)濾器。
Guava’s BloomFilter源碼出處
案例:白名單過(guò)濾器
白名單架構(gòu)說(shuō)明
誤判問(wèn)題,但是概率小可以接受,不能從布隆過(guò)濾器刪除
全部合法的key都需要放入Guava版布隆過(guò)濾器+redis里面,不然數(shù)據(jù)就是返回null
Coding實(shí)戰(zhàn)?
建Module
redis7_study
改POM
<?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.test.redis7</groupId>
<artifactId>redis7_study</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
</properties>
<dependencies>
<!--guava Google 開源的 Guava 中自帶的布隆過(guò)濾器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<!--SpringBoot通用依賴模塊-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<!--lettuce-->
<!--<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.1.RELEASE</version>
</dependency>-->
<!--SpringBoot與Redis整合依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--Mysql數(shù)據(jù)庫(kù)驅(qū)動(dòng)-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--SpringBoot集成druid連接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.2.3</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>1.0.2</version>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!--通用基礎(chǔ)配置junit/devtools/test/log4j/lombok/-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
寫YML
server.port=7777
spring.application.name=redis7_study
# ========================logging=====================
logging.level.root=info
logging.level.com.test.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X結(jié)合swagger2.9.X會(huì)提示documentationPluginsBootstrapper空指針異常,
#原因是在springboot2.6.X中將SpringMVC默認(rèn)路徑匹配策略從AntPathMatcher更改為PathPatternParser,
# 導(dǎo)致出錯(cuò),解決辦法是matching-strategy切換回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
# ========================redis單機(jī)=====================
spring.redis.database=0
# 修改為自己真實(shí)IP
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=111111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
# ========================alibaba.druid=====================
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/bigdata?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.druid.test-while-idle=false
# ========================mybatis===================
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.test.redis7.entities
# ========================redis集群=====================
#spring.redis.password=111111
## 獲取失敗 最大重定向次數(shù)
#spring.redis.cluster.max-redirects=3
#spring.redis.lettuce.pool.max-active=8
#spring.redis.lettuce.pool.max-wait=-1ms
#spring.redis.lettuce.pool.max-idle=8
#spring.redis.lettuce.pool.min-idle=0
##支持集群拓?fù)鋭?dòng)態(tài)感應(yīng)刷新,自適應(yīng)拓?fù)渌⑿率欠袷褂盟锌捎玫母拢J(rèn)false關(guān)閉
#spring.redis.lettuce.cluster.refresh.adaptive=true
##定時(shí)刷新
#spring.redis.lettuce.cluster.refresh.period=2000
#spring.redis.cluster.nodes=192.168.111.185:6381,192.168.111.185:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.184:6385,192.168.111.184:6386
主啟動(dòng)
package com.test.redis7;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;
/**
* @auther admin
*/
@SpringBootApplication
@MapperScan("com.test.redis7.mapper")
//import tk.mybatis.spring.annotation.MapperScan;
public class Redis7Study7777 {
public static void main(String[] args) {
SpringApplication.run(Redis7Study7777.class, args);
}
}
業(yè)務(wù)類(取樣本100W數(shù)據(jù),查查不在100W范圍內(nèi),其它10W數(shù)據(jù)是否存在)
Case01
新建測(cè)試案例,hello入門
@Test
public void testGuavaWithBloomFilter() {
// 創(chuàng)建布隆過(guò)濾器對(duì)象
BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), 100);
// 判斷指定元素是否存在
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
// 將元素添加進(jìn)布隆過(guò)濾器
filter.put(1);
filter.put(2);
System.out.println(filter.mightContain(1));
System.out.println(filter.mightContain(2));
}
Case02
GuavaBloomFilterController
package com.test.redis7.controller;
import com.test.redis7.service.GuavaBloomFilterService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @auther admin
*/
@Slf4j
@RestController
@Api(tags = "google工具Guava處理布隆過(guò)濾器")
public class GuavaBloomFilterController {
@Resource
private GuavaBloomFilterService guavaBloomFilterService;
@ApiOperation("guava布隆過(guò)濾器插入100萬(wàn)樣本數(shù)據(jù)并額外10W測(cè)試是否存在")
@RequestMapping(value = "/guavafilter", method = RequestMethod.GET)
public void guavaBloomFilter() {
guavaBloomFilterService.guavaBloomFilter();
}
}
GuavaBloomFilterService
package com.test.redis7.service;
/**
* @auther admin
*/
public interface GuavaBloomFilterService{
void guavaBloomFilter();
}
GuavaBloomFilterServiceImpl
package com.test.redis7.service.impl;
import com.test.redis7.service.GuavaBloomFilterService;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @auther admin
*/
@Slf4j
@Service
public class GuavaBloomFilterServiceImpl implements GuavaBloomFilterService {
public static final int _1W = 10000;
/**
* 布隆過(guò)濾器里預(yù)計(jì)要插入多少數(shù)據(jù)
*/
public static int size = 100 * _1W;
/**
* 誤判率,它越小誤判的個(gè)數(shù)也就越少(思考,是不是可以設(shè)置的無(wú)限小,沒有誤判豈不更好)
* 當(dāng)不設(shè)置誤判率時(shí),默認(rèn)的誤判率為0.03,當(dāng)誤判率設(shè)置的越小,分配的bit數(shù)組與使用的hash
* 函數(shù)個(gè)數(shù)將會(huì)越多,耗費(fèi)的資源也就越多
* fpp the desired false positive probability
*/
public static double fpp = 0.03;
// 構(gòu)建布隆過(guò)濾器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size, fpp);
public void guavaBloomFilter(){
//1 先往布隆過(guò)濾器里面插入100萬(wàn)的樣本數(shù)據(jù)
for (int i = 1; i <=size; i++) {
bloomFilter.put(i);
}
//故意取10萬(wàn)個(gè)不在過(guò)濾器里的值,看看有多少個(gè)會(huì)被認(rèn)為在過(guò)濾器里
List<Integer> list = new ArrayList<>(10 * _1W);
for (int i = size + 1; i <= size + (10 *_1W); i++) {
if (bloomFilter.mightContain(i)) {
log.info("被誤判了:{}",i);
list.add(i);
}
}
log.info("誤判的總數(shù)量::{}",list.size());
}
}
上一步結(jié)論
現(xiàn)在總共有10萬(wàn)數(shù)據(jù)是不存在的,誤判了3033次,
原始樣本:100W
不存在數(shù)據(jù):1000001W---1100000W
?可以debug源碼分析下,看看hash函數(shù)
布隆過(guò)濾器說(shuō)明
黑名單使用
緩存擊穿?
是什么?
大量的請(qǐng)求同時(shí)查詢一個(gè)?key?時(shí),此時(shí)這個(gè)key正好失效了,就會(huì)導(dǎo)致大量的請(qǐng)求都打到數(shù)據(jù)庫(kù)上面去。(備注:穿透和擊穿,截然不同)
簡(jiǎn)單說(shuō)就是熱點(diǎn)key突然失效了,暴打mysql。
危害
- 會(huì)造成某一時(shí)刻數(shù)據(jù)庫(kù)請(qǐng)求量過(guò)大,壓力劇增。
- 一般技術(shù)部門需要知道熱點(diǎn)key是那些個(gè)?做到心里有數(shù)防止擊穿。
解決
熱點(diǎn)key失效
- 時(shí)間到了自然清除但還被訪問(wèn)到
- delete掉的key,剛巧又被訪問(wèn)?
方案1:差異失效時(shí)間,對(duì)于訪問(wèn)頻繁的熱點(diǎn)key,干脆就不設(shè)置過(guò)期時(shí)間
方案2:互斥跟新,采用雙檢加鎖策略
多個(gè)線程同時(shí)去查詢數(shù)據(jù)庫(kù)的這條數(shù)據(jù),那么我們可以在第一個(gè)查詢數(shù)據(jù)的請(qǐng)求上使用一個(gè) 互斥鎖來(lái)鎖住它。
其他的線程走到這一步拿不到鎖就等著,等第一個(gè)線程查詢到了數(shù)據(jù),然后做緩存。后面的線程進(jìn)來(lái)發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存。
案例
天貓聚劃算功能實(shí)現(xiàn)+防止緩存擊穿
模擬高并發(fā)的天貓聚劃算案例code
是什么
生產(chǎn)案例網(wǎng)址
問(wèn)題,熱點(diǎn)key突然失效導(dǎo)致了緩存擊穿。
技術(shù)方案實(shí)現(xiàn)
分析過(guò)程
步驟
|
說(shuō)明
|
1
|
?100%高并發(fā),絕對(duì)不可以用mysql實(shí)現(xiàn)
|
2?
|
先把mysql里面參加活動(dòng)的數(shù)據(jù)抽取進(jìn)redis,一般采用定時(shí)器掃描來(lái)決定上線活動(dòng)還是下線取消。
|
3
|
?支持分頁(yè)功能,一頁(yè)20條記錄
|
請(qǐng)大家思考,redis里面什么樣子的數(shù)據(jù)類型支持上述功能?
|
高并發(fā)+定時(shí)任務(wù)+分頁(yè)顯示。。。。
redis數(shù)據(jù)類型選型
springboot+redis實(shí)現(xiàn)高并發(fā)的聚劃算業(yè)務(wù)V2?
建Module
修改上面redis7_study工程
業(yè)務(wù)類
entity
package com.test.redis7.entities;
import io.swagger.annotations.ApiModel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @auther admin
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(value = "聚劃算活動(dòng)producet信息")
public class Product {
/**
* 產(chǎn)品ID
*/
private Long id;
/**
* 產(chǎn)品名稱
*/
private String name;
/**
* 產(chǎn)品價(jià)格
*/
private Integer price;
/**
* 產(chǎn)品詳情
*/
private String detail;
}
JHSProductController
package com.test.redis7.controller;
import com.test.redis7.entities.Product;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @auther admin
*/
@Slf4j
@RestController
@Api(tags = "聚劃算商品列表接口")
public class JHSProductController {
public static final String JHS_KEY = "jhs";
@Autowired
private RedisTemplate redisTemplate;
/**
* 分頁(yè)查詢:在高并發(fā)的情況下,只能走redis查詢,走db的話必定會(huì)把db打垮
* @param page 當(dāng)前頁(yè)
* @param size 頁(yè)面容量
* @return 商品列表
*/
@ApiOperation("按照分頁(yè)和每頁(yè)顯示容量,點(diǎn)擊查看")
@RequestMapping(value = "/pruduct/find", method = RequestMethod.GET)
public List<Product> find(int page, int size) {
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
//采用redis list數(shù)據(jù)結(jié)構(gòu)的lrange命令實(shí)現(xiàn)分頁(yè)查詢
list = this.redisTemplate.opsForList().range(JHS_KEY, start, end);
if (CollectionUtils.isEmpty(list)) {
//TODO 走DB查詢
}
log.info("查詢結(jié)果:{}", list);
} catch (Exception ex) {
//這里的異常,一般是redis癱瘓 ,或 redis網(wǎng)絡(luò)timeout
log.error("exception:", ex);
//TODO 走DB查詢
}
return list;
}
}
JHSTaskService
package com.test.redis7.service;
import cn.hutool.core.date.DateUtil;
import com.test.redis7.entities.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @auther admin
*/
@Slf4j
@Service
public class JHSTaskService {
public static final String JHS_KEY = "jhs";
@Autowired
private RedisTemplate redisTemplate;
/**
* 偷個(gè)懶不加mybatis了,模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中
* @return 商品列表
*/
private List<Product> getProductsFromMysql() {
List<Product> list = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
Random rand = new Random();
int id = rand.nextInt(10000);
Product obj = new Product((long) id, "product" + i, i, "detail");
list.add(obj);
}
return list;
}
@PostConstruct
public void initJHS(){
log.info("啟動(dòng)定時(shí)器淘寶聚劃算功能模擬.........." + DateUtil.now());
new Thread(() -> {
//模擬定時(shí)器一個(gè)后臺(tái)任務(wù),定時(shí)把數(shù)據(jù)庫(kù)的特價(jià)商品,刷新到redis中
while (true){
//模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中
List<Product> list = this.getProductsFromMysql();
//采用redis list數(shù)據(jù)結(jié)構(gòu)的lpush來(lái)實(shí)現(xiàn)存儲(chǔ)
this.redisTemplate.delete(JHS_KEY);
//lpush命令
this.redisTemplate.opsForList().leftPushAll(JHS_KEY, list);
//間隔一分鐘 執(zhí)行一遍,模擬聚劃算每3天刷新一批次參加活動(dòng)
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("runJhs定時(shí)刷新..............");
}
}, "t1").start();
}
}
備注
至此步驟,上述聚劃算的功能算是完成,請(qǐng)思考在高并發(fā)下有什么經(jīng)典生產(chǎn)問(wèn)題?
Bug和隱患說(shuō)明
熱點(diǎn)key突然失效導(dǎo)致可怕的緩存擊穿
?delete命令執(zhí)行的一瞬間有空隙,其它請(qǐng)求線程繼續(xù)找Redis為null
打到了mysql,暴擊......
復(fù)習(xí)again
最終目的
2條命令原子性還是其次,主要是防止熱key突然失效暴擊mysql打爆系統(tǒng),^_^。
進(jìn)一步升級(jí)加固案例
復(fù)習(xí),互斥跟新,采用雙檢加鎖策略
多個(gè)線程同時(shí)去查詢數(shù)據(jù)庫(kù)的這條數(shù)據(jù),那么我們可以在第一個(gè)查詢數(shù)據(jù)的請(qǐng)求上使用一個(gè) 互斥鎖來(lái)鎖住它。
其他的線程走到這一步拿不到鎖就等著,等第一個(gè)線程查詢到了數(shù)據(jù),然后做緩存。后面的線程進(jìn)來(lái)發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存。
?差異失效時(shí)間
JHSProductController
package com.test.redis7.controller;
import com.test.redis7.entities.Product;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @auther admin
*/
@Slf4j
@RestController
@Api(tags = "聚劃算商品列表接口")
public class JHSProductController {
public static final String JHS_KEY = "jhs";
public static final String JHS_KEY_A = "jhs:a";
public static final String JHS_KEY_B = "jhs:b";
@Autowired
private RedisTemplate redisTemplate;
/**
* 分頁(yè)查詢:在高并發(fā)的情況下,只能走redis查詢,走db的話必定會(huì)把db打垮
* @param page 當(dāng)前頁(yè)
* @param size 頁(yè)面容量
* @return 商品列表
*/
@ApiOperation("按照分頁(yè)和每頁(yè)顯示容量,點(diǎn)擊查看")
@RequestMapping(value = "/pruduct/find", method = RequestMethod.GET)
public List<Product> find(int page, int size) {
List<Product> list=null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
//采用redis list數(shù)據(jù)結(jié)構(gòu)的lrange命令實(shí)現(xiàn)分頁(yè)查詢
list = this.redisTemplate.opsForList().range(JHS_KEY, start, end);
if (CollectionUtils.isEmpty(list)) {
//TODO 走DB查詢
}
log.info("查詢結(jié)果:{}", list);
} catch (Exception ex) {
//這里的異常,一般是redis癱瘓 ,或 redis網(wǎng)絡(luò)timeout
log.error("exception:", ex);
//TODO 走DB查詢
}
return list;
}
/**
* 防止熱點(diǎn)key突然失效,AB雙緩存架構(gòu)
* @param page 當(dāng)前頁(yè)
* @param size 頁(yè)面容量
* @return 商品列表
*/
@ApiOperation("防止熱點(diǎn)key突然失效,AB雙緩存架構(gòu)")
@RequestMapping(value = "/pruduct/findab", method = RequestMethod.GET)
public List<Product> findAB(int page, int size) {
List<Product> list = null;
long start = (page - 1) * size;
long end = start + size - 1;
try {
//采用redis list數(shù)據(jù)結(jié)構(gòu)的lrange命令實(shí)現(xiàn)分頁(yè)查詢
list = this.redisTemplate.opsForList().range(JHS_KEY_A, start, end);
if (CollectionUtils.isEmpty(list)) {
log.info("=========A緩存已經(jīng)失效了,記得人工修補(bǔ),B緩存自動(dòng)延續(xù)5天");
//用戶先查詢緩存A(上面的代碼),如果緩存A查詢不到(例如,更新緩存的時(shí)候刪除了),再查詢緩存B
list = this.redisTemplate.opsForList().range(JHS_KEY_B, start, end);
if (CollectionUtils.isEmpty(list)) {
//TODO 走DB查詢
}
}
log.info("查詢結(jié)果:{}", list);
} catch (Exception ex) {
//這里的異常,一般是redis癱瘓 ,或 redis網(wǎng)絡(luò)timeout
log.error("exception:", ex);
//TODO 走DB查詢
}
return list;
}
}
JHSTaskService?
package com.test.redis7.service;
import cn.hutool.core.date.DateUtil;
import com.test.redis7.entities.Product;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @auther admin
*/
@Slf4j
@Service
public class JHSTaskService {
public static final String JHS_KEY = "jhs";
public static final String JHS_KEY_A = "jhs:a";
public static final String JHS_KEY_B = "jhs:b";
@Autowired
private RedisTemplate redisTemplate;
/**
* 偷個(gè)懶不加mybatis了,模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中
* @return 商品列表
*/
private List<Product> getProductsFromMysql() {
List<Product> list = new ArrayList<>();
for (int i = 1; i <= 20; i++) {
Random rand = new Random();
int id= rand.nextInt(10000);
Product obj=new Product((long) id, "product" + i, i, "detail");
list.add(obj);
}
return list;
}
//@PostConstruct
public void initJHS(){
log.info("啟動(dòng)定時(shí)器淘寶聚劃算功能模擬.........." + DateUtil.now());
new Thread(() -> {
//模擬定時(shí)器,定時(shí)把數(shù)據(jù)庫(kù)的特價(jià)商品,刷新到redis中
while (true){
//模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中
List<Product> list = this.getProductsFromMysql();
//采用redis list數(shù)據(jù)結(jié)構(gòu)的lpush來(lái)實(shí)現(xiàn)存儲(chǔ)
this.redisTemplate.delete(JHS_KEY);
//lpush命令
this.redisTemplate.opsForList().leftPushAll(JHS_KEY, list);
//間隔一分鐘 執(zhí)行一遍
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("runJhs定時(shí)刷新..............");
}
}, "t1").start();
}
@PostConstruct
public void initJHSAB(){
log.info("啟動(dòng)AB定時(shí)器計(jì)劃任務(wù)淘寶聚劃算功能模擬.........." + DateUtil.now());
new Thread(() -> {
//模擬定時(shí)器,定時(shí)把數(shù)據(jù)庫(kù)的特價(jià)商品,刷新到redis中
while (true){
//模擬從數(shù)據(jù)庫(kù)讀取100件特價(jià)商品,用于加載到聚劃算的頁(yè)面中
List<Product> list = this.getProductsFromMysql();
//先更新B緩存
this.redisTemplate.delete(JHS_KEY_B);
this.redisTemplate.opsForList().leftPushAll(JHS_KEY_B, list);
this.redisTemplate.expire(JHS_KEY_B, 20L, TimeUnit.DAYS);
//再更新A緩存
this.redisTemplate.delete(JHS_KEY_A);
this.redisTemplate.opsForList().leftPushAll(JHS_KEY_A, list);
this.redisTemplate.expire(JHS_KEY_A, 15L, TimeUnit.DAYS);
//間隔一分鐘 執(zhí)行一遍
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("runJhs定時(shí)刷新雙緩存AB兩層..............");
}
}, "t1").start();
}
}
總結(jié)
緩存雪崩
解決方案:
- ??? Redis緩存集群實(shí)現(xiàn)高可用(合理設(shè)置過(guò)期時(shí)間或者延時(shí),采用哨兵集群等)
- ??? 采用多級(jí)緩存策略,例如 Nginx緩存+Redis緩存+ehcache緩存
- ??? 采用Hystrix或者阿里sentinel進(jìn)行服務(wù)限流限流或者降級(jí)
- ??? 監(jiān)控Redis各項(xiàng)指標(biāo),即使災(zāi)難預(yù)警
- ??? 采用AOF/ RDB持久化,盡快恢復(fù)Redis集群
緩存擊穿
解決方案:
- 對(duì)于熱點(diǎn)key加長(zhǎng)過(guò)期時(shí)間,或者干脆不設(shè)置過(guò)期時(shí)間
- 二級(jí)緩存設(shè)置不同的失效時(shí)間,保證不會(huì)同時(shí)失效
- 如果緩存中沒有該key則加鎖,保證只會(huì)有一個(gè)線程打到數(shù)據(jù)庫(kù)進(jìn)行查詢(效率較低)
緩存穿透
解決方案:
- 緩存空對(duì)象或者缺省值
- 如果在數(shù)據(jù)庫(kù)也無(wú)法查詢,同樣回寫到緩存中null值,第二次請(qǐng)求就不會(huì)落到數(shù)據(jù)庫(kù)中
- 如果黑客惡意攻擊每次采取不同的ID那么緩存將越寫越多,所以要設(shè)置過(guò)期時(shí)間,并且大量請(qǐng)求還是直接打中了數(shù)據(jù)庫(kù)所以該方案有缺陷
- 使用布隆過(guò)濾器
大廠真實(shí)需求+面試題?
緩存預(yù)熱、雪崩、穿透、擊穿分別是什么?你遇到過(guò)那幾個(gè)情況?
緩存預(yù)熱你是怎么做的?
如何避免或者減少緩存雪崩?
穿透和擊穿有什么區(qū)別?他兩是一個(gè)意思還是截然不同?
穿透和擊穿你有什么解決方案?如何避免?
假如出現(xiàn)了緩存不一致,你有哪些修補(bǔ)方案?文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-468353.html
。。。。。。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-468353.html
到了這里,關(guān)于第十八章_Redis緩存預(yù)熱+緩存雪崩+緩存擊穿+緩存穿透的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!