Redis的过期策略和 内存淘汰机制有什么区别?Redis 如何实现分布式锁?

本文详细解析了Redis的惰性删除和定期删除过期键机制,以及内存淘汰策略,包括LRU和近似LRU算法的应用,以及如何通过配置实现分布式锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Redis
●对于已经过期的数据, Redis 将使用两种策略来删除这些过期键,它们分别是惰性删除和定期删除
●惰性删除是指Redis服务器不主动删除过期的键值,而是当访问键值时,再检查当前的键值是否过期
如果过期则执行删除并返回null给客户端;如果没过期则正常返回值信息给客户端

●优点:不会浪费太多的系统资源,只是在每次访问时才检查键值是否过期
缺点:删除过期键不及时,造成了一定的空间浪费

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
●除了惰性删除之外,Redis还提供了定期删除功能以弥补惰性删除的不足
●定期删除:指Redis服务器每隔一段时间会检查一 下数据库,看看是否有过期键可以被清除
●默认情况下Redis定期检查的频率是每秒扫描10次
用于定期清除过期键,当然此值还可以通过配置文件进行设置
在redis.conf中修改配置“hz” 即可,默认的值为“hz 10”
tip: 定时删除并不是扫描所有的键值对
这样的话比较费时且太消耗系统资源
Redis服务器采用的是随机抽取形式
每次从过期字典中,取出20个键进行过期检测,过期字典中存储的是所有设置了过期时间的键值对
如果这批随机检查的数据中有25%的比例过期
那么会再抽取20个随机键值进行检测和删除
并且会循环执行这个流程
直到抽取的这批数据中过期键值小于25%
此次检测才算完成

