springboot中redis的快取穿透問題
阿新 • • 發佈:2020-08-03
什麼是快取穿透問題??
我們使用redis是為了減少資料庫的壓力,讓儘量多的請求去承壓能力比較大的redis,而不是資料庫。但是高併發條件下,可能會在redis還沒有快取的時候,大量的請求同時進入,導致一大批的請求直奔資料庫,而不會經過redis。使用程式碼模擬快取穿透問題如下:
首先是service裡面的程式碼:
@Service public class NewsService { @Autowired private NewsDAO newsDAO; //springboot自動初始化,不需要我們進行配置,直接注入到程式碼中使用 @Autowiredprivate 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); } }
這個方法比較好,雖然不能保證只有一個請求請求資料庫,但是當第一批請求進來,第二批之後的所有請求全部會在快取取資料。