Redis的过期策略能介绍一下?要不你再手写一个LRU?

👽System.out.println(“👋🏼嗨,大家好,我是代码不会敲的小符,目前工作于上海某电商服务公司…”);
📚System.out.println(“🎈如果文章中有错误的地方,恳请大家指正!共同进步,共同成长✊”);
🌟System.out.println(“💡如果文章对您有所帮助,希望您可以三连支持一下博主噢🔥”);
🌈System.out.println("🚀正在完成计划中:Java应届第一年规划 ");

故事背景

手里好几篇文章都还没有写,端午节之前一定写完!!先发一篇好久之前的文章把…

基本知识

redis是缓存,不是存储,缓存中的数据可以能随时没有。(一台机器一般是缓存基于内存)。redis是基于内存来进行缓存的。

什么叫缓存?

缓存(Cache)是一种将数据存储在高速缓存存储器(Cache Memory)中,以便于快速访问的技术。在计算机系统中,缓存是一种常见的性能优化技术,用于减少对慢速存储器(如硬盘、数据库等)的访问,从而提高应用程序的响应速度和运行效率。
缓存的工作原理是将经常访问的数据存储在高速缓存存储器中,当应用程序需要访问这些数据时,直接从缓存中获取,而不是从慢速存储器中获取。由于高速缓存存储器的访问速度远远高于慢速存储器,因此可以显著提高应用程序的响应速度和性能。
在计算机系统中,缓存通常被用于优化数据库、文件系统、Web服务器等应用程序的性能。在Web应用程序中,常用的缓存技术包括页面缓存、对象缓存、片段缓存等。缓存技术可以帮助应用程序在高并发情况下快速响应用户请求,提高用户体验和应用程序的吞吐量。

什么叫内存?

内存(Memory)是计算机中的一种重要的存储介质,用于存储程序执行过程中所需要的数据和指令。内存是计算机系统中的一种随机访问存储器(Random Access Memory,简称RAM),可以快速地读取和写入数据。内存通常是被处理器用来进行读取和写入操作的,因此也被称为主存储器(Main Memory)。

内存通常是由许多存储单元组成的,每个存储单元都有一个唯一的地址,可以通过地址来访问内存中的数据。内存的容量通常以字节(Byte)为单位进行计算,例如1GB的内存可以存储大约10亿个字节的数据。内存的访问速度非常快,可以达到几纳秒甚至更快的速度,因此被广泛应用于计算机系统中。
内存在计算机中扮演着非常重要的角色,对计算机的性能和响应速度有着直接的影响。因此,在计算机系统设计和应用程序开发中,需要合理地使用内存资源,减少内存的浪费和冗余,以提高计算机系统的性能和运行效率。

什么叫存储?

存储(Storage)是指计算机系统中用来保存和持久化数据的设备或介质,例如硬盘、SSD、内存、U盘等。存储的数据可以是文件、数据库、程序等,这些数据通常需要长期保存,随时可以被访问和读取。
在计算机系统中,存储通常被分为两种类型:主存储器(Main Memory)和辅助存储器(Auxiliary Storage)。主存储器通常是指内存,用于临时存储程序执行过程中所需要的数据和指令;而辅助存储器则是指硬盘、SSD等设备,用于长期保存数据和程序。
存储设备通常通过数据传输接口(例如SATA、USB等)连接到计算机系统中,可以通过操作系统提供的文件系统进行数据的读取、写入和管理。存储设备的容量、读写速度、稳定性等都是影响计算机系统性能和运行效率的重要因素,因此在计算机系统设计和应用程序开发中,需要合理地选择和使用存储设备,以满足数据存储和访问的需求。

缓存是基于内存的吗?

