重学Redis解决方案(五)—— 缓存击穿问题及解决思路及封装工具类

缓存击穿问题及解决思路及封装工具类

缓存击穿

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

解决方案

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大

image

使用互斥锁解决缓存击穿问题

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

image

核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询

如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿

image

操作锁的代码:

核心思路就是利用redis的setnx方法来表示获取锁,该方法含义是redis中如果没有这个key,则插入成功,返回1,在stringRedisTemplate中返回true, 如果有这个key则插入失败,则返回0,在stringRedisTemplate返回false,我们可以通过true,或者是false,来表示是否有线程成功插入key,成功插入的key的线程我们认为他就是获得到锁的线程。

package com.luoxiao.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luoxiao.hmdp.dto.Result;
import com.luoxiao.hmdp.entity.Shop;
import com.luoxiao.hmdp.mapper.ShopMapper;
import com.luoxiao.hmdp.service.IShopService;
import com.luoxiao.hmdp.utils.CacheClient;
import com.luoxiao.hmdp.utils.RedisConstants;
import com.luoxiao.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 解决缓存穿透
     */
    @Override
    public Result queryShopById(Long id) {
        //互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);

        Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, this::getById,Shop.class);
        if (shop == null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    /**
     * 缓存击穿解决方案,互斥锁
     */
    private Shop queryWithMutex(Long id) {
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        //redis中存在
        String shopJson = redisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(shopJson)){
            return JSONUtil.toBean(shopJson, Shop.class);
        }

        //判断命中的是否是空值
        if (shopJson == null) {
            return null;
        }

        //互斥锁
        Shop shop = null;
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        try {
            boolean isLock = tryLock(lockKey);
            //判断是否获取成功
            if (!isLock){
                //失败休眠重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }

            //redis中不存在
            shop = getById(id);
            Thread.sleep(200);
            if (shop == null){
                //设置空值
                redisTemplate.opsForValue().set(key, "",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }

            redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }finally {
            //释放锁
            unLock(lockKey);
        }
        return shop;
    }

    /**
     * 获取锁
     */
    private boolean tryLock(String key){
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     */
    private void unLock(String key){
        redisTemplate.delete(key);
    }
}

使用逻辑过期解决缓存击穿问题

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

思路分析:当用户开始查询redis时,判断是否命中,如果没有命中则直接返回空数据,不查询数据库,而一旦命中后,将value取出,判断value中的过期时间是否满足,如果没有过期,则直接返回redis中的数据,如果过期,则在开启独立线程后直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。

image

步骤一、添加一个实体类用于保存数据和逻辑过期时间

@Data
public class RedisData<T> {
    /**
     * 逻辑过期时间
     */
    private LocalDateTime expireTime;
    /**
     * 数据对象
     */
    private T data;
}

步骤二、利用单元测试进行缓存预热

/**
 * 将店铺信息存入到redis中
 */
private void saveShopRedis(Long id, Long expireSeconds){
    Shop shop = getById(id);
    //封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    //写入Redis
    redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
@Test
void testSaveShop(){
    Shop shop = shopService.getById(1L);
    cacheClient.setWithLogicalExpire(CACHE_SHOP_KEY + 1L, shop, 10L, TimeUnit.SECONDS);
}

步骤三:正式代码

package com.luoxiao.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.luoxiao.hmdp.dto.Result;
import com.luoxiao.hmdp.entity.Shop;
import com.luoxiao.hmdp.mapper.ShopMapper;
import com.luoxiao.hmdp.service.IShopService;
import com.luoxiao.hmdp.utils.CacheClient;
import com.luoxiao.hmdp.utils.RedisConstants;
import com.luoxiao.hmdp.utils.RedisData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;


    /**
     * 解决缓存击穿
     */
    @Override
    public Result queryShopById(Long id) {
        //设置逻辑过期解决缓存击穿
        Shop shop = queryWithLogicalExpire(id);
        Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, this::getById,Shop.class);
        if (shop == null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    public Shop queryWithLogicalExpire( Long id ) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.存在,直接返回
            return null;
        }
        // 4.命中,需要先把json反序列化为对象
        RedisData<Shop> redisData = JSONUtil.toBean(json, RedisData.class);
        Shop shop = redisData.getData();
        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){
            threadPoolTaskExecutor.submit( ()->{
                try{
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return shop;
    }

    /**
     * 获取锁
     */
    private boolean tryLock(String key){
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     */
    private void unLock(String key){
        redisTemplate.delete(key);
    }
}

Redis工具类封装

基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
  • 方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
  • 方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓

存击穿问题

  • 方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
  • 方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

将逻辑进行封装

package com.luoxiao.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.luoxiao.hmdp.entity.Shop;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
@Component
public class CacheClient {

    private StringRedisTemplate redisTemplate;

    private ThreadPoolTaskExecutor threadPoolTaskExecutor;


    public CacheClient(StringRedisTemplate redisTemplate, ThreadPoolTaskExecutor threadPoolTaskExecutor) {
        this.redisTemplate = redisTemplate;
        this.threadPoolTaskExecutor = threadPoolTaskExecutor;
    }

    /**
     * 存入redis
     * @param key key
     * @param value 对象
     */
    public void set(String key, Object value){
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
    }

    /**
     * 存入redis
     * @param key key
     * @param value 对象
     * @param time 时间
     */
    public void set(String key, Object value,Long time){
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,TimeUnit.MINUTES);
    }

    /**
     * 存入redis
     * @param key key
     * @param value 对象
     * @param unit 时间单位
     */
    public void set(String key, Object value, TimeUnit unit){
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),RedisConstants.CACHE_SHOP_TTL,unit);
    }

    /**
     * 存入redis
     * @param key key
     * @param value 对象
     * @param time 时间
     * @param unit 时间单位
     */
    public void set(String key, Object value, Long time, TimeUnit unit){
        redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    /**
     * 逻辑过期
     * @param key key
     * @param value 对象
     */
    public void setWithLogicalExpire(String key, Object value){
        RedisData<Object> data = new RedisData<>();
        data.setExpireTime(LocalDateTime.now().plusSeconds(RedisConstants.CACHE_SHOP_TTL));
        data.setData(value);
        redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(data));
    }

    /**
     * 逻辑过期
     * @param key key
     * @param value 对象
     * @param time 时间
     * @param unit 时间单位
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        RedisData<Object> data = new RedisData<>();
        data.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        data.setData(value);
        redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(data));
    }

    /**
     * 缓存穿透解决方案,缓存未命中设置空值(带有逻辑过期的查询)
     * @param keyPrefix key前缀
     * @param id id
     * @param dbFallback 查询接口
     * @param type 返回类型
     * @return R 返回对应类型
     */
    public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id,Function<ID,R> dbFallback, Class<R> type) {
        return queryWithLogicalExpire(keyPrefix, id, dbFallback,RedisConstants.CACHE_SHOP_TTL,TimeUnit.SECONDS,type);
    }

    /**
     * 缓存穿透解决方案,缓存未命中设置空值(带有逻辑过期的查询)
     * @param keyPrefix key前缀
     * @param id id
     * @param dbFallback 查询接口
     * @param type 返回类型
     * @return R 返回对应类型
     */
    public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id,Function<ID,R> dbFallback, Long time, TimeUnit unit, Class<R> type) {
        //redis中存在
        String key = keyPrefix + id;
        String shopJson = redisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(shopJson)){
            return null;
        }

        //命中判断过期时间
        String redisDataJson = redisTemplate.opsForValue().get(key);
        RedisData<R> redisData = JSONUtil.toBean(redisDataJson, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        //未过期
        if (redisData.getExpireTime().isAfter(LocalDateTime.now())){
            return r;
        }

        //已过期,重建缓存
        //获取互斥锁
        String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        if (isLock){
            //成功,开启独立线程重建缓存
            threadPoolTaskExecutor.submit(()->{
                try {
                    //查询数据库
                    R r1 = dbFallback.apply(id);
                    //写入redis
                    this.setWithLogicalExpire(key, r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unLock(lockKey);
                }
            });
        }

        return r;
    }

    /**
     * 未命中设置空值,解决缓存穿透
     * @param keyPrefix key前缀
     * @param id id
     * @param dbFallback 查询接口
     * @param type 返回类型
     * @return R 返回对应类型
     */
    public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Function<ID,R> dbFallback, Class<R> type){
        return queryWithPassThrough(keyPrefix,id,dbFallback,RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES,type);
    }

    /**
     * 未命中设置空值,解决缓存穿透
     * @param keyPrefix key前缀
     * @param id id
     * @param dbFallback 查询接口
     * @param time 时间
     * @param unit 时间单位
     * @param type 返回类型
     * @return R 返回对应类型
     */
    public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Function<ID,R> dbFallback,Long time, TimeUnit unit,Class<R> type){
        String key = keyPrefix + id;
        //从redis中查询缓存
        String json = redisTemplate.opsForValue().get(key);
        //缓存存在且不为空字符串,返回对象
        if (StrUtil.isNotBlank(json)){
            return JSONUtil.toBean(json,type);
        }

        //判断命中是否是空值
        if (json != null ){
            return null;
        }

        //不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        if (r == null) {
            redisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL,TimeUnit.SECONDS);
            return null;
        }
        this.set(key, r,time,unit);

        return r;
    }

    /**
     * 获取锁
     */
    private boolean tryLock(String key){
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     */
    private void unLock(String key){
        redisTemplate.delete(key);
    }
}

代码中使用工具类

@Override
public Result queryShopById(Long id) {
    //未命中设置空值解决缓存穿透
    //Shop shop = queryWithPassThrough(id);
    //Shop shop = cacheClient.queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, this::getById, Shop.class);

    //互斥锁解决缓存击穿
    //Shop shop = queryWithMutex(id);

    //设置逻辑过期解决缓存击穿
    //Shop shop = queryWithLogicalExpire(id);
    Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, this::getById,Shop.class);
    if (shop == null){
        return Result.fail("店铺不存在");
    }
    return Result.ok(shop);
}
posted @   转身刹那的潇洒  阅读(116)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示