Redis(四):缓存击穿及其解决方案(SpringBoot+mybatis-plus)

一、概念

复制代码
 * 缓存击穿
 * 定义:指一个热点key在缓存过期的瞬间,同时有大量请求访问这个key,导致所有请求都直接打到数据库上,造成数据库压力激增
 * 解决方案:
 * (1)互斥锁 (Mutex Lock)
 * 核心思想:只允许一个线程去查询数据库,其他线程等待
 * 实现方式:使用Redis的setnx命令或Redisson分布式锁
 * (2)热点数据永不过期
 * 核心思想:对热点数据不设置过期时间,通过其他机制更新
 * 实现方式:逻辑过期时间 + 异步更新
 * (3)接口限流与降级
 * 核心思想:控制访问数据库的并发量
 * 实现方式:使用限流组件如Sentinel

二、代码

2.1 controller层

java 复制代码
package com.study.sredis.stept001.controller;

import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.service.User5Service;
import com.study.sredis.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/snow")
public class CacheTest5Controller {
    @Autowired
    public User5Service user5Service;

    /**
     * 互斥锁方案
     *
     * @param user
     * @return
     */
    @PostMapping("/mutexById")
    public R getUserWithMutex(@RequestBody User user) {
        try {
            User user1 = user5Service.getUserWithMutex(user);
            return R.ok(user1);
        } catch (Exception e) {
            return R.fail("用户信息不存在");
        }
    }

    /**
     * Redisson锁方案
     */
    @PostMapping("/redissonById")
    public R getUserWithRedisson(@RequestBody User user) {
        try {
            User userInfo = user5Service.getUserWithRedissonLock(user);
            return R.ok(userInfo);
        } catch (Exception e) {
            return R.fail("用户信息不存在");
        }
    }


    /**
     * 逻辑过期方案
     */
    @PostMapping("/logicalByid")
    public R getUserWithLogical(@RequestBody User user) {
        try {
            User userInfo = user5Service.getUserWithLogicalExpire(user);
            return R.ok(userInfo);
        } catch (Exception e) {
            return R.fail("用户信息不存在");
        }
    }

    /**
     * 热点数据方案
     */
    @PostMapping("/hotByid")
    public R getHotUser(@RequestBody User user) {
        try {
            User userInfo = user5Service.getHotUser(user);
            return R.ok(userInfo);
        } catch (Exception e) {
            return R.fail("用户信息不存在");
        }
    }

    /**
     * 更新产品信息
     */
    @PostMapping("/updateUser")
    public R updateUser(@RequestBody User user) {
        try {
            boolean success = user5Service.updateUser(user);
            return R.ok(success);
        } catch (Exception e) {
            return R.fail("修改失败");
        }
    }
}

2.2 service层

2.2.1 service接口层

java 复制代码
package com.study.sredis.stept001.service;

import com.study.sredis.stept001.domain.User;

public interface User5Service {
    /**
     * 互斥锁方案
     * @param user
     * @return
     */
    User getUserWithMutex(User user);

    /**
     * Redisson锁方案
     * @param user
     * @return
     */
    User getUserWithRedissonLock(User user);

    /**
     * 逻辑过期方案
     * @param user
     * @return
     */
    User getUserWithLogicalExpire(User user);

    /**
     * 热点数据方案
     * @param user
     * @return
     */
    User getHotUser(User user);

    /**
     * 更新产品信息
     * @param user
     * @return
     */
    boolean updateUser(User user);
}

2.2.2 service实现类

java 复制代码
package com.study.sredis.stept001.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.dto.RedisData;
import com.study.sredis.stept001.mapper.userMapper;
import com.study.sredis.stept001.service.User5Service;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

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

/**
 * 缓存击穿
 * 定义:指一个热点key在缓存过期的瞬间,同时有大量请求访问这个key,导致所有请求都直接打到数据库上,造成数据库压力激增
 * 解决方案:
 * (1)互斥锁 (Mutex Lock)
 * 核心思想:只允许一个线程去查询数据库,其他线程等待
 * 实现方式:使用Redis的setnx命令或Redisson分布式锁
 * (2)热点数据永不过期
 * 核心思想:对热点数据不设置过期时间,通过其他机制更新
 * 实现方式:逻辑过期时间 + 异步更新
 * (3)接口限流与降级
 * 核心思想:控制访问数据库的并发量
 * 实现方式:使用限流组件如Sentinel
 */