缓存可以基于内存,也可以基于磁盘等其他设备。基于内存的缓存通常被称为内存缓存(Memory Cache),其数据通常存储在计算机的主存储器中,可以快速地读取和写入数据,因此具有非常高的读写性能和响应速度。内存缓存通常被用于需要快速响应的场景,例如Web服务器缓存、数据库缓存等。
除了内存缓存之外,还有基于磁盘的缓存(Disk Cache),其数据通常存储在磁盘上,可以缓存磁盘上的文件、数据块等内容。基于磁盘的缓存通常被用于需要长期保存数据或者数据量非常大的场景,例如浏览器缓存、操作系统缓存等。
需要注意的是,基于内存的缓存通常具有较小的缓存容量,因为内存资源比较有限;而基于磁盘的缓存通常具有较大的缓存容量,但是读写速度比较慢。因此,在实际应用中,我们需要根据具体场景和需求,选择合适的缓存类型和策略。

redis缓存基于内存的吗?

Redis缓存是一种基于内存的缓存。Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,通过提供键值对存储功能,支持多种数据结构(例如字符串、哈希、列表、集合、有序集合等),并提供了丰富的操作命令和持久化功能,可以用于缓存、消息队列、分布式锁等多种应用场景。
Redis的数据通常存储在内存中,可以快速地读取和写入数据,因此具有非常高的读写性能和响应速度。同时,Redis也支持数据持久化功能,可以将部分或全部数据保存到磁盘中,以保证数据的可靠性和安全性。
需要注意的是,由于内存资源相对较为有限,因此在使用Redis缓存时需要合理地设置缓存容量和淘汰策略,避免出现内存溢出等问题。一些常用的策略包括LRU(最近最少使用)、LFU(最不经常使用)和随机淘汰等。

Redis 在生产环境中丢失数据可能是由于以下原因导致的:

  • 硬件故障、网络问题、配置问题
  • 内存淘汰:Redis的内存是有限的,如果内存不足,Redis可能会触发内存淘汰策略,导致一些数据被删除。
  • 并发问题:Redis是一种高并发数据库,如果并发量过大,可能会导致数据竞争和数据丢失。

Redis的过期策略、Redis的key是怎样进行删除的?

  1. 定时删除:在设置键值对的过期时间时,同时创建一个定时器,让定时器在键值对过期的时候立即执行删除操作。这种方式占用较多内存,但是能够保证键值对一定会在过期时间之后被删除。
  2. 惰性删除:在访问键值对时,先检查键值对是否过期,如果过期则立即删除。这种方式占用较少内存,但是不能保证键值对一定会在过期时间之后被删除,因为在访问键值对之前可能没有被检查过是否过期。

Redis默认使用惰性删除策略,这是因为定时删除策略会占用大量内存,特别是在键值对数量很多的情况下。但是在一些场景下,比如需要精确控制过期时间的场景,可以使用定时删除策略。 除了这两种过期策略,Redis还有一种基于LRU(最近最少使用)算法的过期策略,即当内存空间不足时,Redis会优先删除最近最少使用的键值对。这种策略可以有效地控制内存使用,但是不能精确控制过期时间。
默认惰性删除策略的意思是:当一个键值对设置了过期时间后,Redis首先会采用惰性删除的方式,在访问该键值对时检查该键值对是否过期,并在过期时立即删除。而定时删除方式只有在惰性删除无法保证键值对一定会被删除时才会触发,即定时删除是一种备用的删除策略。 因为在实际使用中,Redis中的键值对数量可能会非常大,如果采用定时删除的方式,会占用大量内存,而且定时删除也不能完全保证键值对一定会在过期时间之后被删除。因此,Redis默认采用惰性删除的方式来删除过期键值对,这种方式可以避免大量的内存占用,同时也能够保证过期键值对在访问时能够被及时删除。

