1. 程式人生 > 其它 >Redis 中的過期資料清理機制

Redis 中的過期資料清理機制

目前常見的過期清理機制有: 惰性清理、定時清理、定期清理
在 Redis 中採用: 定期清理 + 惰性清理機制來刪除過期資料

惰性清理機制

package com.xtyuns.redisclean;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 惰性清理機制
 * 優點: 無需額外操作, 僅在取值前判斷是否過期即可
 * 缺點: 如果過期的資料不再進行取值操作就會一直存在, 浪費記憶體空間, 因此在 Redis 中需要配合其他策略來使用
 */
public class ALazyClean {
    private static final Map<String, String> redisMap = new HashMap<>();
    private static final Map<String, Long> expireMap = new HashMap<>();

    /**
     * 向容器中新增資料
     * @param key 鍵
     * @param value 值
     * @param expire 設定有效時間, null 表示不自動過期
     */
    public static void set(String key, String value, Long expire) {
        redisMap.put(key, value);
        if (null != expire) {
            expireMap.put(key, System.currentTimeMillis() + expire);
        }
    }

    /**
     * 惰性清理, 當取值時才進行資料清理
     * @param key 所取資料的鍵
     * @return 返回指定資料, 資料失效時返回 null
     */
    public static String get(String key) {
        Long end = expireMap.get(key);
        if (null != end && System.currentTimeMillis() > end) {
            redisMap.remove(key);
            expireMap.remove(key);
        }

        return redisMap.get(key);
    }

    public static void main(String[] args) throws InterruptedException {
        set("k1", "v1", null);
        set("k2", "v2", 2000L);

        TimeUnit.SECONDS.sleep(3);

        System.out.println(get("k1"));
        System.out.println(get("k2"));
    }
}

定時清理機制

package com.xtyuns.redisclean;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 定時清理機制
 * 優點: 實時刪除, 記憶體中不存在已過期的資料
 * 缺點: 當存在大量定時資料時, 需要鉅額的執行緒開銷, 因此在 Redis 中沒有采用這種策略
 */
public class BOnTimeClean {
    private static final Map<String, String> redisMap = new HashMap<>();
    private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            10,
            20,
            30,
            TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy()
    );

    /**
     * 向容器中新增資料, 當該資料設定有效時間時為該資料啟動一條清理執行緒, 在資料有效期結束時刪除該資料
     * @param key 鍵
     * @param value 值
     * @param expire 設定有效時間, null 表示不自動過期
     */
    public static void set(String key, String value, Long expire) {
        redisMap.put(key, value);
        if (null != expire) {
            poolExecutor.submit(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(expire);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                redisMap.remove(key);
            });
        }
    }

    /**
     * 獲取指定資料
     * @param key 所取資料的鍵
     * @return 返回指定資料, 資料失效時返回 null
     */
    public static String get(String key) {
        return redisMap.get(key);
    }

    public static void main(String[] args) throws InterruptedException {
        set("k1", "v1", null);
        set("k2", "v2", 2000L);

        TimeUnit.SECONDS.sleep(3);

        System.out.println(get("k1"));
        System.out.println(get("k2"));
    }
}

週期清理機制

package com.xtyuns.redisclean;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 週期清理機制
 * 優點: 避免了使用大量執行緒的開銷, 同時週期性的清除掉了過期資料
 * 缺點: 由於清理執行緒的週期性會造成資料髒讀情況, 因此在 Redis 中需要配合其他策略來使用
 */
public class CScheduleClean {
    private static final Map<String, String> redisMap = new HashMap<>();
    private static final Map<String, Long> expireMap = new HashMap<>();

    // 在程式開始時啟用一條清理執行緒執行清理任務, 每次任務的時間間隔是 10 秒
    static {
        Thread doClean = new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 這裡不能通過 forEach 來刪除, 因為會觸發 ConcurrentModificationException
                Iterator<Map.Entry<String, Long>> iterator = expireMap.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry<String, Long> next = iterator.next();
                    if (System.currentTimeMillis() > next.getValue()) {
                        iterator.remove();
                        redisMap.remove(next.getKey());
                    }
                }
            }
        });
        doClean.setDaemon(true);
        doClean.start();
    }

    /**
     * 向容器中新增資料
     * @param key 鍵
     * @param value 值
     * @param expire 設定有效時間, null 表示不自動過期
     */
    public static void set(String key, String value, Long expire) {
        redisMap.put(key, value);
        if (null != expire) {
            expireMap.put(key, System.currentTimeMillis() + expire);
        }
    }

    /**
     * 獲取指定資料
     * @param key 所取資料的鍵
     * @return 返回指定資料, 資料失效時返回 null
     */
    public static String get(String key) {
        return redisMap.get(key);
    }

    public static void main(String[] args) throws InterruptedException {
        set("k1", "v1", null);
        set("k2", "v2", 2000L);

        // 3 秒內清理執行緒還未執行, 因此出現髒讀現象
        TimeUnit.SECONDS.sleep(3);

        System.out.println(get("k1"));
        System.out.println(get("k2"));
    }
}

週期 + 惰性清理機制

package com.xtyuns.redisclean;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 週期 + 惰性清理機制
 * 優點: 即保證了記憶體中不會永久儲存過期資料, 也解決了髒讀問題
 * 缺點: 雙重機制執行, 相較於惰性清理機制增加了清理執行緒的開銷, 但是總體影響不大, Redis 中採用這種策略來清除過期資料
 *
 * tip: 在 Redis 的週期清理執行緒中並不是每次都遍歷所有資料, 而是每次隨機取出一定的資料進行遍歷
 */
public class DScheduleLazyClean {
    private static final Map<String, String> redisMap = new HashMap<>();
    private static final Map<String, Long> expireMap = new HashMap<>();

    // 在程式開始時啟用一條清理執行緒執行清理任務, 每次任務的時間間隔是 10 秒
    static {
        Thread doClean = new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 這裡不能通過 forEach 來刪除, 因為會觸發 ConcurrentModificationException
                Iterator<Map.Entry<String, Long>> iterator = expireMap.entrySet().iterator();
                while (iterator.hasNext()) {
                    Map.Entry<String, Long> next = iterator.next();
                    if (System.currentTimeMillis() > next.getValue()) {
                        iterator.remove();
                        redisMap.remove(next.getKey());
                    }
                }
            }
        });
        doClean.setDaemon(true);
        doClean.start();
    }

    /**
     * 向容器中新增資料
     * @param key 鍵
     * @param value 值
     * @param expire 設定有效時間, null 表示不自動過期
     */
    public static void set(String key, String value, Long expire) {
        redisMap.put(key, value);
        if (null != expire) {
            expireMap.put(key, System.currentTimeMillis() + expire);
        }
    }

    /**
     * 週期 + 惰性清理, 當取值時觸發二次資料清理機制
     * @param key 所取資料的鍵
     * @return 返回指定資料, 資料失效時返回 null
     */
    public static String get(String key) {
        Long end = expireMap.get(key);
        if (null != end && System.currentTimeMillis() > end) {
            redisMap.remove(key);
            expireMap.remove(key);
        }

        return redisMap.get(key);
    }

    public static void main(String[] args) throws InterruptedException {
        set("k1", "v1", null);
        set("k2", "v2", 2000L);

        TimeUnit.SECONDS.sleep(3);

        System.out.println(get("k1"));
        System.out.println(get("k2"));
    }
}