前言
缓存是一种将数据存储在临时存储器中的技术,以便在需要时能够快速访问该数据。缓存的重要性在于它可以提高系统的性能和响应速度,减轻服务器的负载,节省网络带宽和资源消耗。因此掌握缓存技术是挺重要的哦。
一、缓存之数据库一致性问题
1.删除缓存还是更新缓存?
(1)更新缓存:每次的更新数据库都更新缓存,无效的写操作较多。No
(2)删除缓存:在更新数据库时让缓存失效,查询时再更新缓存。Yes
2.如何保证缓存和数据库的操作同时成功或同时失败?
(1)单体应用:在同一个事务中执行。
(2)分布式系统:使用分布式事务来实现。
3.先操作缓存还是先操作数据库?
先删除缓存,再操作数据库。
二、缓存穿透
1.概念
在缓存中,一个不存在的key被频繁请求,导致每次请求都需要查询数据库,影响系统性能。
2.解决
(1)布隆过滤器:将查询不存在的数据的请求拦截在缓存层之前;或者将查询不存在的数据的请求返回一个默认值,避免直接查询数据库。
(2)使用互斥锁重建缓存:在缓存失效的时候,使用互斥锁来保证只有一个线程去查询数据库并重建缓存,其他线程等待缓存重建完成后再去获取缓存数据。
三、缓存雪崩
1.概念
在缓存中,大量的key在同一时间失效,导致请求直接打到数据库,造成数据库瞬时压力过大,甚至宕机。
2.解决
(1)缓存数据的过期时间随机化:将缓存数据的过期时间进行随机化,避免大量缓存同时失效。
(2)多级缓存:将缓存分为多个层级,如本地缓存、分布式缓存、CDN缓存等,避免单一缓存出现问题导致雪崩。
(2)使用互斥锁重建缓存:在缓存失效的时候,使用互斥锁来保证只有一个线程去查询数据库并重建缓存,其他线程等待缓存重建完成后再去获取缓存数据。
四、缓存击穿
1.概念
在缓存中,某个热点key过期或被删除,导致大量请求直接打到数据库,造成数据库瞬时压力过大,甚至宕机。
2.解决
(1)设置热点数据永不过期:将热点数据设置成永不过期,但是随之而来的就是Redis需要更多的存储空间。
(2)数据预热:在系统启动、请求低峰期,缓存过期前的时候,将常用的数据预先加载到缓存中,避免在高峰期出现大量请求导致缓存失效的情况。
(3)使用互斥锁重建缓存:在缓存失效的时候,使用互斥锁来保证只有一个线程去查询数据库并重建缓存,其他线程等待缓存重建完成后再去获取缓存数据。
五、示例代码
1.控制层
(1)ProductController.java
package org.example.controller;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.example.pojo.dto.ProductDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@Controller
@RequestMapping(value = "api")
public class ProductController {
private static final String PRODUCT_LIST_KEY = "Product-List";
private static final String PRODUCT_KEY = "Product-";
private static final int PRODUCT_CACHE_TTL = 300;
private static final int PRODUCT_NULL_TTL = 60;
private static final String PRODUCT_LOCK_KEY = "Product-Lock-";
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 初始化固定大小为10的线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 查询商品列表(解决缓存穿透问题,除了返回空对象,还需互斥锁重建缓存)
*/
@GetMapping(value = "queryProductList")
@ResponseBody
@CrossOrigin
public <T> T queryProductList() {
HashMap<String, Object> responseObj = new HashMap<>();
// 查询商品缓存列表
List<String> productCacheList = stringRedisTemplate.opsForList().range(PRODUCT_LIST_KEY, 0, -1);
// 是否命中缓存
if (!productCacheList.isEmpty()) {
// 已命中
List<ProductDTO> list = new ArrayList<>();
for (String s : productCacheList) {
ProductDTO productDTO = JSONUtil.toBean(s, ProductDTO.class);
list.add(productDTO);
}
responseObj.put("code", 200);
responseObj.put("success", true);
responseObj.put("data", list);
System.out.println("queryProductList :: 已命中 -> " + list);
return (T) responseObj;
} else {
// 未命中,模拟查询数据库并返回结果集,存入缓存
List<ProductDTO> list = new ArrayList<>();
list.add(new ProductDTO(1L, "面包", 5));
list.add(new ProductDTO(2L, "牛奶", 3));
list.add(new ProductDTO(3L, "苹果", 2));
list.add(new ProductDTO(4L, "香蕉", 2));
for (ProductDTO productDTO : list){
String s = JSONUtil.toJsonStr(productDTO);
productCacheList.add(s);
}
stringRedisTemplate.opsForList().rightPushAll(PRODUCT_LIST_KEY, productCacheList);
responseObj.put("code", 200);
responseObj.put("success", true);
responseObj.put("data", list);
System.out.println("queryProductList :: 未命中 -> " + list);
return (T) responseObj;
}
}
/**
* 根据ID查询商品(解决缓存穿透问题,除了返回空对象,还需互斥锁重建缓存)
* {
* "id": 1
* }
*/
@PostMapping(value = "queryProductById")
@ResponseBody
@CrossOrigin
public <T> T queryProductById(@RequestBody HashMap<String, Object> data) {
HashMap<String, Object> responseObj = new HashMap<>();
Long id = 1L;
String key = PRODUCT_KEY + id;
// 查询商品缓存
String productCache = stringRedisTemplate.opsForValue().get(key);
System.out.println("queryProductById :: productCache -> " + productCache);
// 是否命中缓存
if (productCache != null) {
// 已命中
ProductDTO productDTO = null;
if (!productCache.equals("")) {
productDTO = JSONUtil.toBean(productCache, ProductDTO.class);
}
responseObj.put("code", 200);
responseObj.put("success", true);
responseObj.put("data", productDTO);
System.out.println("queryProductById :: 已命中 -> " + productDTO);
return (T) responseObj;
} else {
// 未命中,模拟查询数据库并返回结果集,存入缓存
ProductDTO productDTO = null;
productDTO = new ProductDTO(1L, "面包", 5);
if (productDTO == null) {
// 若数据库查询也是空,则直接缓存空对象
stringRedisTemplate.opsForValue().set(key,"", PRODUCT_NULL_TTL, TimeUnit.MINUTES);
} else {
// 若数据库查询非空,则缓存非空对象
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(productDTO), PRODUCT_CACHE_TTL, TimeUnit.MINUTES);
}
responseObj.put("code", 200);
responseObj.put("success", true);
responseObj.put("data", productDTO);
System.out.println("queryProductById :: 未命中 -> " + productDTO);
return (T) responseObj;
}
}
/**
* 根据ID更新商品(先更新数据库再删除缓存)
* {
* "id": 1,
* "name": "美味的面包"
* "price": 1
* }
*/
@PostMapping(value = "updateProductById")
@ResponseBody
@CrossOrigin
public <T> T updateProductById(@RequestBody HashMap<String, Object> data) {
HashMap<String, Object> responseObj = new HashMap<>();
// 模拟更新数据库
// xxxService.updateProductById(data)
// 删除商品缓存
Long id = 1L;
stringRedisTemplate.delete(PRODUCT_KEY + id);
stringRedisTemplate.delete(PRODUCT_LIST_KEY);
responseObj.put("code", 200);
responseObj.put("success", true);
return (T) responseObj;
}
/**
* 根据ID查询商品(解决缓存击穿问题,互斥锁重建缓存)
* {
* "id": 1
* }
*/
@PostMapping(value = "queryProductByIdPlus")
@ResponseBody
@CrossOrigin
public <T> T queryProductByIdPlus(@RequestBody HashMap<String, Object> data) {
HashMap<String, Object> responseObj = new HashMap<>();
String key = PRODUCT_KEY + 1;
Long id = 1L;
// 查询商品缓存
ProductDTO productDTO = null;
String productCache = stringRedisTemplate.opsForValue().get(key); // GET Product-1
if (StrUtil.isBlank(productCache)) {
// 缓存过期或不存在,返回空,开启一个互斥锁的线程进行重建缓存
String lockKey = PRODUCT_LOCK_KEY + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
this.buildProductCache(id, 30L);
System.out.println("queryProductByIdPlus :: 缓存未命中,重建缓存");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
System.out.println("queryProductByIdPlus :: 缓存未命中 -> " + productDTO);
responseObj.put("code", 200);
responseObj.put("success", true);
responseObj.put("data", "");
return (T) responseObj;
} else {
// 缓存命中
productDTO = JSONUtil.toBean(productCache, ProductDTO.class);
System.out.println("queryProductByIdPlus :: 缓存已命中 -> " + productDTO);
}
responseObj.put("code", 200);
responseObj.put("success", true);
responseObj.put("data", productDTO);
return (T) responseObj;
}
private boolean tryLock(String lockKey) {
// SETNX Product-1 1 # 在指定的key不存在时,为key设置指定的值,若设置成功则返回1,若设置失败则返回0
// EXPIRE Product-1 10
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
// 注意不能直接返回,直接返回存在拆箱操作,可能会有空指针
return BooleanUtil.isTrue(flag);
}
private void unLock(String lockKey) {
stringRedisTemplate.delete(lockKey);
}
private void buildProductCache(Long id, Long expireTime) {
// 模拟查询数据库并返回结果集,存入缓存
ProductDTO productDTO = null;
// productDTO = new ProductDTO(id, "面包", 5);
if (productDTO == null) {
// 缓存兜底数据
productDTO = new ProductDTO();
}
// SET Product-1 {}
// EXPIRE Product-1 30
// TTL Product-1
stringRedisTemplate.opsForValue().set(PRODUCT_KEY + id, JSONUtil.toJsonStr(productDTO), expireTime, TimeUnit.SECONDS);
}
}
2.简单对象
(1)UserDTO.java
package org.example.pojo.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class UserDTO {
private String phone;
private String username;
}