1 緩存穿透 問題描述 緩存穿透是指查詢一個一定不存在的數據,由於緩存是不命中時需要從資料庫查詢,查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到資料庫去查詢,進而給資料庫帶來壓力。 解決方案 緩存空值,即對於不存在的數據,在緩存中放置一個空對象(註意,設置過期時間) 2 緩存擊穿 問 ...
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("/stock")
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();
}
}