Redis的LRU算法


Redis的LRU算法

LRU算法背后的的思想在計算機科學中無處不在,它與程序的"局部性原理"很相似。在生產環境中,雖然有Redis內存使用告警,但是了解一下Redis的緩存使用策略還是很有好處的。下面是生產環境下Redis使用策略:最大可用內存限制為4GB,采用 allkeys-lru 刪除策略。所謂刪除策略:當redis使用已經達到了最大內存,比如4GB時,如果這時候再往redis里面添加新的Key,那么Redis將選擇一個Key刪除。那如何選擇合適的Key刪除呢?

CONFIG GET maxmemory

  1. "maxmemory"
  2. "4294967296"

CONFIG GET maxmemory-policy

  1. "maxmemory-policy"
  2. "allkeys-lru"

在官方文檔Using Redis as an LRU cache描述中,提供了好幾種刪除策略,比如 allkeys-lru、volatile-lru等。在我看來按選擇時考慮三個因素:隨機、Key最近被訪問的時間 、Key的過期時間(TTL)

比如:allkeys-lru,"統計一下所有的"Key歷史訪問的時間,把"最老"的那個Key移除。注意:我這里加了引號,其實在redis的具體實現中,要統計所有的Key的最近訪問時間代價是很大的。想想,如何做到呢?

evict keys by trying to remove the less recently used (LRU) keys first, in order to make space for the new data added.

再比如:allkeys-random,就是隨機選擇一個Key,將之移除。

evict keys randomly in order to make space for the new data added.

再比如:volatile-lru,它只移除那些使用 expire 命令設置了過期時間的Key,根據LRU算法來移除。

evict keys by trying to remove the less recently used (LRU) keys first, but only among keys that have an expire set, in order to make space for the new data added.

再比如:volatile-ttl,它只移除那些使用 expire 命令設置了過期時間的Key,哪個Key的 存活時間(TTL KEY 越小)越短,就優先移除。

evict keys with an expire set, and try to evict keys with a shorter time to live (TTL) first, in order to make space for the new data added.

volatile 策略(eviction methods) 作用的Key 是那些設置了過期時間的Key。在redisDb結構體中,定義了一個名為 expires 字典(dict)保存所有的那些用expire命令設置了過期時間的key,其中expires字典的鍵指向redis 數據庫鍵空間(redisServer--->redisDb--->redisObject)中的某個鍵,而expires字典的值則是這個鍵的過期時間(long類型整數)。
額外提一下:redis 數據庫鍵空間是指:在結構體redisDb中定義的一個名為"dict",類型為hash字典的一個指針,它用來保存該redis DB中的每一個鍵對象、以及相應的值對象。

既然有這么多策略,那我用哪個好呢?這就涉及到Redis中的Key的訪問模式了(access-pattern),access-pattern與代碼業務邏輯相關,比如說符合某種特征的Key經常被訪問,而另一些Key卻不怎么用到。如果所有的Key都可能機會均等地被我們的應用程序訪問,那它的訪問模式服從均勻分布;而大部分情況下,訪問模式服從冪指分布(power-law distribution),另外Key的訪問模式也有可能是隨着時間變化的,因此需要一種合適的刪除策略能夠catch 住 (捕獲住)各種情形。而在冪指分布下,LRU是一種很好的策略:

While caches can’t predict the future, they can reason in the following way: keys that are likely to be requested again are keys that were recently requested often. Since usually access patterns don’t change very suddenly, this is an effective strategy.

Redis中LRU策略的實現

最直觀的想法:LRU啊,記錄下每個key 最近一次的訪問時間(比如unix timestamp),unix timestamp最小的Key,就是最近未使用的,把這個Key移除。看下來一個HashMap就能搞定啊。是的,但是首先需要存儲每個Key和它的timestamp。其次,還要比較timestamp得出最小值。代價很大,不現實啊。

第二種方法:換個角度,不記錄具體的訪問時間點(unix timestamp),而是記錄idle time:idle time越小,意味着是最近被訪問的。

The LRU algorithm evicts the Least Recently Used key, which means the one with the greatest idle time.

