redis过期时间


一个key,如果被设置了过期时间,那么不会因为期间有get的操作,而改变这个key过期的时间和设置。

转自https://www.cnblogs.com/xiaoxiongcanguan/p/9937433.html?utm_source=tuicool&utm_medium=referral

1.redis过期时间介绍

  有时候我们并不希望redis的key一直存在。例如缓存,验证码等数据,我们希望它们能在一定时间内自动的被销毁。redis提供了一些命令,能够让我们对key设置过期时间,并且让key过期之后被自动删除。

2.redis过期时间相关命令

  1.EXPIRE PEXPIRE 

  EXPIRE 接口定义:EXPIRE key "seconds"
  接口描述:设置一个key在当前时间"seconds"(秒)之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间。

 
  PEXPIRE 接口定义:PEXPIRE key "milliseconds"
  接口描述:设置一个key在当前时间"milliseconds"(毫秒)之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间。

  

  2.EXPIREAT PEXPIREAT

  EXPIREAT 接口定义:EXPIREAT key "timestamp"
  接口描述:设置一个key在"timestamp"(时间戳(秒))之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间。


  PEXPIREAT 接口定义:PEXPIREAT key "milliseconds-timestamp"
  接口描述:设置一个key在"milliseconds-timestamp"(时间戳(毫秒))之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间 

  

  3.TTL PTTL

  TTL 接口定义:TTL key
  接口描述:获取key的过期时间。如果key存在过期时间,返回剩余生存时间(秒);如果key是永久的,返回-1;如果key不存在或者已过期,返回-2。


  PTTL 接口定义:PTTL key
  接口描述:获取key的过期时间。如果key存在过期时间,返回剩余生存时间(毫秒);如果key是永久的,返回-1;如果key不存在或者已过期,返回-2。

  

  4.PERSIST

  PERSIST 接口定义:PERSIST key
  接口描述:移除key的过期时间,将其转换为永久状态。如果返回1,代表转换成功。如果返回0,代表key不存在或者之前就已经是永久状态。  

  

  5.SETEX

  SETEX 接口定义:SETEX key "seconds" "value"
  接口描述:SETEX在逻辑上等价于SET和EXPIRE合并的操作,区别之处在于SETEX是一条命令,而命令的执行是原子性的,所以不会出现并发问题。

  

3.redis如何清理过期key 

  redis出于性能上的考虑,无法做到对每一个过期的key进行即时的过期监听和删除。但是redis提供了其它的方法来清理过期的key。

  1.被动清理

  当用户主动访问一个过期的key时,redis会将其直接从内存中删除。

  2.主动清理

  在redis的持久化中,我们知道redis为了保持系统的稳定性,健壮性,会周期性的执行一个函数。在这个过程中,会进行之前已经提到过的自动的持久化操作,同时也会进行内存的主动清理。
  在内存主动清理的过程中,redis采用了一个随机算法来进行这个过程:简单来说,redis会随机的抽取N(默认100)个被设置了过期时间的key,检查这其中已经过期的key,将其清除。同时,如果这其中已经过期的key超过了一定的百分比M(默认是25),则将继续执行一次主动清理,直至过期key的百分比在概率上降低到M以下。

  3.内存不足时触发主动清理

  在redis的内存不足时,也会触发主动清理。

4.redis内存不足时的策略

  redis是一个基于内存的数据库,如果存储的数据量很大,达到了内存限制的最大值,将会出现内存不足的问题。redis允许用户通过配置maxmemory-policy参数,指定redis在内存不足时的解决策略。

  

  1.volatile-lru 使用LRU算法删除一个键(只针对设置了过期时间的key
  2.allkeys-lru 使用LRU算法删除一个键
  3.volatile-lfu 使用LFU算法删除一个键(只针对设置了过期时间的键)
  4.allkeys-lfu 使用LFU算法删除一个键
  5.volatile-random 随机删除一个键(只针对设置了过期时间的键)
  6.allkeys-random 随机删除一个键
  7.volatile-ttl 删除最早过期的一个键
  8.noeviction 不删除键,返回错误信息(redis默认选项)

  对于只针对设置了过期时间的键进行删除的策略,在所有的可被删除的键(非永久的键)都被删除时内存依然不足,将会抛出错误。
  其中,LRU算法--->最近最少使用算法,较为注重于时间;LFU算法--->最近最不常用算法,较为注重于被访问频率。

  redis的内存置换算法和操作系统中的内存置换算法类似,就不在这里展开了。

5.redis过期时间总结

  redis的过期时间还有许多的细节值得去深入了解,例如持久化时对过期时间的处理,redis周期性的常规操作等等,只有这样才能更准确的定位问题,解决问题。

  而想进一步的理解这些知识,除了仔细的思考外,最好的办法就是通过阅读源代码,理解redis的底层运行原理。但是这一目标对于现在的我来说难度过大,希望通过不断的学习,将来的我能够做到这一点。

转自https://www.jianshu.com/p/3bb14817fe8a

redis清理过期数据。

定期清理 + 惰性清理

定期删除:redis数据库默认每隔100ms就会进行随机抽取一些设置过期时间的key进行检测,过期则删除。

惰性删除:定期删除还没有来得及删除,就被程序请求到的一个过期key,redis会先检测key是否,过期,如果过期则删除,不进行返回。

但是前面两种机制可能还导致一些问题就是,过期的key如果大量堆积,删除的速度太慢,内存爆满怎么办?

内存淘汰机制

1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。

2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)就是LRU算法。

