赞
踩
缓存穿透
缓存穿透是指在缓存中查找一个不存在的值,由于缓存一般不会存储这种无效的数据,所以每次查询都会落到数据库上,导致数据库压力增大,严重时可能会导致数据库宕机。
解决方案:
1 缓存空值 (本文此方案)
2 布隆过滤器
3 增强id的复杂度
4 做好数据的基础格式校验
5 做好热点参数的限流
缓存击穿
缓存击穿是指一个被频繁访问(高并发访问并且缓存重建业务较复杂)的缓存键因为过期失效,同时又有大量并发请求访问此键,导致请求直接落到数据库或后端服务上,增加了系统的负载并可能导致系统崩溃
解决方案
1 互斥锁
2 逻辑过期
1 前提先好做redis与springboot的集成,redisson的集成【用于加锁解锁】【本文用的redisson】
另外用到了hutool的依赖
2 缓存对象封装的类,这里只是逻辑过期方案可以用上,你也可以自己改
- /**
- * 决缓存击穿--(设置逻辑过期时间)
- */
- @Data
- public class RedisData {
- //逻辑过期时间
- private LocalDateTime expireTime;
- //缓存实际的内容
- private Object data;
- }
3 相关的常量
- public class Constant {
-
- //缓存空值的ttl时间
- public static final Long CACHE_NULL_TTL = 2L;
-
- //缓存时间,单位程序里参数传
- public static final Long CACHE_NEWS_TTL = 10L;
-
- //缓存前缀,根据模块来
- public static final String CACHE_NEWS_KEY = "cache:news:";
-
- //锁-前缀,根据模块来
- public static final String LOCK_NEWS_KEY = "lock:news:";
-
- //持有锁的时间
- public static final Long LOCK_TTL = 10L;
- }
4 缓存核心类
- import cn.hutool.core.util.StrUtil;
- import cn.hutool.json.JSONObject;
- import cn.hutool.json.JSONUtil;
- import lombok.extern.slf4j.Slf4j;
- import org.redisson.api.RLock;
- import org.redisson.api.RedissonClient;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.stereotype.Component;
- import java.time.LocalDateTime;
- import java.time.temporal.ChronoUnit;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.TimeUnit;
- import java.util.concurrent.atomic.AtomicInteger;
- import java.util.function.Function;
-
- import static org.example.service_a.cache.Constant.CACHE_NULL_TTL;
- import static org.example.service_a.cache.Constant.LOCK_NEWS_KEY;
-
-
- @Slf4j
- @Component
- //封装的将Java对象存进redis 的工具类
- public class CacheClient {
-
- @Autowired
- private StringRedisTemplate stringRedisTemplate;
-
- @Autowired
- private RedissonClient redissonClient;
-
- // 定义线程池
- private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
-
- AtomicInteger atomicInteger = new AtomicInteger();
-
- /**
- * 设置TTL过期时间set
- *
- * @param key
- * @param value
- * @param time
- * @param unit
- */
- public void set(String key, Object value, Long time, TimeUnit unit) {
- // 需要把value序列化为string类型
- String jsonStr = JSONUtil.toJsonStr(value);
- stringRedisTemplate.opsForValue().set(key, jsonStr, time, unit);
- }
-
- /**
- * 缓存穿透功能封装
- *
- * @param id
- * @return
- */
- public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
-
- String key = keyPrefix + id;
- //1. 从Redis中查询缓存
- String Json = stringRedisTemplate.opsForValue().get(key);
- //2. 判断是否存在
- if (StrUtil.isNotBlank(Json)) {
- //3. 存在,直接返回
- return JSONUtil.toBean(Json, type);
- }
- // 这里要先判断命中的是否是null,因为是null的话也是被上面逻辑判断为不存在
- // 这里要做缓存穿透处理,所以要对null多做一次判断,如果命中的是null则shopJson为""
- if ("".equals(Json)) {
- return null;
- }
- //4. 不存在,根据id查询数据库
- R r = dbFallback.apply(id);
- log.error("查询数据库次数 {}",atomicInteger.incrementAndGet());
- if (r == null) {
- //5. 不存在,将null写入redis,以便下次继续查询缓存时,如果还是查询空值可以直接返回false信息
- stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
- return null;
- }
- //6. 存在,写入Redis
- this.set(key, r, time, unit);
- //7. 返回
- return r;
- }
-
- /**
- * 解决缓存击穿--(互斥锁)
- * @param keyPrefix
- * @param id
- * @param type
- * @param dbFallback
- * @param time
- * @param unit
- * @return
- * @param <R>
- * @param <ID>
- */
- public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit)
- {
- String key = keyPrefix + id;
- // 1.从redis查询商铺缓存
- String shopJson = stringRedisTemplate.opsForValue().get(key);
- // 2.判断是否存在
- if (StrUtil.isNotBlank(shopJson)) {
- // 3.存在,直接返回
- return JSONUtil.toBean(shopJson, type);
- }
- // 判断命中的是否是空值
- if (shopJson != null) {
- // 返回一个错误信息
- return null;
- }
- log.error("缓存重建----");
- // 4.实现缓存重建
- // 4.1.获取互斥锁
- String lockKey = LOCK_NEWS_KEY + id;
- R r = null;
- try {
- boolean isLock = tryLock(lockKey);
- // 4.2.判断是否获取成功
- if (!isLock) {
- // 4.3.获取锁失败,休眠并重试
- Thread.sleep(10);
- return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
- }
- // 4.4.获取锁成功,根据id查询数据库
- r = dbFallback.apply(id);
- log.info("查询数据库次数 {}",atomicInteger.incrementAndGet());
- // 5.不存在,返回错误
- if (r == null) {
- // 将空值写入redis
- stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
- // 返回错误信息
- return null;
- }
- // 6.存在,写入redis
- this.set(key, r, time, unit);
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }finally {
- // 7.释放锁
- unLock(lockKey);
- }
- // 8.返回
- return r;
- }
-
-
- /**
- * --------------注意 key 没有加过期时间,会一直存在,只是 缓存的内容里有个字段,标识了过期的时间----------------
- * 设置逻辑过期set
- *
- * @param key
- * @param value
- * @param time
- * @param chronoUnit
- */
- public void setWithLogicExpire(String key, Object value, Long time, ChronoUnit chronoUnit) {
- // 设置逻辑过期
- RedisData redisData = new RedisData();
- redisData.setData(value);
- redisData.setExpireTime(LocalDateTime.now().plus(time, chronoUnit));
- // 需要把value序列化为string类型
- String jsonStr = JSONUtil.toJsonStr(redisData);
- stringRedisTemplate.opsForValue().set(key, jsonStr);
- }
-
-
- /**
- * 解决缓存击穿--(设置逻辑过期时间)方式
- * 1. 组合键名,从Redis查询缓存。
- * 2. 缓存不存在,直接返回(预设热点数据已预热)。
- * 3. 解析缓存内容,获取过期时间。
- * 4. 若未过期,直接返回数据。
- * 5. 已过期,执行缓存重建流程:
- * a. 尝试获取互斥锁。
- * b. 二次检查缓存是否已重建且未过期,若是则返回数据。
- * c. 成功获取锁,异步执行:
- * i. 查询数据库获取最新数据。
- * ii. 重新写入Redis缓存,附带新的逻辑过期时间。
- * iii. 最终释放锁。
- * 6. 未能获取锁,直接返回旧数据。
- *
- * @param id
- * @return
- */
- public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, ChronoUnit chronoUnit) throws InterruptedException {
- String key = keyPrefix + id;
- //1. 从Redis中查询缓存
- String Json = stringRedisTemplate.opsForValue().get(key);
- //2. 判断是否存在
- if (StrUtil.isBlank(Json)) {
- //3. 不存在,直接返回(这里做的是热点key,先要预热,所以已经假定热点key已经在缓存中)
- return null;
- }
- //4. 存在,需要判断过期时间,需要先把json反序列化为对象
- RedisData redisData = JSONUtil.toBean(Json, RedisData.class);
- R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
- LocalDateTime expireTime = redisData.getExpireTime();
- //5. 判断是否过期
- if (expireTime.isAfter(LocalDateTime.now())) {
- //5.1 未过期,直接返回店铺信息
- return r;
- }
- log.error("缓存内容已逻辑过期-----------{}",LocalDateTime.now());
- //5.2 已过期,需要缓存重建
- //6. 缓存重建
- //6.1 获取互斥锁
- String lockKey = LOCK_NEWS_KEY + id;
- //6.2 判断是否获取锁成功
- boolean isLock = tryLock(lockKey);
- if (isLock) {
- // 二次验证是否过期,防止多线程下出现缓存重建多次
- String Json2 = stringRedisTemplate.opsForValue().get(key);
- // 这里假定key存在,所以不做存在校验
- // 存在,需要判断过期时间,需要先把json反序列化为对象
- RedisData redisData2 = JSONUtil.toBean(Json2, RedisData.class);
- R r2 = JSONUtil.toBean((JSONObject) redisData2.getData(), type);
- LocalDateTime expireTime2 = redisData2.getExpireTime();
- if (expireTime2.isAfter(LocalDateTime.now())) {
- // 未过期,直接返回店铺信息
- return r2;
- }
- //6.3 成功,开启独立线程,实现缓存重建
- CACHE_REBUILD_EXECUTOR.submit(() -> {
- try {
- // 重建缓存,这里设置的值小一点,方便观察程序执行效果,实际开发应该设为30min
- // 查询数据库
- R apply = dbFallback.apply(id);
-
- log.info("查询数据库次数 {}",atomicInteger.incrementAndGet());
-
- // 写入redis
- this.setWithLogicExpire(key, apply, time, chronoUnit);
- } catch (Exception e) {
- throw new RuntimeException(e);
- } finally {
- // 释放锁
- unLock(lockKey);
- }
- });
- }
- //7. 返回,如果没有获得互斥锁,会直接返回旧数据
- return r;
- }
-
-
- /**
- * 加锁
- * @param lockKey
- * @return
- */
- private boolean tryLock(String lockKey) {
- RLock lock = redissonClient.getLock(lockKey);
- try {
- // 尝试获取锁,最多等待10秒,获取到锁后自动 LOCK_SHOP_TTL 0秒后解锁
- return lock.tryLock(10, Constant.LOCK_TTL, TimeUnit.SECONDS);
- } catch (Exception e) {
- Thread.currentThread().interrupt();
- // 重新抛出中断异常
- log.error("获取锁时发生中断异常", e);
- return false;
- }
- }
-
- /**
- * 解锁
- * @param lockKey
- */
- private void unLock(String lockKey) {
- RLock lock = redissonClient.getLock(lockKey);
- lock.unlock(); // 解锁操作
- }
-
- }
5 缓存预热和测试
- import cn.hutool.json.JSONUtil;
- import org.example.common.AppResult;
- import org.example.common.AppResultBuilder;
- import org.example.service_a.cache.CacheClient;
- import org.example.service_a.cache.RedisData;
- import org.example.service_a.domain.News;
- import org.example.service_a.service.NewsService;
- import org.redisson.api.RLock;
- import org.redisson.api.RedissonClient;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.dao.DataAccessException;
- import org.springframework.data.redis.core.RedisOperations;
- import org.springframework.data.redis.core.SessionCallback;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.validation.annotation.Validated;
- import org.springframework.web.bind.annotation.PathVariable;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- import javax.annotation.PostConstruct;
- import java.time.LocalDateTime;
- import java.time.temporal.ChronoUnit;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.concurrent.TimeUnit;
-
- import static org.example.service_a.cache.Constant.CACHE_NEWS_KEY;
- import static org.example.service_a.cache.Constant.CACHE_NEWS_TTL;
-
-
- @RestController
- @Validated()
- @RequestMapping("/article")
- public class News_Controller {
-
- @Autowired
- private StringRedisTemplate redisTemplate;
-
- @Autowired
- private CacheClient cacheClient;
-
- @Autowired
- private NewsService newsService;
-
- @Autowired
- private RedissonClient redissonClient;
-
- /**
- * @param id 编号
- */
- @RequestMapping("/get/{id}")
- public AppResult<News> getGirl(@PathVariable("id") Long id) throws InterruptedException {
-
- //解决缓存穿透-------->
- News news = cacheClient.queryWithPassThrough(CACHE_NEWS_KEY, id, News.class,
- newsService::getById,
- CACHE_NEWS_TTL, TimeUnit.MINUTES);
-
- //(互斥锁)解决缓存击穿---------->
- // News news = cacheClient.queryWithMutex(CACHE_NEWS_KEY, id, News.class,
- // (x) -> {
- // return newsService.getById(id);
- // }
- // , CACHE_NEWS_TTL, TimeUnit.MINUTES);
-
- //(设置逻辑过期时间)解决缓存击穿---------->
- // News news = cacheClient.queryWithLogicalExpire(
- // CACHE_NEWS_KEY,
- // id,
- // News.class,
- // (x)->{
- // return newsService.getById(id);
- // },
- // CACHE_NEWS_TTL,
- // ChronoUnit.SECONDS);
-
- System.out.println("news = " + news);
- //判断返回值是否为空
- // if (news == null) {
- // return Result.fail("信息不存在");
- // }
- // //返回
- // return Result.ok(news);
- return AppResultBuilder.success(news);
- }
-
- /**
- *缓存预热
- */
- @PostConstruct()
- public void cache_init() {
- RLock lock = redissonClient.getLock("lock:cacheInit");
- lock.lock();
-
- try {
- List<News> list = newsService.list();
- redisTemplate.executePipelined(new SessionCallback<Object>() {
- HashMap<String, Object> objectObjectHashMap = new HashMap<>();
-
- @Override
- public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
-
- list.forEach(news -> {
- //演示缓存击穿--逻辑过期 用这种方式
- // RedisData redisData = new RedisData();
- // redisData.setData(news);
- // redisData.setExpireTime(LocalDateTime.now().plusSeconds(30));
- // objectObjectHashMap.put(CACHE_NEWS_KEY +news.getId(),JSONUtil.toJsonStr(redisData));
-
- //演示缓存击穿--互斥锁 用这种方式
- objectObjectHashMap.put(CACHE_NEWS_KEY + news.getId(), JSONUtil.toJsonStr(news));
- });
-
- operations.opsForValue().multiSet((Map<? extends K, ? extends V>) objectObjectHashMap);
- return null;
- }
- });
- } catch (Exception e) {
- throw new RuntimeException(e);
- } finally {
- lock.unlock();
- }
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。