比如A、B、C、D四個Key,A每5s訪問一次,B每2s訪問一次,C和D每10s訪問一次。(一個波浪號代表1s),從上圖中可看出:A的空閑時間是2s,B的idle time是1s,C的idle time是5s,D剛剛訪問了所以idle time是0s

這里,用一個雙向鏈表(linkedlist)把所有的Key鏈表起來,如果一個Key被訪問了,將就這個Key移到鏈表的表頭,而要移除Key時,直接從表尾移除。

It is simple to implement because all we need to do is to track the last time a given key was accessed, or sometimes this is not even needed: we may just have all the objects we want to evict linked in a linked list.

但是在redis中,並沒有采用這種方式實現,它嫌LinkedList占用的空間太大了。Redis並不是直接基於字符串、鏈表、字典等數據結構來實現KV數據庫,而是在這些數據結構上創建了一個對象系統Redis Object。在redisObject結構體中定義了一個長度24bit的unsigned類型的字段,用來存儲對象最后一次被命令程序訪問的時間:

By modifying a bit the Redis Object structure I was able to make 24 bits of space. There was no room for linking the objects in a linked list (fat pointers!)

畢竟,並不需要一個完全准確的LRU算法,就算移除了一個最近訪問過的Key,影響也不太。

To add another data structure to take this metadata was not an option, however since LRU is itself an approximation of what we want to achieve, what about approximating LRU itself?

最初Redis是這樣實現的:

隨機選三個Key,把idle time最大的那個Key移除。后來,把3改成可配置的一個參數,默認為N=5:maxmemory-samples 5

when there is to evict a key, select 3 random keys, and evict the one with the highest idle time

就是這么簡單,簡單得讓人不敢相信了,而且十分有效。但它還是有缺點的:每次隨機選擇的時候,並沒有利用歷史信息。在每一輪移除(evict)一個Key時,隨機從N個里面選一個Key,移除idle time最大的那個Key;下一輪又是隨機從N個里面選一個Key...有沒有想過:在上一輪移除Key的過程中,其實是知道了N個Key的idle time的情況的,那我能不能在下一輪移除Key時,利用好上一輪知曉的一些信息?

However if you think at this algorithm across its executions, you can see how we are trashing a lot of interesting data. Maybe when sampling the N keys, we encounter a lot of good candidates, but we then just evict the best, and start from scratch again the next cycle.

start from scratch太傻了。於是Redis又做出了改進:采用緩沖池(pooling)

當每一輪移除Key時,拿到了這個N個Key的idle time,如果它的idle time比 pool 里面的 Key的idle time還要大,就把它添加到pool里面去。這樣一來,每次移除的Key並不僅僅是隨機選擇的N個Key里面最大的,而且還是pool里面idle time最大的,並且:pool 里面的Key是經過多輪比較篩選的,它的idle time 在概率上比隨機獲取的Key的idle time要大,可以這么理解:pool 里面的Key 保留了"歷史經驗信息"。

Basically when the N keys sampling was performed, it was used to populate a larger pool of keys (just 16 keys by default). This pool has the keys sorted by idle time, so new keys only enter the pool when they have an idle time greater than one key in the pool or when there is empty space in the pool.

采用"pool",把一個全局排序問題 轉化成為了 局部的比較問題。(盡管排序本質上也是比較,囧)。要想知道idle time 最大的key,精確的LRU需要對全局的key的idle time排序,然后就能找出idle time最大的key了。但是可以采用一種近似的思想,即隨機采樣(samping)若干個key,這若干個key就代表着全局的key,把samping得到的key放到pool里面,每次采樣之后更新pool,使得pool里面總是保存着隨機選擇過的key的idle time最大的那些key。需要evict key時,直接從pool里面取出idle time最大的key,將之evict掉。這種思想是很值得借鑒的。

至此,基於LRU的移除策略就分析完了。Redis里面還有一種基於LFU(訪問頻率)的移除策略,后面有時間再說。

JAVA 里面的LRU實現

JDK中的LinkedHashMap實現了LRU算法,使用如下構造方法,accessOrder 表示"最近最少未使用"的衡量標准。比如accessOrder=true,當執行java.util.LinkedHashMap#get元素時,就表示這個元素最近被訪問了。