void activeExpireCycle(int type) {
    /* Adjust the running parameters according to the configured expire
     * effort. The default effort is 1, and the maximum configurable effort
     * is 10. */
    unsigned long
    effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
    config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
                           ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
    config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
                                 ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
    config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
                                  2*effort,
    config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
                                    effort;

    /* This function has some global state in order to continue the work
     * incrementally across calls. */
    static unsigned int current_db = 0; /* Last DB tested. */
    static int timelimit_exit = 0;      /* Time limit hit in previous call? */
    static long long last_fast_cycle = 0; /* When last fast cycle ran. */

    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL;
    long long start = ustime(), timelimit, elapsed;

    /* When clients are paused the dataset should be static not just from the
     * POV of clients not being able to write, but also from the POV of
     * expires and evictions of keys not being performed. */
    if (clientsArePaused()) return;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* Don't start a fast cycle if the previous cycle did not exit
         * for time limit, unless the percentage of estimated stale keys is
         * too high. Also never repeat a fast cycle for the same period
         * as the fast cycle total duration itself. */
        if (!timelimit_exit &&
            server.stat_expired_stale_perc < config_cycle_acceptable_stale)
            return;

        if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
            return;

        last_fast_cycle = start;
    }

    /* We usually should test CRON_DBS_PER_CALL per iteration, with
     * two exceptions:
     *
     * 1) Don't test more DBs than we have.
     * 2) If last time we hit the time limit, we want to scan all DBs
     * in this iteration, as there is work to do in some DB and we don't want
     * expired keys to use memory for too much time. */
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

    /* We can use at max 'config_cycle_slow_time_perc' percentage of CPU
     * time per iteration. Since this function gets called with a frequency of
     * server.hz times per second, the following is the max amount of
     * microseconds we can spend in this function. */
    timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = config_cycle_fast_duration; /* in microseconds. */

    /* Accumulate some global stats as we expire keys, to have some idea
     * about the number of keys that are already logically expired, but still
     * existing inside the database. */
    long total_sampled = 0;
    long total_expired = 0;

    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        /* Expired and checked in a single loop. */
        unsigned long expired, sampled;

        redisDb *db = server.db+(current_db % server.dbnum);

        /* Increment the DB now so we are sure if we run out of time
         * in the current DB we'll restart from the next. This allows to
         * distribute the time evenly across DBs. */
        current_db++;

        /* Continue to expire if at the end of the cycle there are still
         * a big percentage of keys to expire, compared to the number of keys
         * we scanned. The percentage, stored in config_cycle_acceptable_stale
         * is not fixed, but depends on the Redis configured "expire effort". */
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
            iteration++;

            /* If there is nothing to expire try next DB ASAP. */
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            slots = dictSlots(db->expires);
            now = mstime();

            /* When there are less than 1% filled slots, sampling the key
             * space is expensive, so stop here waiting for better times...
             * The dictionary will be resized asap. */
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;

            /* The main collection cycle. Sample random keys among keys
             * with an expire set, checking for expired ones. */
            expired = 0;
            sampled = 0;
            ttl_sum = 0;
            ttl_samples = 0;

            if (num > config_keys_per_loop)
                num = config_keys_per_loop;

            /* Here we access the low level representation of the hash table
             * for speed concerns: this makes this code coupled with dict.c,
             * but it hardly changed in ten years.
             *
             * Note that certain places of the hash table may be empty,
             * so we want also a stop condition about the number of
             * buckets that we scanned. However scanning for free buckets
             * is very fast: we are in the cache line scanning a sequential
             * array of NULL pointers, so we can scan a lot more buckets
             * than keys in the same time. */
            long max_buckets = num*20;
            long checked_buckets = 0;

            while (sampled < num && checked_buckets < max_buckets) {
                for (int table = 0; table < 2; table++) {
                    if (table == 1 && !dictIsRehashing(db->expires)) break;

                    unsigned long idx = db->expires_cursor;
                    idx &= db->expires->ht[table].sizemask;
                    dictEntry *de = db->expires->ht[table].table[idx];
                    long long ttl;

                    /* Scan the current bucket of the current table. */
                    checked_buckets++;
                    while(de) {
                        /* Get the next entry now since this entry may get
                         * deleted. */
                        dictEntry *e = de;
                        de = de->next;

                        ttl = dictGetSignedIntegerVal(e)-now;
                        if (activeExpireCycleTryExpire(db,e,now)) expired++;
                        if (ttl > 0) {
                            /* We want the average TTL of keys yet
                             * not expired. */
                            ttl_sum += ttl;
                            ttl_samples++;
                        }
                        sampled++;
                    }
                }
                db->expires_cursor++;
            }
            total_expired += expired;
            total_sampled += sampled;

            /* Update the average TTL stats for this database. */
            if (ttl_samples) {
                long long avg_ttl = ttl_sum/ttl_samples;

                /* Do a simple running average with a few samples.
                 * We just use the current estimate with a weight of 2%
                 * and the previous estimate with a weight of 98%. */
                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
            }

            /* We can't block forever here even if there are many keys to
             * expire. So after a given amount of milliseconds return to the
             * caller waiting for the other active expire cycle. */
            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
            /* We don't repeat the cycle for the current database if there are
             * an acceptable amount of stale keys (logically expired but yet
             * not reclaimed). */
        } while (sampled == 0 ||
                 (expired*100/sampled) > config_cycle_acceptable_stale);
    }

    elapsed = ustime()-start;
    server.stat_expire_cycle_time_used += elapsed;
    latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);

    /* Update our estimate of keys existing but yet to be expired.
     * Running average with this sample accounting for 5%. */
    double current_perc;
    if (total_sampled) {
        current_perc = (double)total_expired/total_sampled;
    } else
        current_perc = 0;
    server.stat_expired_stale_perc = (current_perc*0.05)+
                                     (server.stat_expired_stale_perc*0.95);
}

在这里插入图片描述

tip: Redis服务器为了保证过期删除策略不会导致线程卡死,会给过期扫描.增加了最大执行时间为25ms

●当Redis的内存超过最大允许的内存,Redis 会触发内存淘汰策略
这和过期策略是完全不同的两个概念,这两者- -个是在正常情况下清除过期键
●一个是在非正常情况下为了保证Redis顺利运行的保护策略
当Redis内存不够用时,Redis 服务器会根据服务器设置的淘汰策略
删除一些不常用的数据,以保证Redis服务器的顺利运行

Redis内存淘汰策略在这里插入图片描述
当前Redis服务器设置的是“noeviction” 类型的内存淘汰策略
那么这表示什么含义呢? Redis又有几种内存淘汰策略呢?

  • noeviction 不淘汰任何数据 ,当执行新增操作的时候报错
  • allkeys-lru 淘汰整个键值中最久未使用的键值
  • allkeys-random 随机淘汰任意键
  • volatile-lru 淘汰所有设置了过期时间的键值中最久未使用的键值
  • volatile-random 随机淘汰设置了过期时间的键值
  • volatile-ttl 优先淘汰更早过期的键值

从以上内存淘汰策略中可以看出,allkeys-xxx表示从所有的键值中淘汰数据
volatile- xxx表示从设置了过期键的键值中淘汰数据

●这个内存淘汰策略我们可以通过配置文件来修改
redis.conf对应的配置项是“maxmemory-policy noeviction”
只需要把它修改成我们需要设置的类型即可
●还有另一种简单的修改内存淘汰策略的方式
使用命令行工具输入“config set maxmemory-policy noeviction”来修改内存淘汰的策略
但是每次重启后 这种方法设置的缓存策略就会丢失.
在这里插入图片描述

  • LRU ( Least Recently Used)淘汰算法
    一种常用的页面置换算法,也就是说最久没有使用的缓存将会被淘汰.LRU是基于链表结构实现的链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头当需要进行内存淘汰时,只需要删除链表尾部的元素即可.

  • Redis使用的是一-种近似LRU算法,目的是为了更好的节约内存它的实现方式是给现有的数据结构添加一个额外的字段,用于记录此键值的最后一次访问时间.

  • Redis内存淘汰时,会使用随机采样的方式来淘汰数据 它是随机取5个值(此值可配置),然后淘汰最久没有使用的数据

  • LFU (Least Frequently Used)淘汰算法 最不常用的算法是根据总访问次数来淘汰数据的.它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”

  • LFU相对来说比LRU更“智能”,因为它解决了使用频率很低的缓存 只是最近被访问了一次就不会被删除的问题

  • 如果是使用LRU类似这种情况数据是不会被删除的,而使用LFU的话,这个数据就会被删除

  • Redis内存淘汰策略使用了LFU和近LRU的淘汰算法 具体使用哪种淘汰算法,要看服务器是如何设置内存淘汰策略的. 也就是要看“maxmemory-policy” 的值是如何设置的

Redis 如何实现一个分布式锁

●分布式锁是控制分布式系统之间同步访问共享资源的-种方式
为了解决分布式系统中,不同的系统或是同-一个系统的不同主机共享同一个资源的问题
●它通常会采用互斥来保证程序的- -致性,这就是分布式锁的用途以及执行原理
在这里插入图片描述

分布式锁的常见实现方式有四种:

●基于MySQL的悲观锁来实现分布式锁,这种方式使用的最少.因为这种实现方式的性能不好,且容易造成死锁
●基于Memcached实现分布式锁,可使用add方法来实现.如果添加成功了则表示分布式锁创建成功
●基于Redis实现分布式锁,可以使用setnx方法来实现
●基于ZooKeeper实现分布式锁 利用ZooKeeper顺序临时节点来实现

效率分析

●由于MySQL的执行效率问题和死锁问题,所以这种实现方式会被我们先排除掉.而Memcached和Redis的实现方式比较类似
●但因为Redis技术比较普及,所以会优先使用Redis来实现分布式锁
而ZooKeeper确实可以很好的实现分布式锁
●但此技术在中小型公司的普及率不高,尤其是非Java技术栈的公司使用的较少,如果只是为了实现分布式锁而重新搭建一套ZooKeeper集群.显然实现成本和维护成本太高.

所以使用redis实现分布式锁的方式最多.

●之所以可以使用以上四种方式来实现分布式锁
是因为以.上四种方式都属于程序调用的“外部系统”
而分布式的程序是需要共享‘“外部系统” ,这就是分布式锁得以实现的基本前提

锁的分类