定期删除+情性删除(Redis使用的过期策略是定时删除+惰性删除结合的方式)
场景:
定期删除:指的是redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如只过期就删除。假设redis存放了10万个 key,都设置了过期时间,你每隔几百豪秒,就检查10万个 key,那redis基本上就死了,cpu负载会很高的,都消耗在你的检查过期 key上了。
注意,这些可不是每隔100ms就遍历所有的过期时间的 key,那样就是一场性能上的交难。实际上redis是每隔100ms 随机抽投一些 key来检查和删除的。
问题:定期删除可能会导致很多过期的 key到了的时间但并没有被删除,那咋整呢?就是惰性删除了。这就是说,在你获取某个 key 的时候reddis 会检查一下,这个key 如果设置了过期时间,那么检查是否过期了?如果过期就会删除,不会给你返回任何东西。
并不是key到时间就被删除掉,而是你查询这个key的时侯,redis会再懒情的检查一下。通过上述两种手段结合起来,保证过期的 key一定会被删除。
很简单,就是说,你的过期 key,靠定期删除没有被除掉,还停留在内存里,占用着你的内存呢,除非你的系统去查一下那个 key,才会被redis 给删除掉。
但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也没走惰性删除,此时会怎么样?如果大量过期 key积在内存里,导致redis 内存块耗尽了,咋整?

  • 如果定时删除漏掉了很多过期key,并且也没有及时进行惰性删除,那么这些过期key将一直存在于Redis中,占用内存资源,可能会导致内存不足,进而影响Redis的性能和稳定性。
  • 如果大量过期key积在内存里,导致Redis内存耗尽,Redis会触发内存淘汰策略,以释放内存空间。
    答案是:走内存淘汰机制。

Redis内存淘汰策略

Redis的内存淘汰策略包括以下几种:

  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报猎,这个一般没人用吧,实在太恶心了
    ■ 不淘汰,当内存不足时直接返回错误。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
    ■ 从所有的键中,选择最近最少使用的键淘汰。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机终除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key给干掉啊)
    ■ 从所有的键中,随机选择一些键淘汰。
  • volatile-lru:当内存不以容纳新写入教据时,设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合活)
    ■ 从设置过过期时间的键中,选择最近最少使用的键淘汰。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
    ■ 从设置过过期时间的键中,随机选择一些键淘汰。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除(就是移除最快过期的key)
    ■ 从设置过过期时间的键中,选择剩余时间最短的键淘汰。
  • allkeys-lru和volatile-lru是基于LRU(最近最少使用)算法的淘汰策略,可以优先淘汰最近最少使用的键值对。
  • allkeys-random和volatile-random是随机淘汰策略,可以随机选择一些键值对进行删除。
  • volatile-ttl是基于过期时间的淘汰策略,可以优先淘汰剩余时间最短的键值对。

如果Redis的内存块耗尽了,可以通过修改Redis的配置文件,或者使用命令来修改Redis的内存淘汰策略。当Redis的内存不足时,Redis会按照内存淘汰策略进行淘汰,以释放空间。需要注意的是,内存淘汰会导致数据丢失,因此需要根据实际业务需求选择合适的内存淘汰策略。

LRU算法

思路

LRUCache类表示一个LRU缓存,其中Entry类表示缓存中的一个条目,包括键、值、前驱节点和后继节点。
cacheSize表示缓存容量,cacheMap表示缓存中的所有条目,head和tail分别表示缓存中的头节点和尾节点。 LRU缓存的get和put方法分别用于获取缓存中的值和添加新的键值对。

  • 当获取某个键的值时,若该键不存在于缓存中,则返回null;
  • 若该键存在于缓存中,则将该条目移动到链表头部,并返回该条目的值。

当添加新的键值对时

  • 若该键不存在于缓存中,则创建一个新的条目,并将其添加到链表头部;
  • 若该键已经存在于缓存中,则更新该条目的值,并将其移动到链表头部。
  • 若添加新的键值对后,缓存大小超过了缓存容量,将移除链表尾部的条目。

需要注意的是,在上述代码中,所有的LRU缓存操作都是同步的,以避免并发访问时的数据不一致问题。同时,在实际使用中,还需要根据具体情况和需求,选择合适的缓存容量和淘汰策略。

实现

