node怎么把token放到redis_redis缓存过期机制

笔者在线上使用redis缓存的时候发现即使某些查询已经设置了无过期时间的缓存,但是查询仍然非常耗时。经过排查,发现缓存确实已经不存在,导致了缓存击穿,查询直接访问了mysql数据库。因为我们用的是公司公共的redis缓存服务器,在和运维人员交流后发现可能是redis的内存淘汰策略引起。

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是无序的,所以需要维护一个数据最近被访问的顺序队列,如果某个数据节点被访问,那么我们需要将该数据节点移到队列的尾部。

2d018aae3d4119162167d233f9ccab74.png

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

45ec14fc1f7a981d9ff34f5601aa22eb.png

因此我们需要额外的数据结构,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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值