程序中使用的锁叫单机锁,我们日常中所说的“锁”都泛指单机锁
其分类有很多,大体可分为以下几类:

  • 悲观锁,是数据对外界的修改采取保守策略,它认为线程很容易把数据修改掉 因此在整个数据被修改的过程中都会采取锁定状态,直到一个线程使用完,其他线程才可以继续使用,典型应用是synchronized
  • 乐观锁,和悲观锁的概念恰好相反,乐观锁认为一般情况下数据在修改时不会出现冲突,所以在数据访问之前不会加锁,只是在数据提交更改时,才会对数据进行检测 典型应用是ReadWriteLock读写锁
  • 可重入锁,也叫递归锁,指的是同一个线程在外面的函数获取了锁之后那么内层的函数也可以继续获得此锁,在Java语言中ReentrantLock和synchronized都是可重入锁
  • 独占锁和共享锁,只能被单线程持有的锁叫做独占锁,可以被多线程持有的锁叫共享锁,独占锁指的是在任何时候最多只能有一个线程持有该锁,比如ReentrantLock就是独占锁,而ReadWriteLock读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁
  • 单机锁之所以不能应用在分布式系统中是因为在分布式系统中 每次请求可能会被分配在不同的服务器上,而单机锁是在单台服务器上生效的

设置锁和删除锁

127.0.0.1:6379> SETNX locl true;
(integer) 1
127.0.0.1:6379> del locl
(integer) 1
127.0.0.1:6379>

当其他程序setnx失败时,则表示此锁正在使用中这样就可以实现简单的分布式锁了.
但是以.上代码有一个问题,就是没有设置锁的超时时间,因此如果出现异常情况,会导致锁未被释放而其他线程又在排队等待此锁就会导致程序不可用.

127.0.0.1:6379> setnx locl true
(integer) 1
127.0.0.1:6379> setnx locl true
(integer) 0
127.0.0.1:6379> ttl locl
(integer) -1
127.0.0.1:6379> EXPIRE locl 50
(integer) 1
127.0.0.1:6379> ttl locl
(integer) 46
# -2 表示key 不存在 说明已经被删除了,-1 说明key永远不过期 
127.0.0.1:6379> ttl locl
(integer) -2

此方法也有一个问题 setnx lock true和expire lock 30命令是非原子的
也就是一个执行完另一一个才能执行
如果在setnx命令执行完之后,发生了异常情况那么就会导致expire命令不会执行.因此依然没有解决死锁的问题

使用一条命令 set locl true ex 30 nx 来设置key的值和判断是否存在

127.0.0.1:6379> set locl true ex 30 nx
OK
127.0.0.1:6379> ttl locl
(integer) 18
127.0.0.1:6379> 

ex是用来设置超时时间的,而nx是not exists 的意思,用来判断键是否存在
如果返回的结果为“OK” 则表示创建锁成功,否则表示此锁有人在使用

127.0.0.1:6379> set locl true ex 30 nx
OK
127.0.0.1:6379> get locl
"true"
127.0.0.1:6379> set locl true ex 30 nx
(nil)
127.0.0.1:6379> ttl locl
(integer) 8
127.0.0.1:6379> set locl true ex 30 nx
(nil)
127.0.0.1:6379> ttl locl
(integer) -2
127.0.0.1:6379> set locl true ex 30 nx
OK
127.0.0.1:6379> ttl locl
(integer) 26
127.0.0.1:6379> 

注意
使用set命令之后好像一切问题都解决了,但在这里我要告诉你,其实并没有例如,我们给锁设置了超时时间为10s,但程序的执行需要使用15s,那么在第10s时此锁因为超时就会被释放,这时候线程二在执行set命令时正常获取到了锁,于是在很短的时间内2s之后删除了此锁,这就造成了锁被误删的情况
在这里插入图片描述
在这里插入图片描述

锁超时可以通过两种方案来解决:
●把执行耗时的方法从锁中剔除,减少锁中代码的执行时间,保证锁在超时之前,代码一定可以执行完
●把锁的超时时间设置的长一些,正常情况下我们在使用完锁之后,会调用删除的方法手动删除锁
因此可以把超时时间设置的稍微长- -些

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值