首页 > 数据库 >Redis工具类(解决缓存穿透、缓存击穿)

Redis工具类(解决缓存穿透、缓存击穿)

时间:2024-10-26 19:17:14浏览次数:5  
标签:缓存 return log Redis 击穿 field key id

文章目录

前言

该工具类包含以下功能:

1.将任意对象存储在 hash 类型的 key 中,并可以设置 TTL

2.将任意对象存储在 hash 类型的 key 中,并且可以设置逻辑过期时间

3.将空对象存入 hash 类型的 key 中,并且可以设置超时时间

4.缓存空对象解决缓存穿透

5.布隆过滤器解决缓存穿透

6.布隆过滤器+缓存空对象解决缓存穿透

7.互斥锁解决缓存击穿

8.逻辑过期解决缓存击穿

以下是关键代码

IBloomFilter

IBloomFilter 是自定义的布隆过滤器接口。这里使用接口的原因在于,每个业务使用的布隆过滤器可能是不一样的,因此为了让工具类能兼容所有的布隆过滤器,这里添加接口,并使用泛型表示布隆过滤器内部存储数据的类型

public interface IBloomFilter<T> {

    // 添加
    void add(T id);

    // 判断是否存在
    boolean mightContain(T id);

}

ObjectMapUtils

ObjectMapUtils 是对象与 Map 类型的相互转换,可以让对象转换为 Map 集合,也可以让 Map 集合转回对象

public class ObjectMapUtils {

    // 将对象转为 Map
    public static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {
        Map<String, String> result = new HashMap<>();
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 如果为 static 且 final 则跳过
            if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
                continue;
            }
            field.setAccessible(true); // 设置为可访问私有字段
            Object fieldValue = field.get(obj);
            if (fieldValue != null) {
                result.put(field.getName(), field.get(obj).toString());
            }
        }
        return result;
    }

    // 将 Map 转为对象
    public static<R> R map2Obj(Map<Object, Object> map, Class<R> clazz) throws Exception {
        R obj = clazz.getDeclaredConstructor().newInstance();
        for (Map.Entry<Object, Object> entry : map.entrySet()) {
            Object fieldName = entry.getKey();
            Object fieldValue = entry.getValue();
            Field field = clazz.getDeclaredField(fieldName.toString());
            field.setAccessible(true); // 设置为可访问私有字段
            String fieldValueStr = fieldValue.toString();
            // 根据字段类型进行转换
            fillField(obj, field, fieldValueStr);

        }
        return obj;
    }

    // 将 Map 转为对象(含排除字段)
    public static<R> R map2Obj(Map<Object, Object> map, Class<R> clazz, String... excludeFields) throws Exception {
        R obj = clazz.getDeclaredConstructor().newInstance();
        for (Map.Entry<Object, Object> entry : map.entrySet()) {
            Object fieldName = entry.getKey();
            if(Arrays.asList(excludeFields).contains(fieldName)) {
                continue;
            }
            Object fieldValue = entry.getValue();
            Field field = clazz.getDeclaredField(fieldName.toString());
            field.setAccessible(true); // 设置为可访问私有字段
            String fieldValueStr = fieldValue.toString();
            // 根据字段类型进行转换
            fillField(obj, field, fieldValueStr);
        }
        return obj;
    }

    // 填充字段
    private static void fillField(Object obj, Field field, String value) throws IllegalAccessException {
        if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
            field.set(obj, Integer.parseInt(value));
        } else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
            field.set(obj, Boolean.parseBoolean(value));
        } else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
            field.set(obj, Double.parseDouble(value));
        } else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
            field.set(obj, Long.parseLong(value));
        } else if (field.getType().equals(String.class)) {
            field.set(obj, value);
        } else if(field.getType().equals(LocalDateTime.class)) {
            field.set(obj, LocalDateTime.parse(value));
        }
        // 如果有需要可以继续添加...
    }

}

CacheClient

CacheClient 就是缓存工具类,包含了之前提到的所有功能

@Component
@Slf4j
public class CacheClient {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 重建缓存线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    // 将任意对象存储在 hash 类型的 key 中,并可以设置 TTL
    public void setByHash(String key, Object value, Long time, TimeUnit unit) throws IllegalAccessException {
        redisTemplate.opsForHash().putAll(key, ObjectMapUtils.obj2Map(value));
        redisTemplate.expire(key, time, unit);
    }

    // 将任意对象存储在 hash 类型的 key 中,并且可以设置逻辑过期时间
    public void setWithLogicalExpireByHash(String key, Object value, Long time, TimeUnit unit) throws IllegalAccessException {
        Map<String, String> map = ObjectMapUtils.obj2Map(value);
        // 添加逻辑过期时间
        map.put(RedisConstants.EXPIRE_KEY, LocalDateTime.now().plusSeconds(unit.toSeconds(time)).toString());
        redisTemplate.opsForHash().putAll(key, map);
    }

