一个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/redisgithub.com
参考文章
【Redis源码分析】Redis中的LRU算法实现segmentfault.com