/**
     * Constructs an empty <tt>LinkedHashMap</tt> instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - <tt>true</tt> for
     *         access-order, <tt>false</tt> for insertion-order
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

再重寫:java.util.LinkedHashMap#removeEldestEntry方法即可。

The {@link #removeEldestEntry(Map.Entry)} method may be overridden to impose a policy for removing stale mappings automatically when new mappings are added to the map.

為了保證線程安全,用Collections.synchronizedMap將LinkedHashMap對象包裝起來:

Note that this implementation is not synchronized. If multiple threads access a linked hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally. This is typically accomplished by synchronizing on some object that naturally encapsulates the map.

實現如下:(org.elasticsearch.transport.TransportService)

final Map<Long, TimeoutInfoHolder> timeoutInfoHandlers =
        Collections.synchronizedMap(new LinkedHashMap<Long, TimeoutInfoHolder>(100, .75F, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry eldest) {
                return size() > 100;
            }
        });

當容量超過100時,開始執行LRU策略:將最近最少未使用的 TimeoutInfoHolder 對象 evict 掉。

參考鏈接:

最近,在生產環境上對某一微服務部署了多份,多個client並發訪問同一份數據,造成了數據的重復計算,需要分布式鎖解決這個問題。看了下redis 的 transaction documention 以及redis distlock,在結尾發現《design data intensive applications》的作者寫了一篇文章 how-to-do-distributed-locking分析了使用redis實現分布式鎖的缺陷:嚴重依賴於計算機時鍾的"准確性",而在分布式環境下,本地時鍾是不可靠的,因此使用redis作為分布式鎖在某些條件下(比如網絡延時)違反了鎖的一個性質:安全性。作者給出的解決方案是每次修改數據時比較 fencing token,這種使用 fencing token 的思路非常類似於MySQL里面的"樂觀鎖"實現方式,即:都是基於"數據版本號"判斷是否發生了沖突。此外,martin kleppmann 還建議如果是使用分布式鎖保證安全性,那么最好使用基於Zookeeper實現分布式鎖,由zookeeper的高可用性保證分布式鎖的可用性。
有趣的是,redlock的作者也寫了一篇文章 Is Redlock safe?作為回應martin kleppmann的質疑,讀完之后相信對分布式鎖會有一個更好的理解。不禁感嘆讀官方文檔、英文原文可以極大提高理解技術本質的效率,一篇好的文章真的能節約很多時間。好了,不說了,說多了我就懷疑自己寫的東西了^~^。

最后,再扯一點與本文不相關的東西:關於數據處理的一些思考:
5G有能力讓各種各樣的節點都接入網絡,就會產生大量的數據,如何高效地處理這些數據、挖掘數據(機器學習、深度學習)背后的隱藏的模式是一件非常有趣的事情,因為這些數據其實是人類行為的客觀反映。舉個例子,微信運動的計步功能,會讓你發現:哎,原來每天我的活動量大概保持在1萬步左右。再深入一步,以后各種工業設備、家用電器,都會產生各種各樣的數據,這些數據是社會商業活動的體現,也是人類活動的體現。如何合理地利用這些數據呢?現有的存儲系統能支撐這些數據的存儲嗎?有沒有一套高效的計算系統(spark or tensorflow...)分析出數據中隱藏的價值?另外,基於這些數據訓練出來的算法模型應用之后會不會帶來偏見?有沒有濫用數據?網上買東西有算法給你推薦,刷朋友圈會給你投針對性的廣告,你需要借多少錢,貸多少款也會有模型評估,看新聞、看娛樂八卦也有算法推薦。你每天看到的東西、接觸的東西,有很多很多都是這些"數據系統"做的決策,它們真的公平嗎?在現實中如果你犯了錯,有老師、朋友、家人給你反饋、糾正,你改了,一切還可以從頭開始。可在這些數據系統之下呢?有糾錯反饋機制嗎?會不會刺激人越陷越深?就算你很正直、有良好的信用,算法模型也可能存在概率偏差,這是不是影響人類"公平競爭"的機會?這些都是數據開發人員值得思考的問題。

原文:https://www.cnblogs.com/hapjin/p/10933405.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM