简历_熟悉缓存高并发场景处理方法,如缓存穿透、缓存击穿、缓存雪崩

系列博客目录


文章目录


1.缓存穿透

缓存穿透 是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见的解决方案有两种:

  1. 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:额外的内存消耗、可能造成短期的不一致
  2. 布隆过滤
    • 优点:内存占用较少,没有多余key
    • 缺点:实现复杂、存在误判可能

这里选择缓存空对象。

总结

  1. 缓存穿透产生的原因是什么?

    用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

  2. 缓存穿透的解决方案有哪些?

    • 缓存null值
    • 布隆过滤
    • 增强id的复杂度,避免被猜测id规律
    • 做好数据的基础格式校验
    • 加强用户权限校验
    • 做好热点参数的限流

2.缓存雪崩

缓存雪崩 是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

这部分后面代码总结中没有,因为就是简单的为TTL设置随机值。

3.缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:互斥锁、逻辑过期


案例:基于逻辑过期方式解决缓存击穿问题

需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

这里最左边缓存未命中,返回空而不进行其他操作,比如查数据库回设缓存,是为了主要掌握逻辑过期的逻辑,而不注重于其他。

代码总结

java 复制代码
package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 实现缓存穿透
        // Shop shop = queryWithPassThrough(id);

        // 利用互斥锁解决缓存击穿
        // Shop shop = queryWithMutex(id);
        // 利用逻辑过期解决缓存击穿
        Shop shop = queryWithLogicalExpire(id);
        if (shop == null) {
            return  Result.fail("店铺不存在");
        }
        // 7.返回
        return Result.ok(shop);
    }


    private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    public Shop queryWithLogicalExpire(Long  id){
        String key = CACHE_SHOP_KEY + id;
        // 1.从Redis查询商户缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isBlank(shopJson)){
            // 3.存在,直接返回
            return null;
        }
        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();

        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())){
            // 5.1 未过期,直接返回店铺信息
            return shop;
        }

        // 5.2 已过期,需要缓存重建
        // 6. 缓存重建
        // 6.1 获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);


        // 6.2 判断是否获取锁成功
        if(isLock){
            // 6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() ->{
                try {
                    //重建缓存
                    this.saveShop2Redis(id, 20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
        }


        // 6.4 不成功,返回过期商铺信息
        return shop;
    }

    public Shop queryWithMutex(Long  id){
        String key = CACHE_SHOP_KEY + id;
        // 1.从Redis查询商户缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        // 命中的是否是空值
        if(shopJson != null){
            // 返回一个错误信息 , 现在是空值
            return null;
        }
        // 实现缓存重建
        // 4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2 判断获取互斥锁是否成功
            if(!isLock){
                // 4.3 失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            // 4.4 成功, 根据id查询数据库
            shop = getById(id);
            // 模拟重建延迟
            Thread.sleep(200);
            // 5.不存在,返回错误
            if(shop == null){
                // 将空值写入Redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放互斥锁
            unLock(lockKey);
        }

        // 8.返回
        return shop;
    }


    public Shop queryWithPassThrough(Long  id){
        String key = CACHE_SHOP_KEY + id;
        // 1.从Redis查询商户缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            // 3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        // 命中的是否是空值
        if(shopJson != null){
            // 返回一个错误信息 , 现在是空值
            return null;
        }
        // 4.不存在,根据id查询数据库
        Shop shop = getById(id);
        // 5.不存在,返回错误
        if(shop == null){
            // 将空值写入Redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 7.返回
        return shop;
    }

    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }


    private void unLock(String key){
        stringRedisTemplate.delete(key);
    }

    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
        // 1.查询店铺数据
        Shop shop = getById(id);
        // 为了更好观察逻辑过期设置睡眠时间
        Thread.sleep(200);
        // 2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));

        // 3.写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null){
            return Result.fail("店铺id不能为空");
        }

        // 1.更新数据库
        updateById(shop);
        // 2.删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());

        return Result.ok();
    }


}
相关推荐
oyzz12034 分钟前
Spring EL 表达式的简单介绍和使用
java·后端·spring
ward RINL1 小时前
redis分页查询
数据库·redis·缓存
oLLI PILO1 小时前
Redis连接池
数据库·redis·缓存
后置的猿猴2 小时前
Spring 循环依赖
java·后端·spring
热爱Java,热爱生活2 小时前
浅谈Spring三级缓存
java·spring·缓存
heRs BART2 小时前
Redis简介、常用命令及优化
数据库·redis·缓存
蒸汽求职3 小时前
破局“无效互面”:跨国大厂视角的工业级 Mock Interview 价值解析
缓存·面试·职场和发展·金融·notion
Irissgwe4 小时前
redis之常见数据类型
数据库·redis·缓存
DROm RAPS4 小时前
redis 配置
数据库·redis·缓存
shark22222225 小时前
Spring 的三种注入方式?
java·数据库·spring