8.1 緩存預(yù)熱
8.1.1 是什么
緩存預(yù)熱就是系統(tǒng)上線后,提前將相關(guān)的緩存數(shù)據(jù)直接加載到緩存系統(tǒng)。避免在用戶請求的時候,先查詢數(shù)據(jù)庫,然后再將數(shù)據(jù)緩存的問題!用戶直接查詢事先被預(yù)熱的緩存數(shù)據(jù)!
8.1.2 解決
使用 @PostConstruct 初始化白名單數(shù)據(jù)
8.2 緩存雪崩
8.2.1 是什么
緩存雪崩就是瞬間過期數(shù)據(jù)量太大,導(dǎo)致對數(shù)據(jù)庫服務(wù)器造成壓力。
8.2.2 發(fā)生
- redis 主機掛了, Redis全盤崩潰,偏硬件運維
- redis 中有大量key 同時過期大面積失效,偏軟件開發(fā)
8.2.3 預(yù)防 + 解決
- redis 中 key 設(shè)置為永不過期 or 過期時間錯開
- redis 緩存集群實現(xiàn)高可用
- 主從 + 哨兵
- Redis 集群
- 開啟Redis 持久化機制 aof / rdb,盡快恢復(fù)緩存集群
- 多緩存結(jié)合預(yù)防雪崩
- ehcache 本地緩存 + redis緩存
- 服務(wù)降級
- Hystrix 或者 sentinel 限流 & 降級
8.3 緩存穿透
8.3.1 是什么
緩存穿透 就是請求去查詢一條數(shù)據(jù),先查redis,redis里面沒有,再查mysql,mysql里面無,都查詢不到該條記錄,但是請求每次都會打到數(shù)據(jù)庫上面去,導(dǎo)致后臺數(shù)據(jù)庫壓力暴增
8.3.2 解決
1 空對象緩存或者缺省值
如果發(fā)生緩存穿透,可以針對要查詢的數(shù)據(jù),在Redis里存一個和業(yè)務(wù)部門商量后確定的缺省值(比如 零、負(fù)數(shù)、defaultNull等)
public Customer findCustomerById(Integer customerId) {
Customer customer = null;
// 緩存redis的key名稱
String key = CACHE_KEY_CUSTOMER + customerId;
// 1.去redis上查詢
customer = (Customer) redisTemplate.opsForValue().get(key);
// 2. 如果redis有,直接返回 如果redis沒有,在mysql上查詢
if (customer == null) {
// 3.對于高QPS的優(yōu)化,進(jìn)來就先加鎖,保證一個請求操作,讓外面的redis等待一下,避免擊穿mysql(大公司的操作 )
synchronized (CustomerService.class) {
// 3.1 第二次查詢redis,加鎖后
customer = (Customer) redisTemplate.opsForValue().get(key);
// 4.再去查詢我們的mysql
customer = customerMapper.selectByPrimaryKey(customerId);
// 5.mysql有,redis無
if (customer != null) {
// 6.把mysql查詢到的數(shù)據(jù)會寫到到redis, 保持雙寫一致性 7天過期
redisTemplate.opsForValue().set(key, customer, 7L, TimeUnit.DAYS);
}else {
// defaultNull 規(guī)定為redis查詢?yōu)榭?、MySQL查詢也沒有,緩存一個defaultNull標(biāo)識為空,以防緩存穿透
redisTemplate.opsForValue().set(key, "defaultNull", 7L, TimeUnit.DAYS);
}
}
}
return customer;
}
2 Google布隆過濾器Guava
案例:白名單過濾器
- POM
<!--guava Google 開源的 Guava 中自帶的布隆過濾器-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
-
業(yè)務(wù)類
- GUavaBloomFilterController
import com.xfcy.service.GuavaBloomFilterService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; 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; /** * @author 曉風(fēng)殘月Lx * @date 2023/3/29 19:06 * guava版的布隆過濾器 谷歌開源 */ @Api(tags = "gogle工具Guava處理布隆過濾器") @RestController @Slf4j public class GuavaBloomFilterController { @Resource private GuavaBloomFilterService guavaBloomFilterService; @ApiOperation("guava布隆過濾器插入100萬樣本數(shù)據(jù),額外10w(110w)測試是否存在") @RequestMapping(value = "/guavafilter", method = RequestMethod.GET) public void guavaBloomFilter() { guavaBloomFilterService.guavaBloomFilter(); } }
-
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; /** * @author 曉風(fēng)殘月Lx * @date 2023/3/29 19:17 */ @Slf4j @Service public class GuavaBloomFilterService { // 1.定義一個常量 public static final int _1W = 10000; // 2.定義我們guava布隆過濾器,初始容量 public static final int SIZE = 100 * _1W; // 3.誤判率,它越小誤判的個數(shù)也越少(思考:是否可以無限??? 沒有誤判豈不是更好) public static double fpp = 0.01; // 這個數(shù)越小所用的hash函數(shù)越多,bitmap占用的位越多 默認(rèn)的就是0.03,5個hash函數(shù) 0.01,7個函數(shù) // 4.創(chuàng)建guava布隆過濾器 private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), SIZE, fpp); public void guavaBloomFilter() { // 1.先讓bloomFilter加入100w白名單數(shù)據(jù) for (int i = 0; i < SIZE; i++) { bloomFilter.put(i); } // 2.故意取10w個不在合法范圍內(nèi)的數(shù)據(jù),來進(jìn)行誤判率的演示 ArrayList<Integer> list = new ArrayList<>(10 * _1W); // 3.驗證 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()); } }
8.4 緩存擊穿
8.4.1 是什么
緩存擊穿就是大量請求同時查詢一個key時,此時這個key正好失效了,就會導(dǎo)致大量的請求都打到數(shù)據(jù)庫上面去,也就是熱點key突然都失效了,MySQL承受高并發(fā)量
8.4.2 解決
1.差異失效時間,對于訪問頻繁的熱點key,干脆就不設(shè)置過期時間
2.互斥更新,采用雙檢加鎖
8.4.3 案例編碼(防止緩存擊穿)
對于分頁顯示數(shù)據(jù),在高并發(fā)下,絕對不能使用mysql,可以用redis的list結(jié)構(gòu)
差異失效時間 用在雙緩存架構(gòu)
-
Product類
import io.swagger.annotations.ApiModel; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author 曉風(fēng)殘月Lx * @date 2023/3/30 9:40 */ @ApiModel(value = "聚劃算活動product信息") @AllArgsConstructor @NoArgsConstructor @Data public class Product { // 產(chǎn)品id private Long id; // 產(chǎn)品名稱 private String name; // 產(chǎn)品價格 private Integer price; // 產(chǎn)品詳情 private String detail; }
-
JHSTaskService(采用定時器將參加活動的商品加入redis)文章來源:http://www.zghlxwxcb.cn/news/detail-403124.html
import com.xfcy.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; /** * @author 曉風(fēng)殘月Lx * @date 2023/3/30 9:43 */ @Service @Slf4j 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; /** * 模擬從數(shù)據(jù)庫讀取20件特價商品 * @return */ private List<Product> getProductsFromMysql() { List<Product> list = new ArrayList<>(); for (int i = 0; i <= 20; i++) { Random random = new Random(); int id = random.nextInt(10000); Product product = new Product((long) id, "product" + i, i, "detail"); list.add(product); } return list; } //@PostConstruct // 測試單緩存 public void initJHS(){ log.info("啟動定時器 天貓聚劃算模擬開始 ==============="); // 1.用線程模擬定時任務(wù),后臺任務(wù)定時將mysql里面的特價商品刷新的redis new Thread(() -> { while (true){ // 2.模擬從mysql查到數(shù)據(jù),加到redis并返回給頁面 List<Product> list = this.getProductsFromMysql(); // 3.采用redis list數(shù)據(jù)結(jié)構(gòu)的lpush命令來實現(xiàn)存儲 redisTemplate.delete(JHS_KEY); // 4.加入最新的數(shù)據(jù)給redis redisTemplate.opsForList().leftPushAll(JHS_KEY, list); // 5.暫停1分鐘,間隔1分鐘執(zhí)行一次,模擬聚劃算一天執(zhí)行的參加活動的品牌 try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } },"t1").start(); } /** * 差異失效時間 */ @PostConstruct // 測試雙緩存 public void initJHSAB(){ log.info("啟動AB的定時器 天貓聚劃算模擬開始 ==============="); // 1.用線程模擬定時任務(wù),后臺任務(wù)定時將mysql里面的特價商品刷新的redis new Thread(() -> { while (true){ // 2.模擬從mysql查到數(shù)據(jù),加到redis并返回給頁面 List<Product> list = this.getProductsFromMysql(); // 3.先更新B緩存且讓B緩存過期時間超過緩存A時間,如果A突然失效了還有B兜底,防止擊穿 redisTemplate.delete(JHS_KEY_B); redisTemplate.opsForList().leftPushAll(JHS_KEY_B, list); redisTemplate.expire(JHS_KEY_B, 86410L, TimeUnit.SECONDS); // 4.再更新A緩存 redisTemplate.delete(JHS_KEY_A); redisTemplate.opsForList().leftPushAll(JHS_KEY_A, list); redisTemplate.expire(JHS_KEY_A, 86400L, TimeUnit.SECONDS); // 5.暫停1分鐘,間隔1分鐘執(zhí)行一次,模擬聚劃算一天執(zhí)行的參加活動的品牌 try { TimeUnit.MINUTES.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } },"t1").start(); } }
-
JHSProductController類文章來源地址http://www.zghlxwxcb.cn/news/detail-403124.html
import com.xfcy.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; /** * @author 曉風(fēng)殘月Lx * @date 2023/3/30 9:55 */ @RestController @Slf4j @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; /** * 分頁查詢:在高并發(fā)情況下,只能走redis查詢,走db必定會把db打垮 * @param page * @param size * @return */ @RequestMapping(value = "/product/find", method = RequestMethod.GET) @ApiOperation("聚劃算案例,每次1頁每頁5條顯示") 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結(jié)構(gòu)里面的range命令來實現(xiàn)加載和分頁 list = redisTemplate.opsForList().range(JHS_KEY, start, end); if (CollectionUtils.isEmpty(list)) { // TODO 走mysql查詢 } log.info("參加活動的商家: {}", list); }catch (Exception e){ // 出異常了,一般redis宕機了或者redis網(wǎng)絡(luò)抖動導(dǎo)致timeout log.error("jhs exception{}", e); e.printStackTrace(); // 。。。重試機制 再次查詢mysql } return list; } @RequestMapping(value = "/product/findAB", method = RequestMethod.GET) @ApiOperation("AB雙緩存架構(gòu),防止熱點key突然消失") 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結(jié)構(gòu)里面的range命令來實現(xiàn)加載和分頁 list = redisTemplate.opsForList().range(JHS_KEY_A, start, end); if (CollectionUtils.isEmpty(list)) { log.info("-----A緩存已經(jīng)過期或活動結(jié)束了,記得人工修補,B緩存繼續(xù)頂著"); // A沒有來找B list = redisTemplate.opsForList().range(JHS_KEY_B, start, end); if (CollectionUtils.isEmpty(list)){ // TODO 走mysql查詢 } } log.info("參加活動的商家: {}", list); }catch (Exception e){ // 出異常了,一般redis宕機了或者redis網(wǎng)絡(luò)抖動導(dǎo)致timeout log.error("jhs exception{}", e); e.printStackTrace(); // 。。。重試機制 再次查詢mysql } return list; } }
8.5 總結(jié)
緩存問題 | 產(chǎn)生原因 | 解決方案 |
---|---|---|
緩存更新不一致 | 數(shù)據(jù)變更、緩存時效性 | 同步更新、失效更新、異步更新、定時更新 |
緩存不一致 | 同步更新失敗、異步更新 | 增加重試、補償任務(wù)、最終一致 |
緩存穿透 | 惡意攻擊 | 空對象緩存、bloomFilter 過濾器 |
緩存擊穿 | 熱點key失效 | 互斥更新、隨即退避、差異失效時間 |
緩存雪崩 | 緩存掛掉 | 快速失敗熔斷、主從模式、集群模式 |
到了這里,關(guān)于Redis7之緩存預(yù)熱 + 緩存雪崩 + 緩存擊穿 + 緩存穿透(八)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!