3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,一般没人用。

4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)

5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key

6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

LRU算法实现

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    
private final int CACHE_SIZE;
 
    // 这里就是传递进来最多能缓存多少数据
    public LRUCache(int cacheSize) {
        super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); // 这块就是设置一个hashmap的初始大小,同时最后一个true指的是让linkedhashmap按照访问顺序来进行排序,最近访问的放在头,最老访问的就在尾
        CACHE_SIZE = cacheSize;
    }
 
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > CACHE_SIZE; // 这个意思就是说当map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据
    }
 
}
 
 public static void main(String[] args) {
        LRUCache<String, Integer> testCache = new LRUCache<>(3);
        testCache.put("A", 1);
        testCache.put("B", 2);
        testCache.put("C", 3);
        System.out.println(testCache.get("B"));
        System.out.println(testCache.get("A"));
        testCache.put("D", 4);
        System.out.println(testCache.get("D"));
        System.out.println(testCache.get("C"));
    }

转自https://zhuanlan.zhihu.com/p/97875165

1. 什么是内存淘汰

redis是基于内存的key-value数据库,内存是有限的宝贵资源,当内存耗尽的时候,redis有如下5种处理方式来应对

No-eviction

在该策略下,如果继续向redis中添加数据,那么redis会直接返回错误

Allkeys-lru

从所有的key中使用LRU算法进行淘汰

Volatile-lru

从设置了过期时间的key中使用LRU算法进行淘汰

Allkeys-random

从所有key中随机淘汰数据

Volatile-random

从设置了过期时间的key中随机淘汰

Volatile-ttl

从设置了过期时间的key中,选择剩余存活时间最短的key进行淘汰

除上述6种淘汰策略外,Redis 4.0新增了两种淘汰策略

Volatile-lfu

从设置了过期时间的key中选择key进行淘汰

Allkeys-lfu

从所有的key中使用lfu进行淘汰

2. 内存淘汰算法选择

上述总共谈到了8种内存淘汰策略,但是如何选择呢?

从缓存类型来说,其中名称中带volatile的策略确定了被淘汰的缓存仅从设置了过期时间的key中选择,如果没有任何key被设置过期时间,那么Volatile-lru,Volatile-random,Volatile-lfu表现得就分别和allkeys-lru,allkeys-random,allkey-lfu算法相同。

如果缓存有明显的热点分布,那么应该选择lru类型算法,如果缓存被访问几率相同,那么应该选择随机淘汰算法。

在使用中,lru和lfu效果差不多,而且lru可能更有优势一些,笔者在下文详细的对比了lru和lfu

3. 配置使用

我们可以通过redis.conf文件配置如下两个参数

maxmemory 10mb
maxmemory-policy allkeys-random
如果maxmemory被设置为0则代表无限制redis的内存使用,但是这种方案可能会导致redis耗尽内存从而造成磁盘数据交换,这种情况下可能会造成频繁的访问中断,redis也失去了高性能的优势。Redis上述的两个参数也支持运行情况下修改,比如使用如下命令将内存使用限制为100MB,

CONFIG SET maxmemory 100MB
4. LRU

LRU即最近最久未使用算法,它利用局部性原理管理数据,它根据历史访问记录淘汰数据,如果数据最近被访问过,那么将来被访问的几率也越高。

4.1 使用java实现lru

