1. 程式人生 > 實用技巧 >springboot中redis的快取穿透問題

springboot中redis的快取穿透問題

什麼是快取穿透問題??

我們使用redis是為了減少資料庫的壓力,讓儘量多的請求去承壓能力比較大的redis,而不是資料庫。但是高併發條件下,可能會在redis還沒有快取的時候,大量的請求同時進入,導致一大批的請求直奔資料庫,而不會經過redis。使用程式碼模擬快取穿透問題如下:

首先是service裡面的程式碼:

@Service
public class NewsService {
    @Autowired
    private NewsDAO newsDAO;

    //springboot自動初始化,不需要我們進行配置,直接注入到程式碼中使用
    @Autowired
    
private RedisTemplate<Object,Object> redisTemplate; public /*synchronized*/ List<News> getLatestNews(int userId,int offset,int limit){ //設定序列化方式,防止亂碼 redisTemplate.setKeySerializer(new StringRedisSerializer()); //第一步:查詢快取 News news= (News) redisTemplate.opsForValue().get("newsKey");
//判斷是否存在快取 if(null == news){//查詢資料庫 news = newsDAO.selectByUserIdAndOffset(userId,offset,limit).get(0); // redisTemplate.opsForValue().set("newsKey",news); System.out.println("進入資料庫。。。。。。。。"); }else{ System.out.println(
"進入快取。。。。。。。。。"); } return newsDAO.selectByUserIdAndOffset(userId,offset,limit); } }

然後是使用執行緒池在Controller裡面對請求進行模擬:

@Controller
public class HomeController {
    @Autowired
    UserService userService;

    @Autowired
    NewsService newsService;

    //遇到的坑,如果不加method,頁面啟動不起來。
    @RequestMapping(value = "/home",method = {RequestMethod.GET, RequestMethod.POST})
    @ResponseBody
    public String index(Model model){
        //這邊是可以讀出資料來的

        //執行緒池------快取穿透問題的復現
        ExecutorService executorService = Executors.newFixedThreadPool(8*2);

        for(int i = 0;i < 50000;i++){
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    List<News> newsList = newsService.getLatestNews(0,0,10);
                }
            });
        }

        List<News> newsList = newsService.getLatestNews(0,0,10);
        News news=newsList.get(0);
        return news.getImage();
    }
}

結果如圖:大量的請求進入資料庫,那麼如何解決這個問題?

方法一、在方法上加鎖:

@Service
public class NewsService {
    @Autowired
    private NewsDAO newsDAO;

    //springboot自動初始化,不需要我們進行配置,直接注入到程式碼中使用
    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    //第一種方式:方法加鎖
    public synchronized List<News> getLatestNews(int userId,int offset,int limit){

        //設定序列化方式,防止亂碼
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        //第一步:查詢快取
        News news= (News) redisTemplate.opsForValue().get("newsKey");
        //判斷是否存在快取
        if(null == news){
//查詢資料庫
                news = newsDAO.selectByUserIdAndOffset(userId,offset,limit).get(0);
                //
                redisTemplate.opsForValue().set("newsKey",news);

                System.out.println("進入資料庫。。。。。。。。");

        }else{
            System.out.println("進入快取。。。。。。。。。");
        }


        return newsDAO.selectByUserIdAndOffset(userId,offset,limit);

    }
}

直接在方法上加鎖,保證每次只有一個請求可以進入。但是這個方法存在一個缺陷,每次只有一個請求可以進入,請求處理的速度變得相當的慢,不利於系統的實時性。

方法二、使用雙重校驗鎖:

@Service
public class NewsService {
    @Autowired
    private NewsDAO newsDAO;

    //springboot自動初始化,不需要我們進行配置,直接注入到程式碼中使用
    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    //第一種方式:方法加鎖
    public /*synchronized*/ List<News> getLatestNews(int userId,int offset,int limit){

        //設定序列化方式,防止亂碼
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        //第一步:查詢快取
        News news= (News) redisTemplate.opsForValue().get("newsKey");
        //判斷是否存在快取
        if(null == news){

            //第二種方式:雙重檢測鎖
            synchronized (this){
                //查詢資料庫
                news = newsDAO.selectByUserIdAndOffset(userId,offset,limit).get(0);
                //
                redisTemplate.opsForValue().set("newsKey",news);

                System.out.println("進入資料庫。。。。。。。。");
            }

        }else{
            System.out.println("進入快取。。。。。。。。。");
        }


        return newsDAO.selectByUserIdAndOffset(userId,offset,limit);

    }
}

這個方法比較好,雖然不能保證只有一個請求請求資料庫,但是當第一批請求進來,第二批之後的所有請求全部會在快取取資料。