@Service
public class User5ServiceImpl extends ServiceImpl<userMapper, User> implements User5Service {
    @Autowired
    private userMapper userMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    private static final String PRODOUCT_KEY_PREFIX = "product";
    private static final String LOCK_KEY_PREFIX = "lock:product";
    private static final Duration CACHE_TIMEOUT = Duration.ofMinutes(30);
    private static final Duration LOCK_TIMEOUT = Duration.ofSeconds(10);


    /**
     * 解决方案一:互斥锁
     *
     * @param user
     * @return
     */
    @Override
    public User getUserWithMutex(User user) {
        String cachekey = PRODOUCT_KEY_PREFIX + user.getId();

//        1.从缓存查询
        User user1 = getFromCache(cachekey);
        if (user1 != null && user1.getId() != null) {
            return user1;
        }
//        2.获取分布式锁
        String lockKey = LOCK_KEY_PREFIX + user.getId();
        boolean locked = false;
        try {
//            尝试获取锁
            locked = tryLock(lockKey);
            if (locked) {
//                3.双重检查缓存
                user1 = getFromCache(cachekey);
                if (user1 != null && user1.getId() != null) {
                    return user1;
                }
//                4.查询数据库
                user1 = this.getById(user.getId());
                if (user1 != null) {
//                    写入缓存
                    setCache(cachekey, user1, CACHE_TIMEOUT);
                } else {
//              缓存空值防止穿透
                    setCache(cachekey, new User(), Duration.ofMinutes(5));
                }
                return user1;
            } else {
//                    获取锁失败,等待后重试
                Thread.sleep(50);
                return getUserWithMutex(user);
            }
        } catch (Exception e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取用户信息失败", e);
        } finally {
            if (locked) {
                releaseLock(lockKey);
            }
        }
    }

    private User getFromCache(String key) {
        try {
            return (User) redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            return null;
        }
    }

    private void setCache(String key, Object value, Duration timeout) {
        try {
            redisTemplate.opsForValue().set(key, value, timeout);
        } catch (Exception e) {

        }
    }

