CacheClient工具类:

CacheClient工具类:

package com.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 lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

@Component
    @Slf4j
    public class CacheClient {
        //线程池
        private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
        //注入StringRedisTemplate
        @Resource
        StringRedisTemplate stringRedisTemplate;
    //设置Redis内容(Redis TTL过期)
    public void set(String key, Object value, Long expireTime, TimeUnit timeUnit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);
    }
//设置Redis内容(逻辑过期)
public void setWithLogicTime(String key, Object value, Long expireTime, TimeUnit timeUnit) {
    RedisData redisData = new RedisData();
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expireTime)));
    redisData.setData(value);
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}

//缓存查询(解决缓存穿透问题)
public <R, ID> R queryShopWithPassthrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack
        , Long expireTime, TimeUnit timeUnit) {
    String key = keyPrefix + id;
    //获取Redis中数据
    String json = stringRedisTemplate.opsForValue().get(key);
    //是否有值
    if (StrUtil.isNotBlank(json)) {
        //有值则直接返回
        R bean = JSONUtil.toBean(json, type);
        return bean;
    }
    //是否为空字符串
    if (json != null) {
        return null;
    }
    //获取数据库中数据并转化为R类型
    R r = dbFallBack.apply(id);
    String rStr = JSONUtil.toJsonStr(r);
    //数据库和缓存中都不存在,在Redis中存放空字符串
    if (r == null) {
        //存入空字符串,防止缓存穿透
        this.set(key, "", expireTime, timeUnit);
        return null;
    }
    //数据库中存在,缓存不存在,则存入Redis
    this.set(key, rStr, expireTime, timeUnit);
    return r;
}

//缓存查询(使用逻辑过期解决缓存击穿)
public <R, ID> R queryShopWithLogicTime(String Cachekey, String lockkey, ID id, Class<R> type, Function<ID, R> dbToRedis
        , Long logicExpireTime, TimeUnit timeUnit) {
    //Redis的Key
    String key = Cachekey + id;
    //从Redis中获取JSON格式数据
    String redisDataEntries = stringRedisTemplate.opsForValue().get(key);
    //是否有值
    if (StrUtil.isBlank(redisDataEntries)) {
        //为空则直接返回
        return null;
    }
    //将RedisData的JSON转化成bean对象
    RedisData redisData = JSONUtil.toBean(redisDataEntries, RedisData.class);
    //获取redisData中的Data(JSON格式,需要转化成bean)
    R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
    //获取redisData的逻辑过期时间
    LocalDateTime expireTime = redisData.getExpireTime();
    //有值则判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
        //1.未过期则直接返回
        return r;
    }
    //2.已过期
    //2.1.尝试获取锁
    String lockKey = lockkey + id;
    boolean isLock = tryLock(lockKey);
    //2.1.1. 获取锁成功
    if (isLock) {
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //获取数据库并转换成R类型对象
                R r1 = dbToRedis.apply(id);
                //存入Redis
                this.setWithLogicTime(key, r1, logicExpireTime, timeUnit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unLock(lockKey);
            }
        });
    }
    //2.1.2. 获取锁失败
    return r;
}
//尝试获取锁
private boolean tryLock(String key) {
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(lock);
}
//删除锁
private void unLock(String key) {
    stringRedisTemplate.delete(key);
}
    } 

缓存穿透:

缓存穿透是指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力。

解决方法:

1.缓存空值:当客户端请求一个Redis和Mysql中都不存在的数据时,在Redis中存入一个空值,其Key为请求的参数。

因为此类为工具类,所以返回值不能确定,因此要设定一个泛型对象。
首先获取Redis中的数据,如果存在数据(不为空),则直接将数据转化成R类型的对象并且返回。
如果获取的数据为一个空字符串,则证明发生了缓存穿透,返回null。
如果不是以上两种(Redis中不存在该Key),则先从数据库查询并且转化为R类型的对象,如果该对象为空,则证明数据库和缓存中都不存在,在在Redis中存放空字符串。
如果不为空,则证明数据库中存在,缓存不存在。在Redis中存入该对象的JSON格式并且返回即可。

缓存空值

public <R, ID> R queryShopWithPassthrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallBack
            , Long expireTime, TimeUnit timeUnit) {
        String key = keyPrefix + id;
        //获取Redis中数据
        String json = stringRedisTemplate.opsForValue().get(key);
        //是否有值
        if (StrUtil.isNotBlank(json)) {
            //有值则直接返回
            R bean = JSONUtil.toBean(json, type);
            return bean;
        }
        //是否为空字符串
        if (json != null) {
            return null;
        }
        //获取数据库中数据并转化为R类型
        R r = dbFallBack.apply(id);
        String rStr = JSONUtil.toJsonStr(r);
        //数据库和缓存中都不存在,在Redis中存放空字符串
        if (r == null) {
            //存入空字符串,防止缓存穿透
            this.set(key, "", expireTime, timeUnit);
            return null;
        }
        //数据库中存在,缓存不存在,则存入Redis
        this.set(key, rStr, expireTime, timeUnit);
        return r;
    }

