文章目录

  • 一.引言
  • 二.Redis数据存活时间的实现
  • 1.过期时间的存储
  • 2.过期时间的格式
  • 3.其他的函数
  • 三.Redis数据删除策略
  • 1.过期数据的删除策略
  • 2.未过期数据的删除策略
  • 四.数据删除相关知识
  • 1.数据共享
  • 2.数据删除的流程
  • 五.总结


一.引言

在Redis中存储的数据可以拥有一个存活时间,在存活时间内的数据可以被使用,当过了存活时间就会被删除。存活时间这一功能让Redis中的数据具有了新陈代谢的能力,提高了数据库的可靠性,也降低了数据的冗余。
本文会从存活时间功能的实现,数据删除的策略和删除操作相关知识点三个方面简单聊这一块内容。

二.Redis数据存活时间的实现

1.过期时间的存储

创建一条记录,Redis会把插入的数据保存到redisDB结构体的dict指针指向的内存区域中(也就是键空间)。dict是一个hash表,通过key/value的形式存储着该数据库的所有数据。

在redisDB结构体中还有一个指针是expires,它也指向一个hash表结构的内存区域,这个hash表的key/value分别是dict保存数据的键和其对应的过期时间。

redis 存放map redis 存放的用户信息的时效_redis 存放map

2.过期时间的格式

每个过期时间数据标志该键的过期时间。在Redis中,过期时间存粗的格式只有一种,就是一个毫秒时间戳,表示达到该时间点时对应的记录就会过期。
虽然过期时间对的存储结构的只有毫秒时间戳一种,但是可以通过4个不同的函数去设置过期时间,分别是:

  • EXPIRE:给键设定一个单位为秒生存时间。
  • PEXPIRE:给键设定一个单位为毫秒的生存时间。
  • EXPIREAT:给键设定一个单位为秒的过期时间戳。
  • PEXPIREAT:给键设定一个单位为毫秒的过期时间戳。

四个不同的函数可以通过不同的方式为键设定过期时间,但是过期时间的存储格式都是毫秒时间戳,也就是和PEXPIREAT的参数一致。

这里大家应该也能猜出Redis是怎么处理这四种函数了吧。使用前三个函数设定过期时间时,Redis都会在函数内部进行时间的转化,然后调用PEXPIREAT函数为键设定一个过期时间。

举个例子:现在想给键为"key1"的数据设定一个过期时间。调用EXPIRE函数,传入参数"key1"和"100"。表示key1的存活时长为100秒。在EXPIRE函数内部会先把100秒换算为100000毫秒,调用PEXPIRE函数,参数为"key1"和"100000",然后在PEXPIRE内部会根据当前时间点和100000毫秒这个时间去计算出一个过期时间戳,最后调用PEXPIREAT函数,传入"key1"和一个毫秒单位的时间戳,PEXPIREAT函数把记录存储到expire的hash表中。

四个函数整体的转化规则如下:

redis 存放map redis 存放的用户信息的时效_redis 存放map_02

3.其他的函数

除了设置过期时间的四个函数,Redis还提供了一些操作过期时间的功能函数。下面列举这一块常用的函数。

  • TTL:返回指定键的生存时间
  • PTTL:返回指定键的过期时间戳
  • PERSIST:删除指定键的过期时间

三.Redis数据删除策略

1.过期数据的删除策略

定时删除
定时删除策略就是在给一个键设定过期时间的同时创建一个定时任务,该任务在键过期的时间点执行,会把相应的数据删除。
定时删除策略的优点是无需关心过期数据对内存的占用,在数据过期时定时任务会主动删除它,释放内存。但是定时删除策略的缺点也很明显,每一个有过期时间的键都创建定时任务,当数据库变得很庞大时,定时任务会变得非常多,带来很大的负担。

惰性删除

惰性删除策略在一条数据过期时不会主动删除它,在之后操作这条数据时判断它为过期数据并删除。使用了惰性删除策略之后,所有对数据操作的函数内部都会判断该数据是否过期,如果未过期会继续执行操作,否则不会执行操作并删除数据。流程图如下:

redis 存放map redis 存放的用户信息的时效_redis 存放map_03


惰性策略的优点是不需要额外的开销去保证键的及时清理和内存释放。缺点是会造成大量的数据积压在数据库中,在极端的情况下会导致内存泄露的问题。

