缓存是一种将数据存储在临时存储器中的技术,以便在需要时能够快速访问该数据。缓存的重要性在于它可以提高系统的性能和响应速度,减轻服务器的负载,节省网络带宽和资源消耗。因此掌握缓存技术是挺重要的哦。
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)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 queryProductList() { - HashMap
responseObj = new HashMap<>(); - // 查询商品缓存列表
- List
productCacheList = stringRedisTemplate.opsForList().range(PRODUCT_LIST_KEY, 0, -1); - // 是否命中缓存
- if (!productCacheList.isEmpty()) {
- // 已命中
- List
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
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 queryProductById(@RequestBody HashMap data) { - HashMap
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 updateProductById(@RequestBody HashMap data) { - HashMap
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 queryProductByIdPlus(@RequestBody HashMap data) { - HashMap
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);
- }
- }
(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;
- }