2.使用布隆过滤器

缓存击穿:

缓存击穿是指某一个或少数几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。

解决方法:

1.加互斥锁(不讨论缓存穿透的情况):

为了防止大量请求直接到达数据库,使用Redis中的互斥锁。如果获取互斥锁成功,则将数据存入Redis并且解锁,反之则证明已经有线程获取到了锁,其他线程只需要重试此方法。需要注意的是,使用setnx语句存储数据时,需要设定一个超时TTL,防止因为某些原因无法正常执行unlock语句。 (以下方法非工具类中的方法)

互斥锁

//使用互斥锁解决缓存击穿
private Result queryShopWithMutex(Long id) {
    String shopEntries = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
    Shop shop;
    //是否有值
    if (StrUtil.isNotBlank(shopEntries)) {
        shop = JSONUtil.toBean(shopEntries, Shop.class);
        return Result.ok(shop);
    }
    //添加互斥锁
    //1.获取锁
    String lockKey = RedisConstants.LOCK_SHOP_KEY + "" + id;
    try {
        boolean tryLock = tryLock(lockKey);
        //2.判断是否获取成功
        //2.1.失败则休眠或重试
        if (!tryLock){
            Thread.sleep(50);
            queryShopWithMutex(id);
        }
        //2.2.成功则获取数据库数据
        //数据库中存在,缓存不存在
        shop = getById(id);
        String shopStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, shopStr, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    finally {
        unLock(lockKey);
    }
    return Result.ok(shop);
}
private boolean tryLock(String key){
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(lock);
}

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

2.加逻辑过期时间:

逻辑过期指在Redis存数据时,将过期时间一同存入。如果使用了逻辑过期,需要线将数据在Redis中预热。
需要注意的是,使用逻辑过期时,如果一个数据的逻辑时间过期了,所有线程在更新线程重新写入数据前获取到的都是旧数据,当更新线程执行完后获取到新数据。
首先根据请求获取Redis中的数据redisDataEntries,因为数据已预热过,所以如果不存在的话直接返回空值。
存在的话分已经过期和未过期
未过期: get到逻辑过期时间,并且和LocalDateTime.now()进行比较,如果未过期的话说明此时redisDataEntries仍可继续使用,将redisDataEntries中的data转化为R类型的对象后返回即可。
已过期:如果逻辑时间和LocalDateTime.now()过期了,则需要尝试获取锁,如果获取锁成功,则需要从线程池中选取一个“更新线程”执行setWithLogicTime()方法。
如果获取锁失败的线程直接返回旧数据即可。

逻辑过期

//缓存查询(使用逻辑过期解决缓存击穿)
public <R, ID> R queryShopWithLogicTime(String Cachekey, String lockkey, ID id, Class<R> type, Function<ID, R> dbToRedis
        , Long logicExpireTime, TimeUnit timeUnit) {
    //Redis的Key
    String key = Cachekey + id;
    //从Redis中获取JSON格式数据
    String redisDataEntries = stringRedisTemplate.opsForValue().get(key);
    //是否有值
    if (StrUtil.isBlank(redisDataEntries)) {
        //为空则直接返回
        return null;
    }
    //将RedisData的JSON转化成bean对象
    RedisData redisData = JSONUtil.toBean(redisDataEntries, RedisData.class);
    //获取redisData中的Data(JSON格式,需要转化成bean)
    R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
    //获取redisData的逻辑过期时间
    LocalDateTime expireTime = redisData.getExpireTime();
    //有值则判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())) {
        //1.未过期则直接返回
        return r;
    }
    //2.已过期
    //2.1.尝试获取锁
    String lockKey = lockkey + id;
    boolean isLock = tryLock(lockKey);
    //2.1.1. 获取锁成功
    if (isLock) {
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //获取数据库并转换成R类型对象
                R r1 = dbToRedis.apply(id);
                //存入Redis
                this.setWithLogicTime(key, r1, logicExpireTime, timeUnit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unLock(lockKey);
            }
        });
    }
    //2.1.2. 获取锁失败
    return r;
}
//尝试获取锁
private boolean tryLock(String key) {
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(lock);
}
//删除锁
private void unLock(String key) {
    stringRedisTemplate.delete(key);
}

RedisData类:


package com.hmdp.utils;

import lombok.Data;

import java.time.LocalDateTime;

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

最后修改:2024 年 05 月 04 日
如果觉得我的文章对你有用,请随意赞赏