1. 程式人生 > >雙重檢測同步鎖---防止Redis快取穿透

雙重檢測同步鎖---防止Redis快取穿透

快取穿透:



注:
上面三個圖會有什麼問題呢?

我們在專案中使用快取通常都是先檢查快取中是否存在,如果存在直接返回快取內容,如果不存在就直接查詢資料庫然後再快取查詢結果返回。這個時候如果我們查詢的某一個數據在快取中一直不存在,就會造成每一次請求都查詢DB,這樣快取就失去了意義,在流量大時,可能DB就掛掉了。

那這種問題有什麼好辦法解決呢?

要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。
有一個比較巧妙的作法是,可以將這個不存在的key預先設定一個值。
比如,”key” , “&&”。
在返回這個&&值的時候,我們的應用就可以認為這是不存在的key,那我們的應用就可以決定是否繼續等待繼續訪問,還是放棄掉這次操作。如果繼續等待訪問,過一個時間輪詢點後,再次請求這個key,如果取到的值不再是&&,則可以認為這時候key有值了,從而避免了透傳到資料庫,從而把大量的類似請求擋在了快取之中。

快取併發:

有時候如果網站併發訪問高,一個快取如果失效,可能出現多個程序同時查詢DB,同時設定快取的情況,如果併發確實很大,這也可能造成DB壓力過大,還有快取頻繁更新的問題。

我現在的想法是對快取查詢加鎖,如果KEY不存在,就加鎖,然後查DB入快取,然後解鎖;其他程序如果發現有鎖就等待,然後等解鎖後返回資料或者進入DB查詢。

這種情況和剛才說的預先設定值問題有些類似,只不過利用鎖的方式,會造成部分請求等待。


以上來源:http://ifeve.com/concurrency-cache-cross/

根據快取併發這種思想,寫了以下的雙重檢測同步鎖:

@Service
public class StudentServiceImpl implements StudentService {

    @Autowired
    private StudentMapper studentMapper;

    //springboot自動配置的,直接注入到類中即可使用
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    /**指定key的序列化方式,字串的方式*/
    private RedisSerializer keyRedisSerializer = new StringRedisSerializer();

    /**指定value的序列化方式,字串的方式*/
    private RedisSerializer valueRedisSerializer = new Jackson2JsonRedisSerializer<Student>(Student.class);

    /**
     * 查詢所有學生資訊,帶有快取
     *
     * @return
     */
    public List<Student> getAllStudent() {
        //KEY 是按照字串方式序列化,可讀性較好
        redisTemplate.setKeySerializer(keyRedisSerializer);
        
        //在高併發條件下,會出現快取穿透
        List<Student> studentList = (List<Student>)redisTemplate.opsForValue().get("allStudent");

        if (null == studentList) {
            //5個人, 4個等,1個進入
            synchronized (this) {
    //雙重檢測鎖,假使同時有5個請求進入了上一個if(null == studentList),加了鎖之後one by one 的訪問,這裡再次對快取進行檢測,盡一切可能防止快取穿透的產生,但是效能會有所損失
                studentList = (List<Student>)redisTemplate.opsForValue().get("allStudent");
                if (null == studentList) {
                    studentList = studentMapper.getAllStudent();
                    redisTemplate.opsForValue().set("allStudent", studentList);
                    System.out.println("請求的資料庫。。。。。。");
                } else {
                    //System.out.println("請求的快取。。。。。。");
                }
            }
        } else {
            System.out.println("請求的快取。。。。。。");
        }
        return studentList;
    }
}