利用java提供LinkedHashMap我们可以非常轻松实现一个指定大小的lru缓存

public class LruCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;
    public LruCache(int capacity) {
        super((int) (Math.ceil(capacity / 0.75) + 1), 0.75f, true);
        this.capacity = capacity;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        //将当前缓存中最旧的元素打印出来
        System.out.println(eldest.getKey() + ":" + eldest.getValue());
        return this.size() > capacity;
    }
}
LinkedHashMap实现LRU缓存的原理是什么呢?LinkedHashMap是基于HashMap实现的。因为HashMap是无序的,所以需要维护一个数据最近被访问的顺序队列,如果某个数据节点被访问,那么我们需要将该数据节点移到队列的尾部。

如果某个节点key1长时间不被访问,那么它就会位于队首,在移除最近没有访问的节点的时候,移除队首节点元素即可。

因此我们需要额外的数据结构,LinkedHashMap使用了链表维护访问顺序,因为节点在队列的位置经常被移动,所以LinkedHashMap使用了双向链表。双向链表节点如下所示

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
如果我们每向缓存中添加一个键值对,就需要增加一个Node节点。如果使用java语言维护缓存key,一个缓存节点需要耗费多少内存呢?

首先Node对象Markword需要占用16个字节,变量before和after分别占用了8个字节,父类中还有如下四个变量

final int hash; //4个字节
final K key; //8个字节 
V value; // 8个字节 
Node<K,V> next; //8个字节
所以为了维护键值对缓存,总共需要60个字节,如果redis实例维护1000万个key,那么总共需要耗费内存572MB内存。即时考虑指针压缩,仍然需要300MB以上内存。

所以如果维护一个严格的LRU算法链表,需要消耗非常多的内存和CPU资源,并且redis是单线程设计的,一旦因为维护key造成阻塞,redis性能就急剧下降,所以在Redis中使用近似LRU算法 N Key In LRU

4.2 N Key In LRU算法

Redis的每个key对象都有一个时间戳字段,它记录的是key最近一次被访问的时间戳,Redis采用的N Key In LRU算法,指的是随机取出若干个key,然后针对这n个key执行lru算法,淘汰掉最近最久未使用的key,redis对n默认的配置是5(在redis配置文件redis.conf具体的参数名为maxmemory-samples),maxmemory-samples参数越大,那么redis的N Key In LRU算法就越接近lru算法,但是maxmemory-samples越大,那么就越消耗CPU

5. LFU算法

从redis4.0开始,redis实现了新的内存淘汰算法LFU,LFU也称为最近最少使用算法,它也是基于局部性原理:如果一个数据在最近一段时间内使用次数最少,那么在将来一段时间内被使用的可能性也很小。

5.1 使用java实现lfu算法

笔者使用java实现了一个简单的lfu算法。核心的put方法如下

public void put(K key, V value) {
    //首先判断是否有剩余空间
    if (this.data.size() >= this.capacity) {
        Map.Entry<K, CacheValue<V>> old = Collections.min(this.data.entrySet(), (o1, o2) -> {
            final CacheValue<V> v1 = o1.getValue();
            final CacheValue<V> v2 = o2.getValue();
            if (v1.getCount() != v2.getCount()) {
                return v1.getCount() - v2.getCount();
            }
            return (int) (v1.getTime() - v2.getTime());
        });
        System.out.println("remove:" + old.getKey() + "," + old.getValue());
        this.data.remove(old.getKey());
    }
    CacheValue old = this.data.get(key);
    if (old != null) {
        old.incCount();
    } else {
        CacheValue<V> cacheValue = new CacheValue<>(value);
        this.data.put(key, cacheValue);
    }
}

LFU和LRU不同之处在于LRU关注于访问时间,而LFU关注于访问频次。和lru相比,笔者没有想到lfu的优点,但是lfu的缺点还是比较明显的。Lfu的缺陷是,如果某些缓存在某段时间内访问非常频繁,这些数据就会立即成为热点数据,但是这些数据都是暂时的高频访问数据,之后长期不访问,在lfu算法下,这些数据都不会被淘汰。比如秒杀场景,在秒杀时,秒杀商品都是高频访问的key,但是秒杀结束后,这些key都可能不会再被访问,但是在lfu算法下,它很长时间都不会被淘汰。上述笔者写的代码也存在一个非常大的问题,就是排序遍历取全部缓存节点访问频次最少的一个key,如果缓存不多还好,如果达到了千万级别缓存,排序遍历这些key的成本是非常大的,Redis作者在实现lfu算法的时候也有考虑到上述问题,

