【Redis笔记】缓存穿透与缓存击穿以及应对方法

一、缓存穿透

1. 缓存穿透概念

缓存穿透指查询不存在的数据时,不会命中缓存(缓存层),便会向数据库(持久层)发送查询请求,但同样查不到数据。

会给数据库性能带来压力

2. 缓存穿透解决方法

持久层查询到不存在的空数据时,也将其添加至缓存,并设置过期时间

对空数据进行缓存,可以避免重复查找空数据给数据库带来过多压力

访问无效数据的请求,一般只会在短期内重复访问(误操作或恶意访问),可以设置较短的过期时间,如 5 分钟,便于节省空间

示例代码

场景:根据用户 id 查看某用户的信息

业务层 UserServiceImpl 示例如下:

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
    

    @Resource
    private StringRedisTemplate stringRedisTemplate;

	@Override
    public User getByIdTreatPenetration(Long id) {
    
    
        // 在缓存中查找用户
        String key = "cache:user:" + id;
        String userStr = stringRedisTemplate.opsForValue().get(key);
        // 判断缓存是否命中,命中则直接返回
        if (userStr != null) {
    
    
            if (userStr.equals(""))
                return null;
            else
                return JSONUtil.toBean(userStr, User.class);
        }
        // 缓存未命中,从数据库查找
        User user = this.getById(id);
        // 将数据存入缓存
        stringRedisTemplate.opsForValue().set(
                key
                , (user == null) ? "" : JSONUtil.toJsonStr(user)	// 空数据缓存空字符串
                , (user == null) ? 5 : 60	// 空数据缓存 5 分钟,非空缓存 60 分钟
                , TimeUnit.MINUTES
        );
        return user;
    }
}

二、缓存击穿

1. 缓存击穿概念

缓存击穿是指,当某个热点数据的缓存过期后,服务器同时收到大量查询此数据的请求,此时新的缓存尚未建立,就会向数据库发送大量查询请求

2. 缓存击穿解决方法

方法一:互斥锁

缓存未命中时获取锁,建立新的缓存,其它线程等待锁释放后再查找数据

如此可以达到 同一时刻对同一数据只有一个请求在访问数据库


【互斥锁特点】

  • 缓存正在被建立时,其它请求会等待建立完成,速度可能较慢
  • 用户查到的数据严格与数据库一致

示例代码

同样根据用户 id 查询某用户的信息

锁的操作使用了 Redisson,在处理缓存穿透的基础上,为未命中缓存的部分补充锁的部分即可

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
    

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public User getByIdTreatBreakdownWithLock(Long id) {
    
    
        // 在缓存中查找用户
        String key = "user:" + id;
        String userStr = stringRedisTemplate.opsForValue().get(key);
        // 判断缓存是否命中,命中则直接返回
        if (userStr != null) {
    
    
            if (userStr.equals(""))
                return null;
            else
                return JSONUtil.toBean(userStr, User.class);
        }

        // 缓存未命中,获取锁,查询数据库并建立缓存
        // 获取锁 Redisson 对象
        String lockName = "lock:user" + id;
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://192.168.100.103:6379")
                .setPassword("123456")
                .setDatabase(0);
        RLock lock = Redisson.create(config).getLock(lockName);
        try {
    
    
            // 尝试获取锁
            boolean flag = lock.tryLock();
            if (!flag) {
    
    
                // 获取锁失败,锁已被占用,线程睡眠 50 毫秒,然后递归调用此方法查询数据
                Thread.sleep(50);
                return this.getByIdTreatBreakdownWithLock(id);
            }
            // 从数据库查询
            User user = this.getById(id);
            // 将数据存入缓存
            stringRedisTemplate.opsForValue().set(
                    key
                    , (user == null) ? "" : JSONUtil.toJsonStr(user)	// 空数据缓存空字符串
                    , (user == null) ? 5 : 60	// 空数据缓存 5 分钟,非空缓存 60 分钟
                    , TimeUnit.MINUTES
            );
            return user;
        } catch (Exception e) {
    
    
            throw new RuntimeException(e.getMessage());
        } finally {
    
    
            // 释放锁
            lock.unlock();
        }
    }
    
}

方法二:设置逻辑过期时间

前提:数据被建立时就加入缓存,并设置逻辑过期时间(实际缓存过期时间为 -1)

缓存未命中时,即说明数据库中不存在此数据,返回空
缓存命中时,查看逻辑过期时间是否过期,若已过期,返回已过期的数据,并在新线程中重新建立缓存并设置逻辑过期时间