    // 将空对象存入 hash 类型的 key 中,并且可以设置超时时间
    public void setNullByHash(String key, Long time, TimeUnit unit) {
        redisTemplate.opsForHash().put(key, "", "");
        redisTemplate.expire(key, time, unit);
    }

    // 尝试加锁
    private boolean tryLock(String key, Long time, TimeUnit unit) {
        Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(key,"1", time, unit);
        return Boolean.TRUE.equals(isLocked);
    }

    // 解锁
    private void unlock(String key) {
        redisTemplate.delete(key);
    }

    // 缓存空对象解决缓存穿透
    public<R, ID> R queryWithCacheNull(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
                                        Long time, TimeUnit unit) {
        // 从 redis 查询
        String key = keyPrefix + id;
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        // 缓存命中
        if (!entries.isEmpty()) {
            try {
                // 如果是空对象,表示一定不存在数据库中,直接返回(解决缓存穿透)
                if (entries.containsKey("")) {
                    log.info("redis查询到id={}的空对象,", id);
                    return null;
                }
                // 刷新有效期
                redisTemplate.expire(key, time, unit);
                R r = ObjectMapUtils.map2Obj(entries, clazz);
                log.info("缓存命中,结果为:{}", r);
                return r;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        // 查询数据库
        R r = dbFallback.apply(id);
        if (r == null) {
            log.info("id={}不存在于数据库,存入redis", id);
            // 存入空值
            setNullByHash(key, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 不存在,直接返回
            return null;
        }
        // 存在,写入 redis
        try {
            setByHash(key, r, time, unit);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        log.info("查询数据库,获取结果:{}", r);
        return r;
    }

    // 布隆过滤器解决缓存穿透
    public<R, ID> R queryWithBloom(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
                                   Long time, TimeUnit unit, IBloomFilter<ID> bloomFilter) {
        // 如果不在布隆过滤器中,直接返回
        if (!bloomFilter.mightContain(id)) {
            log.info("id={}不存在于布隆过滤器", id);
            return null;
        }
        // 从 redis 查询
        String key = keyPrefix + id;
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        // 缓存命中
        if (!entries.isEmpty()) {
            try {
                // 刷新有效期
                redisTemplate.expire(key, time, unit);
                R r = ObjectMapUtils.map2Obj(entries, clazz);
                log.info("缓存命中,结果为:{}", r);
                return r;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        // 查询数据库
        R r = dbFallback.apply(id);
        if (r == null) {
            log.info("id={}不存在于数据库", id);
            // 不存在,直接返回
            return null;
        }
        // 存在,写入 redis
        try {
            setByHash(key, r, time, unit);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        log.info("查询数据库,获取结果:{}", r);
        return r;
    }

    // 布隆过滤器+缓存空对象解决缓存穿透
    public<R, ID> R queryWithBloomAndCacheNull(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
                                  Long time, TimeUnit unit, IBloomFilter<ID> bloomFilter) {
        // 如果不在布隆过滤器中,直接返回
        if (!bloomFilter.mightContain(id)) {
            log.info("id={}不存在于布隆过滤器", id);
            return null;
        }
        // 从 redis 查询
        String key = keyPrefix + id;
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        // 缓存命中
        if (!entries.isEmpty()) {
            try {
                // 如果是空对象,表示一定不存在数据库中,直接返回(解决缓存穿透)
                if (entries.containsKey("")) {
                    log.info("redis查询到id={}的空对象,", id);
                    return null;
                }
                // 刷新有效期
                redisTemplate.expire(key, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
                R r = ObjectMapUtils.map2Obj(entries, clazz);
                log.info("缓存命中,结果为:{}", r);
                return r;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        // 查询数据库
        R r = dbFallback.apply(id);
        if (r == null) {
            log.info("id={}不存在于数据库,存入redis", id);
            // 存入空值
            setNullByHash(key, RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 不存在,直接返回
            return null;
        }
        // 存在,写入 redis
        try {
            setByHash(key, r, time, unit);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        log.info("查询数据库,获取结果:{}", r);
        return r;
    }

    // 互斥锁解决缓存击穿
    public<R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
                                   Long cacheTime, TimeUnit cacheUnit, String lockKeyPrefix,
                                   Long lockTime, TimeUnit lockUnit) {
        String key = keyPrefix + id;
        String lockKey = lockKeyPrefix + id;
        boolean flag = false;
        int cnt = 10; // 重试次数
        try {
            do {
                // 从 redis 查询
                Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
                // 缓存命中
                if (!entries.isEmpty()) {
                    try {
                        // 刷新有效期
                        redisTemplate.expire(key, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
                        R r = ObjectMapUtils.map2Obj(entries, clazz);
                        log.info("缓存命中,结果为:{}", r);
                        return r;
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
                // 缓存未命中,尝试获取互斥锁
                log.info("缓存未命中,尝试获取互斥锁 id={}", id);
                flag = tryLock(lockKey, lockTime, lockUnit);
                if (flag) { // 获取成功,进行下一步
                    log.info("成功获取互斥锁 id={}", id);
                    break;
                }
                // 获取失败,睡眠后重试
                Thread.sleep(50);
            } while ((--cnt) != 0); // 未获取到锁,休眠后重试
            if(!flag) { // 重试次数到达上限
                log.info("重试次数达到上限 id={}", id);
                return null;
            }
            // 查询数据库
            R r = dbFallback.apply(id);
            if (r == null) {
                log.info("id={}不存在于数据库", id);
                // 不存在,直接返回
                return null;
            }
            // 存在,写入 redis
            try {
                setByHash(key, r, cacheTime, cacheUnit);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
            log.info("查询数据库,获取结果:{}", r);
            return r;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            if (flag) { // 获取了锁需要释放
                log.info("解锁id={}", id);
                unlock(lockKey);
            }
        }
    }

    // 逻辑过期解决缓存击穿
    public<R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> clazz, Function<ID, R> dbFallback,
                                           Long expireTime, TimeUnit expireUnit,
                                           String lockKeyPrefix, Long lockTime, TimeUnit lockUnit) {
        String key = keyPrefix + id;
        String lockKey = lockKeyPrefix + id;
        // 从 redis 查询
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        // 缓存未命中,返回空
        if(entries.isEmpty()) {
            log.info("缓存未命中,返回空 id={}", id);
            return null;
        }
        try {
            R r = ObjectMapUtils.map2Obj(entries, clazz, RedisConstants.EXPIRE_KEY);
            LocalDateTime expire = LocalDateTime.parse(entries.get(RedisConstants.EXPIRE_KEY).toString());
            // 判断缓存是否过期
            if(expire.isAfter(LocalDateTime.now())) {
                // 未过期则直接返回
                log.info("缓存未过期,结果为;{}", r);
                return r;
            }
            // 过期需要先尝试获取互斥锁
            log.info("尝试获取互斥锁 id={}", id);
            if(tryLock(lockKey, lockTime, lockUnit)) {
                log.info("获得到互斥锁 id={}", id);
                // 获取成功
                // 双重检验
                entries = redisTemplate.opsForHash().entries(key);
                r = ObjectMapUtils.map2Obj(entries, clazz, RedisConstants.EXPIRE_KEY);
                expire = LocalDateTime.parse(entries.get(RedisConstants.EXPIRE_KEY).toString());
                if(expire.isAfter(LocalDateTime.now())) {
                    // 未过期则直接返回
                    log.info("缓存未过期,结果为;{}", r);
                    log.info("解锁 id={}", id);
                    unlock(lockKey);
                    return r;
                }
                // 通过线程池完成重建缓存任务
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    try {
                        log.info("进行重建缓存任务 id={}", id);
                        setWithLogicalExpireByHash(key, dbFallback.apply(id), expireTime, expireUnit);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        log.info("解锁 id={}", id);
                        unlock(lockKey);
                    }
                });
            }
            log.info("返回结果:{}", r);
            return r;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

}

使用示例

具体业务的布隆过滤器

public class ShopBloomFilter implements IBloomFilter<Long> {

    private BloomFilter<Long> bloomFilter;

    public ShopBloomFilter(ShopMapper shopMapper) {
        // 初始化布隆过滤器,设计预计元素数量为100_0000L,误差率为1%
        bloomFilter = BloomFilter.create(Funnels.longFunnel(), 100_0000, 0.01);
        // 将数据库中已有的店铺 id 加入布隆过滤器
        List<Shop> shops = shopMapper.selectList(null);
        for (Shop shop : shops) {
            bloomFilter.put(shop.getId());
        }
    }

    public void add(Long id) {
        bloomFilter.put(id);
    }

    public boolean mightContain(Long id){
        return bloomFilter.mightContain(id);
    }

}

控制层

/**
 * 根据id查询商铺信息
 * @param id 商铺id
 * @return 商铺详情数据
 */
@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    return shopService.queryShopById(id);
}

服务层

@Override
public Result queryShopById(Long id) {
    // 缓存空对象解决缓存穿透
    /*Shop shop = cacheClient.queryWithCacheNull(RedisConstants.CACHE_SHOP_KEY,
            id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);*/

    // 布隆过滤器解决缓存穿透
    /*Shop shop = cacheClient.queryWithBloom(RedisConstants.CACHE_SHOP_KEY,
            id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES, shopBloomFilter);*/

    // 布隆过滤器+缓存空对象解决缓存穿透
    /*Shop shop = cacheClient.queryWithBloomAndCacheNull(RedisConstants.CACHE_SHOP_KEY,
            id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES, shopBloomFilter);*/

    // 互斥锁解决缓存击穿
    /*Shop shop = cacheClient.queryWithMutex(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById,
            RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES,
            RedisConstants.LOCK_SHOP_KEY, RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);*/

    // 逻辑过期解决缓存击穿
    Shop shop = cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById,
            RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES,
            RedisConstants.LOCK_SHOP_KEY, RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);

    if(shop == null) {
        return Result.fail("商铺不存在");
    }
    return Result.ok(shop);
}

标签:缓存,return,log,Redis,击穿,field,key,id
From: https://blog.csdn.net/Vendetta_A_A/article/details/143258243

相关文章

  • Redis篇
    Redis篇redis使用场景-缓存-缓存穿透解决方案一:缓存空数据解决方案二:布隆过滤器布隆过滤器的介绍面试问答redis使用场景-缓存-缓存击穿两种解决方案:1)互斥锁2)逻辑过期两种解决方案:互斥锁/逻辑过期上述两者对比:对于容错率低的场景,比如银行等采用互斥锁,其满......
  • (已解决!!!非常详细)无法连接redis服务器
    问题:写springboot项目连接redis失败,报错如下:也可能有其他报错,反正就是连接不上发现能连接上虚拟机,但是连接不上redis上网寻求解决方法,发现一些文章比较乱不是很容易理解,所以总结了一下网上的方法成功解决前提:已经在vmware安装好centos,并且已经安装了redis且能运行,使用p......
  • 简单说说 redis主从同步原理
    主从同步分为以下几个情况1从节点和主节点建立连接时进行全量同步2从节点和主节点正常运行时同步3从节点和主节点断开连接后重新连接进行全量或者增量同步从节点和主节点建立连接时同步1从节点向主节点发生psyncrepIdoffsetId,其中repId是主节点标识,offsetId代表偏移......
  • Redis & 事务 & 总结
    前言 相关系列《Redis&目录》(持续更新)《Redis&事务&源码》(学习过程/多有漏误/仅作参考/不再更新)《Redis&事务&总结》(学习总结/最新最准/持续更新)《Redis&事务&问题》(学习解答/持续更新)  参考文献《Redis事务详解》  概述    Redis事务并......
  • Qt编程技巧小知识点(5)GPIB缓存区数据读取(升级版)
    文章目录Qt编程技巧小知识点(5)GPIB缓存区数据读取(升级版)小结Qt编程技巧小知识点(5)GPIB缓存区数据读取(升级版)  大端小端的问题,GPIB返回的数据经常是小端数据,而我们转化需要大端数据,看代码,Qt的这个函数很好用哦!代码输入//添加库文件#include<QtDebug>#include<Q......
  • Redis5.0.10集群搭建
    参考文档https://www.cnblogs.com/hmwh/p/10289138.htmlhttps://www.cnblogs.com/zgqbky/p/11792141.html以下操作均需在每台服务器上执行安装依赖关系yuminstallmakezlibopenssl*ImageMagick-develgcc*rubygems-y2、创建节点目录mkdir-p/opt/app/redis-cluste......
  • Redis4.0.12集群搭建
    服务器:节点1:10.10.175.55 端口:6379/7379节点2:10.10.175.56 端口:6379/7379节点3:10.10.175.57 端口:6379/7379以下操作均需在每台服务器上执行安装依赖关系yuminstallmakezlibopenssl*ImageMagick-develgcc*rubygems-y2、创建节点目录mkdir-p/usr/local/redis-cl......
  • Redis的基础命令
    一、数据库操作命令1.redis中库的说明redis中的库默认存在16个库,分别按照0-15来排列选择库的命令:select0-15例如:select1就是选择一号库的意思2.清空表的命令1.清除当前表:flushdb2.清除所有表:flushall3.redis中客户端显示中文./redis-cli-p7000--raw二、操作key相......
  • Redis的详细安装教程和环境变量配置(附有详细步骤讲解及相关操作截图和代码)
    NoSQL简介NoSQL数据库是一种非关系型数据库,它在处理大规模、高并发的动态网站数据时具有明显优势。NoSQL数据库的出现是为了解决传统关系数据库在处理大数据量和高并发请求时遇到的性能瓶颈。NoSQL数据库的设计允许它们在分布式环境中更有效地扩展,同时提供灵活的数据模型来适应不......
  • redis数据库操作指令
    一、数据库操作指令2、redis中库说明对于一个redis服务而言,包含默认有16个数据库给我们使用,从0开始编号,共15号数据库,默认使用的是0号数据库切换库,select库号举例:使用1号库:select1库和库之间数据不共享库和库之间的键可以重名2、redis中清空库的指令清空当前库flush......