1 緩存穿透
問題描述
緩存穿透是指查詢一個一定不存在的數據,由於緩存是不命中時需要從數據庫查詢,查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到數據庫去查詢,進而給數據庫帶來壓力。
解決方案
緩存空值,即對於不存在的數據,在緩存中放置一個空對象(注意,設置過期時間)
2 緩存擊穿
問題描述
緩存擊穿是指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的併發請求過來,從而大量的請求打到數據庫。
解決方案
加互斥鎖,在併發的多個請求中,只有第一個請求線程能拿到鎖並執行數據庫查詢操作,其他的線程拿不到鎖就阻塞等着,等到第一個線程將數據寫入緩存後,直接走緩存。
3 緩存雪崩
問題描述
緩存雪崩是指緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至down機。
解決方案
可以給緩存的過期時間時加上一個隨機值時間,使得每個 key 的過期時間分佈開來,不會集中在同一時刻失效。
4 緩存服務器宕機
問題描述
併發太高,緩存服務器連接被打滿,最後掛了
解決方案
- 限流:nginx、spring cloud gateway、sentinel等都支持限流
- 增加本地緩存(JVM內存緩存),減輕redis一部分壓力
5 Redis實現分佈式鎖
問題描述
如果用redis做分佈式鎖的話,有可能會存在這樣一個問題:key丟失。比如,master節點寫成功了還沒來得及將它複製給slave就掛了,於是slave成爲新的master,於是key丟失了,後果就是沒鎖住,多個線程持有同一把互斥鎖。
解決方案
必須等redis把這個key複製給所有的slave並且都持久化完成後,才能返回加鎖成功。但是這樣的話,對其加鎖的性能就會有影響。
zookeeper同樣也可以實現分佈式鎖。在分佈式鎖的的實現上,zookeeper的重點是CP,redis的重點是AP。因此,要求強一致性就用zookeeper,對性能要求比較高的話就用redis
5 示例代碼
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo426</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo426</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.1</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Product.java
package com.example.demo426.domain;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* @Author ChengJianSheng
* @Date 2022/4/26
*/
@Data
public class Product implements Serializable {
private Long productId;
private String productName;
private Integer stock;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Integer isDeleted;
private Integer version;
}
ProductController.java
package com.example.demo426.controller;
import com.alibaba.fastjson.JSON;
import com.example.demo426.domain.Product;
import com.example.demo426.service.ProductService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @Author ChengJianSheng
* @Date 2022/4/26
*/
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private RedissonClient redissonClient;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Autowired
private ProductService productService;
private final Cache PRODUCT_LOCAL_CACHE = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(Duration.ofMinutes(60))
.build();
private final String PRODUCT_CACHE_PREFIX = "cache:product:";
private final String PRODUCT_LOCK_PREFIX = "lock:product:";
private final String PRODUCT_RW_LOCK_PREFIX = "lock:rw:product:";
/**
* 更新
* 寫緩存的方式有這麼幾種:
* 1. 更新完數據庫後,直接刪除緩存
* 2. 更新完數據庫後,主動更新緩存
* 3. 更新完數據庫後,發MQ消息,由消費者去刷新緩存
* 4. 利用canal等工具,監聽MySQL數據庫binlog,然後去刷新緩存
*/
@PostMapping("/update")
public void update(@RequestBody Product productDTO) {
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(PRODUCT_RW_LOCK_PREFIX + productDTO.getProductId());
RLock wLock = readWriteLock.writeLock();
wLock.lock();
try {
// 寫數據庫
// update product set name=xxx,...,version=version+1 where id=xx and version=xxx
Product product = productService.update(productDTO);
// 放入緩存
PRODUCT_LOCAL_CACHE.put(product.getProductId(), product);
stringRedisTemplate.opsForValue().set(PRODUCT_CACHE_PREFIX + product.getProductId(), JSON.toJSONString(product), getProductTimeout(60), TimeUnit.MINUTES);
} finally {
wLock.unlock();
}
}
/**
* 查詢
*/
@GetMapping("/query")
public Product query(@RequestParam("productId") Long productId) {
// 1. 嘗試從緩存讀取
Product product = getProductFromCache(productId);
if (null != product) {
return product;
}
// 2. 準備從數據庫中加載
// 互斥鎖
RLock lock = redissonClient.getLock(PRODUCT_LOCK_PREFIX + productId);
lock.lock();
try {
// 再次先查緩存
product = getProductFromCache(productId);
if (null != product) {
return product;
}
// 爲了避免緩存與數據庫雙寫不一致
// 讀寫鎖
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(PRODUCT_RW_LOCK_PREFIX + productId);
RLock rLock = readWriteLock.readLock();
rLock.lock();
try {
// 查數據庫
product = productService.getById(productId);
if (null == product) {
// 如果數據庫中沒有,則放置一個空對象,這樣做是爲了避免”緩存穿透“問題
product = new Product();
} else {
PRODUCT_LOCAL_CACHE.put(productId, product);
}
// 放入緩存
stringRedisTemplate.opsForValue().set(PRODUCT_CACHE_PREFIX + productId, JSON.toJSONString(product), getProductTimeout(60), TimeUnit.MINUTES);
} finally {
rLock.unlock();
}
} finally {
lock.unlock();
}
return null;
}
/**
* 查緩存
*/
private Product getProductFromCache(Long productId) {
// 1. 嘗試從本地緩存讀取
Product product = PRODUCT_LOCAL_CACHE.getIfPresent(productId);
if (null != product) {
return product;
}
// 2. 嘗試從Redis中讀取
String key = PRODUCT_CACHE_PREFIX + productId;
String value = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) {
product = JSON.parseObject(value, Product.class);
return product;
}
return null;
}
/**
* 爲了避免緩存集體失效,故而加了隨機時間
*/
private int getProductTimeout(int initVal) {
Random random = new Random(10);
return initVal + random.nextInt();
}
}