【逻辑过期特点】

  • 数据被建立时就必须加入缓存
  • 异步建立缓存,不影响其它请求,速度较快
  • 由于逻辑过期依然返回已过期的数据,用户查到的数据可能与数据库一致

示例代码

根据用户 id 查询某用户的信息

由于要设置逻辑过期时间,可以将实际数据与过期时间包装成类,示例如下:

@Data
public RedisData getRedisData(String key) {
    
    
    String jsonStr = get(key);
    if (jsonStr == null) {
    
    
        return null;
    }
    return JSONUtil.toBean(jsonStr, RedisData.class);
}

业务层示例如下:

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    
    

	// 线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    @Override
    public User getByIdTreatBreakdownWithLogicExpire(Long id) {
    
    
        String key = "user:" + id;
        // 查询缓存
        String jsonStr = stringRedisTemplate.opsForValue().get(key);
        // 缓存未命中,即代表数据不存在,返回空
		if (redisData == null) {
    
    
            return null;
        }
        // 将缓存字符串装换为对象
        RedisData redisData = JSONUtil.toBean(jsonStr, RedisData.class);
        JSONObject jsonObject = (JSONObject) redisData.getData();
        User user = jsonObject.toBean(User.class);
        LocalDateTime expirationTime = redisData.getExpirationTime();
        // 未到期,直接返回
        if (LocalDateTime.now().isBefore(expirationTime)) {
    
    
            return user;
        }
        
        // 已到期,重建缓存
        String lockName = "lock:user:" + id;
        RLock lock = redissonClient.getLock(lockName);
        boolean flag = lock.tryLock();
        if (flag) {
    
    
	        // 获取锁后进行二次判断,现有缓存是否过期
            jsonStr = stringRedisTemplate.opsForValue().get(key);
            redisData = JSONUtil.toBean(jsonStr, StringRedisCacheUtil.RedisData.class);
            expirationTime = redisData.getExpirationTime();
            // 第二次判断未到期,说明缓存已被别的线程重建,返回结果
            if (LocalDateTime.now().isBefore(expirationTime)) {
    
    
                return user;
            }
			// 新线程中重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
    
    
                User user1 = this.getById(id);
                StringRedisCacheUtil.RedisData redisData1 = new StringRedisCacheUtil.RedisData();
                redisData1.setData(user1);
                redisData1.setExpirationTime(LocalDateTime.now().plusMinutes(60));
                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData1));
                lock.unlock();
            });

        }
        return user;

    }
}

三、工具类示例

@Component
public class StringRedisCacheUtil {
    
    

    private static StringRedisTemplate stringRedisTemplate;
    private static RedissonClient redissonClient;

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

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

    private static Long NULL_CACHE_TTL = 5L;
    private static TimeUnit NULL_CACHE_TIMEUNIT = TimeUnit.MINUTES;

    // Spring 容器启动时,为静态变量注入对象
    // ----------------------------------------------------
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedissonClient client;
    @PostConstruct
    private void init() {
    
    
        stringRedisTemplate = this.redisTemplate;
        redissonClient = client;
    }
    private StringRedisCacheUtil() {
    
     }
    // ----------------------------------------------------

    // 根据键获取值
    public static String get(String key) {
    
    
        return stringRedisTemplate.opsForValue().get(key);
    }

    // 根据键获取带逻辑时间的值
    public static RedisData getRedisData(String key) {
    
    
        String jsonStr = get(key);
        if (jsonStr == null) {
    
    
            return null;
        }
        return JSONUtil.toBean(jsonStr, RedisData.class);
    }

    // 判断是否超过逻辑过期时间
    public static boolean isExpired(RedisData redisData) {
    
    
        return redisData == null || LocalDateTime.now().isAfter(redisData.getExpirationTime());
    }


    // 判断是否还没有逻辑过期时间
    public static boolean isNotExpired(RedisData redisData) {
    
    
        return !isExpired(redisData);
    }