定期删除
定期删除策略是定时删除策略的优化版,它不会给每个键创建定时任务,而是在一定时间间隔之后对数据库进行检查,删除已过期的数据。
定时删除的策略是周期性进行的,而且每次并不一定要把整个数据库都扫描一遍。在Redis中有DEFAULT_DB_NUMBERS,DEFAULT_KEY_NUMBERS,CURRENT_DB三个变量来控制定期删除函数的执行。

  • DEFAULT_DB_NUMBERS:每次扫描的数据库数量
  • DEFAULT_KEY_NUMBERS:每次扫描的键的数量
  • CURRENT_DB:当前扫描进度

每次定期删除策略函数执行时会扫描DEFAULT_DB_NUMBERS数量的数据库,对于一个数据库,定期删除执行函数会随机从中抽取DEFAULT_KEY_NUMBERS数量的键进行检查,在函数执行结束会把当前扫描完毕的库记录在CURRENT_DB中,下次执行时从CURRENT_DB记录的数据库开始即可。
定期删除策略是前两种删除策略的折中方案,它具备前两种方案的优点,开销不大,过期数据在内存中停留时间短。但是它也不能完全做到极小开销,数据过期时及时删除。

2.未过期数据的删除策略

什么情况下会删除未过期数据
Redis是内存数据库,内存的容量远小于硬盘,当Redis达到最大容量阈值时,会通过一些策略删除未过期数据(当然会先删除过期数据,当无过期数据可删除时才会删除未过期数据)。

空转时间
每一个value数据结构体中有个名为lru的变量用来标记这条数据最后被访问的时间。通过这个变量可以计算出这条数据的空转时间(闲置时间),在删除未过期数据时可以根据空转时间选择空闲时长最长的数据进行删除。

数据淘汰策略
删除未过期数据可以理解为淘汰一些已有数据,Redis中有以下几个淘汰数据策略:

  • volatile-ttl:在设置了过期时间的数据中挑选剩余存活时间最短的数据淘汰。
  • volatile-random:在设置了过期时间的数据中随机挑选数据淘汰。
  • volatile-lru:在设置了过期时间的数据中根据空转时间选择空闲时间最长的数据淘汰。
  • allkeys-random:在所有数据中随机淘汰。
  • allkeys-lru:在所有数据中根据空转时间选择空闲时间最长的数据淘汰
  • noeviction:不淘汰数据,达到最大内存后,如果需要申请更多的内存会报错。

四.数据删除相关知识

1.数据共享

为了更好的利用内存,Redis会进行共享数据的操作。比如有个键"key1"对于的值是"100"。接着又使用set命令插入一条数据"key2"对应"100",这时Redis会让"key2"也指向"key1"指向的内存空间,并标记"100"的refcount值(引用数量)为2。"key1"和"key2"共享同一个值。
Redis在启动时会创建0-9999的整数值,如果有插入的键对应的值在这其中就直接指向这些值,然后变更refcount。
除了0-9999意外的数据不会进行共享,这也是对性能的考虑,判定两个值是否相等需要的时间复杂度是0(n),如果允许所有的数据都功能,那么插入数据的性能会变得非常差,尤其是插入长字符串。

2.数据删除的流程

在一个数据库中,键是唯一的,值不是唯一的。删除一条数据时,首先会删除键记录,这样用户就无法访问到值了,保证了逻辑的合理,然后会根据refcount的值删除值记录。
Redis是根据引用计数法来记录一个值是否死亡(可被删除)的。在Java中判断值是否死亡用的是可达性分析法,因为引用计数法在Java会让循环引用的对象产生内存泄露问题。但是Redis中不存在循环引用的问题,因为只有0-9999可以共享,这就可以使用开销更小的引用计数法。

五.总结

与MySQL数据库不同,Redis这类内存数据库的载体是内存,内存小容量的特性使得它们必须要有更好的数据淘汰策略和持久化策略。
Redis在数据淘汰这一块做了很复杂的设计,有针对过期数据的,也有在内存不够用时针对未过期数据的。并且提供了多种淘汰数据的策略,层层递进,平衡时间开销和空间开销,使Redis数据库在不同的操作系统,不同状态下都能保证高可用性。