LRUCache是一个泛型类,表示一个LRU缓存,其中K和V分别表示键和值。
cacheSize表示缓存容量,cacheMap表示缓存容器,存储所有的缓存条目,head和tail分别表示链表头部和尾部的缓存条目。
LRUCache类中有get、put、addEntry、removeEntry、moveToHead和removeTail六个方法,分别实现获取缓存、设置缓存、添加节点、删除节点、将节点移到头部和删除尾部节点的功能。
Entry是一个内部类,表示缓存中的一个条目,它包括键、值、前驱节点和后继节点。

import java.util.HashMap;
import java.util.Map;
public class LRUCache<K, V> {
    private final int cacheSize;   // 缓存容量
    private final Map<K, Entry<K, V>> cacheMap;   // 缓存容器
    private Entry<K, V> head;   // 链表头
    private Entry<K, V> tail;   // 链表尾
    
    // 构造方法,指定缓存容量
    public LRUCache(int cacheSize) {
        this.cacheSize = cacheSize;
        cacheMap = new HashMap<>(cacheSize);   // 创建一个指定容量的HashMap
    }
    
    // 获取缓存
    public synchronized V get(K key) {
        Entry<K, V> entry = cacheMap.get(key);   // 从缓存容器中获取指定key的Entry
        if (entry == null) {   // 若Entry不存在,则返回null
            return null;
        }
        moveToHead(entry);   // 将该Entry移动到链表头部,表示最近使用
        return entry.value;   // 返回该Entry的value
    }
    
    // 设置缓存
    public synchronized void put(K key, V value) {
        Entry<K, V> entry = cacheMap.get(key);   // 从缓存容器中获取指定key的Entry
        if (entry == null) {   // 若Entry不存在,则创建一个新的Entry,并将其加入缓存容器和链表头部
            entry = new Entry<>(key, value);
            cacheMap.put(key, entry);
            addEntry(entry);
            if (cacheMap.size() > cacheSize) {   // 若缓存容器中的Entry数量超过了缓存容量,则删除链表尾部的Entry
                removeTail();
            }
        } else {   // 若Entry已经存在,则更新其value,并将其移动到链表头部
            entry.value = value;
            moveToHead(entry);
        }
    }
    
    // 将指定的Entry添加到链表头部
    private void addEntry(Entry<K, V> entry) {
        entry.next = head;
        entry.prev = null;
        if (head != null) {   // 若链表不为空,则将头部Entry的prev指向该Entry
            head.prev = entry;
        }
        head = entry;   // 将该Entry设置成链表头部
        if (tail == null) {   // 若链表为空,则将该Entry设置成链表尾部
            tail = head;
        }
    }
    
    // 将指定的Entry从链表中删除
    private void removeEntry(Entry<K, V> entry) {
        if (entry.prev != null) {   // 若该Entry有前驱节点,则将其前驱节点的next指向其后继节点
            entry.prev.next = entry.next;
        } else {   // 若该Entry没有前驱节点,则将链表头部指向其后继节点
            head = entry.next;
        }
        if (entry.next != null) {   // 若该Entry有后继节点,则将其后继节点的prev指向其前驱节点
            entry.next.prev = entry.prev;
        } else {   // 若该Entry没有后继节点,则将链表尾部指向其前驱节点
            tail = entry.prev;
        }
    }
    
    // 将指定的Entry移动到链表头部
    private void moveToHead(Entry<K, V> entry) {
        removeEntry(entry);   // 先将该Entry从链表中删除
        addEntry(entry);   // 再将该Entry添加到链表头部
    }
    
    // 删除链表尾部的Entry
    private void removeTail() {
        cacheMap.remove(tail.key);   // 从缓存容器中删除该Entry
        removeEntry(tail);   // 从链表中删除该Entry
    }
    
    // 缓存中的条目
    private static class Entry<K, V> {
        K key;   // 键
        V value;   // 值
        Entry<K, V> prev;   // 前驱节点
        Entry<K, V> next;   // 后继节点
        
        // 构造方法
        public Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }
}

最后

慢慢的来,别着急!学会有质量的走过每一步


我是代码不会敲的小符,希望认识更多有经验的大佬,也在努力摸索出自己的道路
欢迎交流:A13781678921,一起加油

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码不会敲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值