    // 设置缓存
    public static void set(String key, String value, Long timeout, TimeUnit unit) {
    
    
        stringRedisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    // 为空数据设置缓存
    public static void setNull(String key) {
    
    
        set(key, "", NULL_CACHE_TTL, NULL_CACHE_TIMEUNIT);
    }

    // 设置缓存并带有逻辑过期时间
    public static void setWithLogicalExpire(String key, Object value, Long timeout, TimeUnit unit) {
    
    
        RedisData redisData = new RedisData();
        redisData.setData(value);
        LocalDateTime now = LocalDateTime.now();
        redisData.setExpirationTime(now.plusMinutes(unit.toSeconds(timeout)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 根据 id 查询数据,并解决缓存穿透问题(不解决击穿问题)
     *
     * @param keyPrefix      键前缀
     * @param id             主键 id
     * @param resultType     返回类型的 Class
     * @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数
     * @param timeOut        过期时间
     * @param unit           时间单位
     * @param <R>            返回类型
     * @param <ID>           主键 id 类型
     * @return 根据 id 查询到的数据或 null
     */
    public static <R, ID> R getByIdTreatPenetration(String keyPrefix, ID id, Class<R> resultType,
                                                    Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {
    
    
        String key = keyPrefix + id.toString();
        String JSONStr = get(key);
        if (JSONStr != null) {
    
    
            if (JSONStr.equals("")) return null;
            return JSONUtil.toBean(JSONStr, resultType);
        }
        R result = dbCallbackFunc.apply(id);
        if (result == null) setNull(key);
        else set(key, JSONUtil.toJsonStr(result), timeOut, unit);
        return result;
    }

    /**
     * 根据 id 查询数据,利用互斥锁解决缓存击穿问题
     *
     * @param keyPrefix      键前缀
     * @param lockNamePrefix 锁名称前缀
     * @param id             主键 id
     * @param resultType     返回类型的 Class
     * @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数
     * @param timeOut        过期寿命
     * @param unit           时间单位
     * @param <R>            返回类型
     * @param <ID>           主键 id 类型
     * @return 根据 id 查询到的数据或 null
     */
    public static <R, ID> R getByIdTreatBreakdownWithLock(String keyPrefix, String lockNamePrefix, ID id, Class<R> resultType,
                                                          Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {
    
    
        String key = keyPrefix + id.toString();
        String JSONStr = get(key);
        if (JSONStr != null) {
    
    
            if (JSONStr.equals("")) return null;
            return JSONUtil.toBean(JSONStr, resultType);
        }

        String lockName = lockNamePrefix + id;
        RLock lock = redissonClient.getLock(lockName);
        try {
    
    
            boolean flag = lock.tryLock();
            if (!flag) {
    
    
                Thread.sleep(50);
                return getByIdTreatBreakdownWithLock(keyPrefix, lockNamePrefix, id, resultType, dbCallbackFunc, timeOut, unit);
            }
            R result = dbCallbackFunc.apply(id);
            if (result == null) setNull(key);
            else set(key, JSONUtil.toJsonStr(result), timeOut, unit);
            return result;
        } catch (Exception e) {
    
    
            throw new RuntimeException(e.getMessage());
        } finally {
    
    
            // 释放锁
            lock.unlock();
        }
    }

    /**
     * 根据 id 查询数据,利用逻辑过期解决缓存击穿问题(缓存未命中则代表不存在数据,不考虑穿透问题)
     *
     * @param keyPrefix      键前缀
     * @param lockNamePrefix 锁名称前缀
     * @param id             主键 id
     * @param resultType     返回类型的 Class
     * @param dbCallbackFunc 缓存未命中时,查询数据库所用的回调函数
     * @param timeOut        逻辑过期寿命
     * @param unit           时间单位
     * @param <R>            返回类型
     * @param <ID>           主键 id 类型
     * @return 根据 id 查询到的数据
     */
    public static <R, ID> R getByIdTreatBreakdownWithLogicExpire(String keyPrefix, String lockNamePrefix, ID id, Class<R> resultType,
                                                                 Function<ID, R> dbCallbackFunc, Long timeOut, TimeUnit unit) {
    
    
        String key = keyPrefix + id;
        RedisData redisData = getRedisData(key);

        if (redisData == null) {
    
    
            return null;
        }

        JSONObject jsonObject = (JSONObject) redisData.getData();
        R result = JSONUtil.toBean(jsonObject, resultType);

        if (isNotExpired(redisData)) {
    
    
            return result;
        }

        String lockName = lockNamePrefix + id.toString();
        RLock lock = redissonClient.getLock(lockName);

        boolean flag = lock.tryLock();
        if (flag && isExpired(getRedisData(key))) {
    
    
            CACHE_REBUILD_EXECUTOR.submit(() -> {
    
    
                R data = dbCallbackFunc.apply(id);
                setWithLogicalExpire(key, data, timeOut, unit);
                lock.unlock();
            });
        }
        return result;
    }

}

猜你喜欢

转载自blog.csdn.net/Cey_Tao/article/details/127454635