    private boolean tryLock(String lockKey) {
        return Boolean.TRUE.equals(
                redisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_TIMEOUT)
        );
    }

    private void releaseLock(String lockKey) {
        try {
            redisTemplate.delete(lockKey);
        } catch (Exception e) {

        }
    }

    /**
     * 解决方案二:分布式锁
     *
     * @param user
     * @return
     */
    @Override
    public User getUserWithRedissonLock(User user) {
        String cacheKey = PRODOUCT_KEY_PREFIX + user.getId();
//        1.从缓存中查询
        User user1 = getFromCache(cacheKey);
        if (user1 != null && user1.getId() != null) {
            return user1;
        }
        String lockKey = LOCK_KEY_PREFIX + user.getId();
        RLock lock = redissonClient.getLock(lockKey);
        try {
//            尝试加锁,等待时间3秒,锁超时时间10秒
            boolean isLock = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (isLock) {
                try {
//                    3.双重检查
                    user1 = getFromCache(cacheKey);
                    if (user1 != null && user1.getId() != null) {
                        return user1;
                    }
//                    4.查询数据库
                    user1 = this.getById(user.getId());
                    if (user1 != null) {
                        setCache(cacheKey, user1, CACHE_TIMEOUT);
                    } else {
                        setCache(cacheKey, new User(), Duration.ofMinutes(5));
                    }
                    return user1;
                } finally {
                    lock.unlock();
                }
            } else {
//                获取锁失败,等待后重试
                Thread.sleep(100);
                return getUserWithRedissonLock(user);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取用户信息失败", e);
        }
    }

    /**
     * 解决方案三:逻辑过期
     *
     * @param user
     * @return
     */
    @Override
    public User getUserWithLogicalExpire(User user) {
        String cacheKey = PRODOUCT_KEY_PREFIX + user.getId();
//        1.从缓存查询包装数据
        RedisData<User> redisData = getRedisDataFromCache(cacheKey);
        if (redisData == null) {
//            缓存不存在,直接查询并设置
            return getAndSetProduct(user);
        }
        User user1 = redisData.getData();
        LocalDateTime expireTime = redisData.getExpireTime();
//        2.判断是否逻辑过期
        if (expireTime.isAfter(LocalDateTime.now())) {
//            未过期,直接返回
            return user1;
        }

//        3.已过期, 尝试获取锁重建缓存
        String lockKey = LOCK_KEY_PREFIX + user.getId();
        RLock lock = redissonClient.getLock(lockKey);
        if (lock.tryLock()) {
            try {
//                4.双重检查
                redisData = getRedisDataFromCache(cacheKey);
                if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {
                    return redisData.getData();
                }
//                5.异步重建缓存
                CompletableFuture.runAsync(() -> {
                    rebuildUserCache(user);
                });
            } finally {
                lock.unlock();
            }
        }

//      返回旧数据
        return user1;
    }

    private User getAndSetProduct(User user) {
        User user1 = this.getById(user.getId());
        if (user1 != null) {
            RedisData<User> redisData = new RedisData<>(
                    user1,
                    LocalDateTime.now().plusMinutes(30)  //逻辑过期时间30分钟
            );
            setRedisDataCache(PRODOUCT_KEY_PREFIX + user.getId(), redisData);
        }
        return user1;
    }

    @Async
    public void rebuildUserCache(User user) {
        String lockKey = LOCK_KEY_PREFIX + user.getId();
        RLock lock = redissonClient.getLock(lockKey);

        if (lock.tryLock()) {
            try {
//                再次检查,防止重复更新
                User user1 = this.getById(user.getId());
                if (user1 != null) {
                    RedisData<User> redisData = new RedisData<>(
                            user1,
                            LocalDateTime.now().plusMinutes(30)
                    );
                    setRedisDataCache(PRODOUCT_KEY_PREFIX + user.getId(), redisData);
                }
            } catch (Exception e) {

            } finally {
                lock.unlock();
            }
        }
    }

    private RedisData<User> getRedisDataFromCache(String key) {
        try {
            return (RedisData<User>) redisTemplate.opsForValue().get(key);
        } catch (Exception e) {
            return null;
        }
    }

    private void setRedisDataCache(String key, RedisData<User> redisData) {
        try {
//            永不过期,使用逻辑过期时间控制
            redisTemplate.opsForValue().set(key, redisData);
        } catch (Exception e) {

        }
    }


    @Override
    public User getHotUser(User user) {
        return null;
    }

    @Override
    public boolean updateUser(User user) {
        return false;
    }
}
相关推荐
q***65695 分钟前
Windows环境下安装Redis并设置Redis开机自启
数据库·windows·redis
q***47439 分钟前
Windows 和 Linux 系统下,如何查看 Redis 的版本号?
linux·windows·redis
q***965811 分钟前
Windows版Redis本地后台启动
数据库·windows·redis
利刃大大13 分钟前
【c++中间件】redis介绍 && redis-plus-plus库使用
c++·redis·中间件
q***816415 分钟前
【Redis】centos7 systemctl 启动 Redis 失败
数据库·redis·缓存
AH_HH1 小时前
Spring Boot 4.0 发布总结:新特性、依赖变更与升级指南
java·spring boot·后端
Tadas-Gao1 小时前
Spring Boot 4.0架构革新:构建更精简、更安全、更高效的Java应用
java·spring boot·分布式·微服务·云原生·架构·系统架构
vx_bisheyuange1 小时前
基于SpringBoot的库存管理系统
java·spring boot·后端·毕业设计
你不是我我2 小时前
【Java 开发日记】有了解过 SpringBoot 的参数配置吗?
java·开发语言·spring boot
q***49452 小时前
SpringBoot Maven 项目 pom 中的 plugin 插件用法整理
spring boot·后端·maven