5.2 redis lfu原理

redis作者是如何实现lfu的呢?

实现lfu需要两个关键的字段,一个是key创建时间戳,另一个是访问总数。为了复用字段,redis复用了key的时间戳字段,将时间戳字段一分为二,高16位用于存储分钟级别的时间戳,低八位用于记录访问总数counter值,八位二进制最大值为255。但是key高并发情况下,1000以上的qps也不足为奇。为了将访问次数缩放到255以内,redis引入了server.lfu_log_factor配置值,通过这个配置值,即使是千万级别的访问量,redis也能将其缩放到255以内,redis是通过如下这个方法实现缩放counter值,。

uint8_t LFULogIncr(uint8_t counter) {
if (counter == 255) return 255; 
//取一随机小数
double r = (double)rand()/RAND_MAX; 
//counter减去初始值5,设置初始值的目的是防止key刚被放入就被淘汰
    double baseval = counter - LFU_INIT_VAL;
if (baseval < 0) baseval = 0;
//server.lfu_log_factor默认为10
double p = 1.0/(baseval*server.lfu_log_factor+1);
// counter越大,则p越小,counter获得增长的机会也越小
    if (r < p) counter++; 
    return counter;
}
如何避免临时高频访问的key常驻内存呢?redis采用了一种策略,它会让key的访问次数随着时间衰减。

unsigned long LFUDecrAndReturn(robj *o) {
    //分钟时间戳
unsigned long ldt = o->lru >> 8; 
//当前counter值
unsigned long counter = o->lru & 255;
// 默认每经过一分钟counter衰减1
unsigned long num_periods = server.lfu_decay_time ? LFUTimeElapsed(ldt) / server.lfu_decay_time : 0;
if (num_periods)
    //计算衰减后的值
        counter = (num_periods > counter) ? 0 : counter - num_periods; 
    return counter;
}
为了避免排序过程,redis采用了如下的设计方案。

redis新增了pool机制, redis每次都将随机选择的10个key放在pool中,但是随机选择的key的时间戳必须小于pool中最小的key的时间戳才会继续放入,直到pool放满了,如果有新的key需要放入,那么需要将池中最大的一个时间戳的key取出。

6. 键过期删除策略

6.1 删除方案

定时器

如果key被设置了过期时间,那么便为key创建一个定时器,在缓存键到期的时候,触发一个任务,将该key对应的缓存删除。虽然能保证key对应的键在到达指定的时间时删除并释放缓存占用的内存,但是这种处理方案会创建非常多的定时器,定时器本身会占用非常多的资源,如果遇上缓存雪崩,定时任务在同一时间点被触发,那么在这一时间点会占用非常多的CPU资源

惰性删除

Key过期的时候,不会立即删除缓存。而是当缓存被访问的时候,先检查key是否到期,若到期则删除,同时向用户返回nil。这种处理方式能尽量减少CPU的占用,但是如果有大量的key过期,并且这些缓存永远都不会被用户访问的情况下,会存在内存回收的问题,那么内存浪费严重。

定期删除

创建守护线程,守护线程每隔一段时间对key进行一次扫描,发现过期的key则将缓存删除。这种处理方案也许是内存和CPU之间比较好的平衡,它既不会像定时器方案一样创建大量的定时器占用内存和CPU,也不会像惰性删除那样存在内存泄露的潜在问题。但是这种方案存在最大的问题是用户仍然可能会访问到过期的缓存,并且这种方案难点在于守护线程触发频率的选择上,如果频率太高,并且存在数百万甚至上千万key的时候,那么守护线程占用CPU时间也会特别长可能会影响用户查询,如果执行频率太低,失效的缓存会占用太多的内存。

6.2 Redis删除方案

上述的三种删除方案各有各的好处,为了尽量回收内存,同时减少CPU占用,Redis采用了定时删除+惰性删除的联合方案。一方面采用后台线程定期删除失效的缓存key,另一方面为了避免用户查询到失效的缓存,用户在查询缓存的时候首先需要检查key是否仍然有效,如果失效就删除缓存

上文中涉及的java代码如下

https://github.com/lan1tian/redis​github.com

参考文章

【Redis源码分析】Redis中的LRU算法实现​segmentfault.com


————————————————
版权声明:本文为CSDN博主「gaoshan12345678910」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/gaoshan12345678910/article/details/109805143

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值