【Redis笔记】缓存穿透、缓存击穿的Java代码大致解决方案

关于缓存穿透、缓存击穿是什么,可以去我的博客专栏 Redis 下查看 【Redis笔记】缓存——缓存分类、更新策略、缓存穿透、缓存雪崩、缓存击穿

为了能够使用Java操作Redis,我们首先需要先给SpringBoot加入Redis的依赖坐标:

		<!--redis依赖及其连接池-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

application.yml 文件中配置数据源信息(也可以是application.properties),此处我使用的是application.yml,其他配置文件格式不太一样。

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/数据库名称?useSSL=false&serverTimezone=UTC # 数据库url
    username: root
    password: 1234
  redis:
    host: 192.168.1.1 # redis服务器地址
    port: 6379 # redis默认端口
    password: 123456 # redis服务器密码
    lettuce:
      pool: # redis连接池信息
        max-active: 10 
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s

缓存穿透

关于缓存穿透的解决方案,之前提到过 缓存空对象布隆过滤两种。那么对于缓存空对象,业务逻辑上如何实现查询和更新呢?

缓存空对象部分代码

在一次查询操作过程中,先去请求Redis,如果查到了数据,也不用管是否为空值(“”),直接返回该数据即可;如果未在Redis中查询到数据,那么就去数据库进行查询,。如果数据库中也未能查到想要查询到的目标,则在Redis中设置该键值为空字符串(“”)。
可能有人有疑问,如果后面数据库中有这个数据了怎么办?那么,由于我们的Redis设置了过期时间,所以当一段时间之后,Reids中的值失效了,那么再次查询时就会去数据库中更新。
那这样就会存在一段时间的间隙,数据库与Redis不一致的情况,不过这也是没办法的问题。为了避免有人恶意或者无意使用不存在信息对数据库查询,从而瘫痪数据库系统,设置空值,能够有效的缓解不断查询数据库不存在的信息对数据库造成的压力。

	// Redis查询数据
    public void set(String key, Object value, Long time, TimeUnit unit) {
        // 业务调用时传入键值,过期时间
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    // 解决缓存穿透
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type,
                                          Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 存在即返回
            return JSONUtil.toBean(json, type);
        }
        // 判断是否为""空值
        if (json != null) {
            return null;
        }
        // 不存在根据id查询数据库
        // 使用Function,由调用方法传入数据库查询逻辑
        R r = dbFallback.apply(id);
        // 不存在返回错误
        if (r == null) {
            // 写入空值,CACHE_NULL_TTL自定义常量,设置空值的过期时间
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 存在,写入redis
        this.set(key, r, time, unit);
        // 返回
        return r;
    }

布隆过滤

待更新…

缓存击穿

逻辑过期时间

如果是采用逻辑过期时间策略来避免缓存击穿,那么就需要在给Redis存入数据的时候,将原始数据和我们设置的逻辑时间一并打包存入。这需要我们新增一个实体对象:

@Data
public class RedisData {
    private LocalDateTime expireTime;
    private Object data;
}

其中 expireTime 这里记录的是我们设置的过期时间,data 存的就是我们想要存入的数据。JSON数据的解析,我使用的是hutool的工具,需要添加依赖:

		<!--hutool工具-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>

逻辑过期时间详细代码:

	public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    // 异步线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type,
                                            Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        // 从redis查询商铺信息缓存
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断是否命中
        if (StrUtil.isBlank(json)) {
            // 未命中直接返回null
            return null;
        }
        // 命中需要先反序列化JSON
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        // 判断是否过期
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 未过期,直接返回
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 过期时间是否在当前之后
            return r;
        }
        // 已过期,开始缓存重建
        // 申请互斥锁
        boolean isLock = tryLock(key);
        // 判断是否获取锁成功
        if (isLock) {
            // 成功,开启线程执行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 重建缓存
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 写入Redis
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(key);
                }
            });
        }
        // 返回店铺过期信息
        return r;
    }

    private boolean tryLock(String key) {
        // 异步线程的时间,这里设置的是10s,大家根据自己业务需要可以更改
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

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

互斥锁

待更新…

Redis工具类

为了方便所有业务进行直接使用,我们最好把这一类的解决方案放在一个工具类中,供所有业务直接调用

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // Redis插入数据
    public void set(String key, Object value, Long time, TimeUnit unit) {
        // 业务调用时传入键值,过期时间
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    // 解决缓存穿透
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type,
                                          Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 从redis查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 存在即返回
            return JSONUtil.toBean(json, type);
        }
        // 判断是否为""空值
        if (json != null) {
            return null;
        }
        // 不存在根据id查询数据库
        // 使用Function,由调用方法传入数据库查询逻辑
        R r = dbFallback.apply(id);
        // 不存在返回错误
        if (r == null) {
            // 写入空值,CACHE_NULL_TTL自定义常量,设置空值的过期时间
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 存在,写入redis
        this.set(key, r, time, unit);
        // 返回
        return r;
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    // 异步线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type,
                                            Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        // 从redis查询商铺信息缓存
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        // 判断是否命中
        if (StrUtil.isBlank(json)) {
            // 未命中直接返回null
            return null;
        }
        // 命中需要先反序列化JSON
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        // 判断是否过期
        JSONObject data = (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 未过期,直接返回
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 过期时间是否在当前之后
            return r;
        }
        // 已过期,开始缓存重建
        // 申请互斥锁
        boolean isLock = tryLock(key);
        // 判断是否获取锁成功
        if (isLock) {
            // 成功,开启线程执行缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 重建缓存
                    // 查询数据库
                    R r1 = dbFallback.apply(id);
                    // 写入Redis
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unlock(key);
                }
            });
        }
        // 返回店铺过期信息
        return r;
    }

    private boolean tryLock(String key) {
        // 异步线程的时间,这里设置的是10s,大家根据自己业务需要可以更改
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}
  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值