Redis的LRU实现和应用

在编程中,计数器是一种基本但强大的工具,用于跟踪和管理数据和资源。本文将深入探讨不同类型的计数器的应用,从redis的lru(最近最少使用)缓存淘汰算法的实现,到如何在内存受限的环境中有效地使用计数器,再到普通计数器的巧妙应用。
1. redis的lru实现
redis,作为一个高性能的键值存储系统,使用lru算法来决定淘汰哪些数据以释放内存。这个算法的关键在于跟踪每个对象最后一次被访问的时间,但为了节约内存,redis并不使用完整的时间戳。相反,它采用了一种巧妙的方法:
时间戳精度的简化:redis通过将时间戳除以一个固定的分辨率(例如1000毫秒)来降低其精度。
位数限制:redis进一步使用固定位数(例如24位)来存储这些简化的时间戳。
按位与操作:通过使用按位与操作确保计数器在达到最大值后自动从零开始,有效避免溢出。
这种方法在减少内存占用和保持时间戳更新效率之间取得了平衡,从而使lru算法的实现既高效又节省空间。
redis计算lru时间的源码如下(6.0.6)
#define lru_bits 24#define lru_clock_max ((1lru field of redisobject structures. */unsigned int getlruclock(void) { return (mstime()/lru_clock_resolution) & lru_clock_max;}  
在redis的lru算法实现中,使用24位来存储时间戳是一种权衡内存使用和精度的方法。下面我们分析这种方法的具体细节:
24位时间戳的最大值:
首先,24位可以表示的最大值是 (2^{24} - 1),即16777215。这是因为每个二进制位有两个可能的值(0或1),所以24位可以表示 (2^{24}) 个不同的值,从0开始计数,最大值就是 (2^{24} - 1)。
精度
redis中,时间戳的精度被降低到秒。这意味着每个时间戳表示从某个固定点(通常是unix纪元,即1970年1月1日0000 utc)开始的秒数。
相对时间点
使用24位存储时间戳,意味着能够表示的最大秒数是16777215秒。换算成更直观的时间单位:
天数:194天
小时数: 4655小时
因此,24位时间戳可以表示从某个起始点开始的大约194天内的任意秒。
位运算和精度
当时间戳的值超过24位可以表示的最大值时,由于按位与操作(与16777215按位与),时间戳将自动回绕到0。这意味着每过194天左右,时间戳就会重置。重置后的时间戳值是从0开始的,但这并不影响redis lru算法的有效性,因为该算法主要关心的是对象相对于彼此的“最近使用”状态,而不是绝对的时间点。
总结
所以,在redis的lru算法中,虽然时间戳的精度仍然是秒,但由于使用24位存储,它只能表示大约194天的时间跨度。一旦超过这个时间跨度,时间戳就会回绕。这种设计在维持足够精度的同时,大幅减少了每个对象的内存占用,非常适合于内存受限的高效缓存系统。
2. 内存效率和时间戳的近似表示
除了redis的应用,固定位数的计数器在其他许多场景中也非常有用,特别是在需要以内存高效的方式存储大量数据时。以下是两个具体的应用示例:
2.1 内存效率的例子
假设你正在开发一个高性能的日志处理系统,该系统需要跟踪数百万条日志记录的时间戳。如果使用完整的64位时间戳(精确到毫秒),对于每个日志记录,时间戳将占用8字节的存储空间。这在大量数据的情况下会导致巨大的内存消耗。
为了提高内存效率,你可以选择使用24位来表示时间戳。虽然这会减少时间的精度,但对于很多日志分析任务来说,这种精度已经足够。在这种情况下,每个时间戳只占用3字节的存储空间。
通过这种方法,你可以显著减少内存使用,同时仍然保留了足够的信息来进行有效的日志分析。
不节省内存情况
使用标准的64位时间戳(精确到毫秒)。
每个时间戳占用8字节。
总内存使用量 = 100万个事件 × 8字节/事件 = 800万字节(约7.63 mb)。
节省内存情况
使用24位时间戳(精确到某个更大的时间单位,比如分钟)。
每个时间戳占用3字节。
总内存使用量 = 100万个事件 × 3字节/事件 = 300万字节(约2.86 mb)。
节省效果
节省了约4.77 mb的内存。
对于某些应用(如日志分析),精确到分钟可能已足够,因此这种方法既节省内存又能提供所需的时间信息。
2.2 时间戳的近似表示
考虑一个网站缓存系统,它需要记录每个页面最后一次被访问的时间。通过将unix时间戳转换为以10分钟为单位的近似值,可以减少存储需求,同时仍然提供足够的信息来有效管理缓存。
下面是一个举例说明:
完整精度情况
使用完整的32位unix时间戳(精确到秒)。
时间戳示例:1617181723(代表2021年3月31日1423 utc)。
近似精度情况
使用简化的20位时间戳(以10分钟为单位)。
假设我们以2021年1月1日为基准,计算从那时起经过的10分钟间隔的数量。
时间戳示例:对于2021年3月31日14:15的时间,计算得到的20位时间戳可能是 99000(这是一个假设的值,具体取决于基准日期和计算方法)。
精度对比
完整精度时间戳能精确到秒。
近似精度时间戳精确到10分钟,对于缓存淘汰决策而言,这通常是足够的。例如,它可以用来判断一个页面是在最近一小时内被访问过,还是在更久之前。
3. 循环计数器的实际应用
利用固定位数和按位与操作实现高效的循环计数器
在编程中,经常需要跟踪特定的事件或状态的次数,尤其是在资源受限(如内存或存储空间有限)的环境中。传统的方法可能会涉及检查计数器是否达到某个值,然后手动将其重置。然而,这种方法既繁琐又容易出错。幸运的是,有一种更优雅、高效的方法可以实现同样的目标:使用固定位数的计数器结合按位与操作。
固定位数计数器的原理
固定位数计数器的概念很简单。就是选择一个特定的位数(比如16位、24位或32位)来存储计数器的值。这个选择直接决定了计数器的最大值,计数器的最大值为 (2^{位数} - 1)。例如,一个24位计数器的最大值是 (2^{24} - 1 = 16777215)。
为何选择按位与操作
按位与操作 (&) 是一种基本的位运算,它对两个数的每一位进行比较,只有当相同位置的两个位都为1时,结果的那位才为1。在这种用法中,它的作用是确保计数器值在达到其最大值后自动归零。
实现步骤
确定位数:首先,确定你需要的计数器位数。这将取决于你的特定应用和所需的最大计数范围。
计算掩码值:计算掩码值,即计数器的最大值。对于一个n位计数器,掩码值为 (2^n - 1)。
应用按位与:在增加计数器值时使用按位与操作,以确保计数器在达到最大值后自动回绕。
示例代码
假设我们使用一个16位计数器:
#include #define counter_bits 16#define counter_max ((1 << counter_bits) - 1)int main() { unsigned int counter = 0; for(int i = 0; i > 16) & 0xff; // 最高有效字节timestamp[1] = (value >> 8) & 0xff; // 中间字节timestamp[2] = value & 0xff; // 最低有效字节// 解析值uint32_t recovered_value = (timestamp[0] << 16) | (timestamp[1] << 8) | timestamp[2];这种方法给予了你更多控制,但增加了代码的复杂性。  
使用内置类型并接受一些空间浪费
有时,简单地使用标准的4字节整型(如 int 或 uint32_t)可能是更实际的选择,即使这意味着你不会完全利用所有的位。虽然这会浪费一些空间,但代码会更简单、更清晰,并且更容易维护。这在不是极度内存受限的环境下通常是可接受的。
选择方法
选择哪种方法取决于你的具体需求。如果内存效率至关重要,并且你能够处理额外的复杂性,那么使用位域或字节数组可能是合适的。如果代码的可读性和维护性更重要,那么简单地使用标准的整型类型可能是更好的选择。
链接:https://juejin.cn/post/7312035412159528969
(版权归原作者所有,侵删)
原文标题:从redis的lru实现到内存效率和位操作
文章出处:【微信公众号:马哥linux运维】欢迎添加关注!文章转载请注明出处。

美国大幅放缓对半导体公司聘用中国籍员工担任高级工程职位的审批
基于E820-DTU(2I2-433L)和E90-DTU(433C30)的水位监测应用
可穿戴设备如何脱颖而出?
机器和人类将继续合作的四个理由
预测全球电动汽车电池市场年增长率将超19%
Redis的LRU实现和应用
dfrobotVeyron25A直流有刷电机驱动器简介
语言进步推动人工智能发展
连衰六季,EDA产业营收终获连续性成长
ROHM开发出抗EMI性能的优异接地检测比较器——BA8290xYxxx-C系列
AWG510任意波形发生器参数AWG510
谁是iPhone8的最强对手?小米6:配置残暴+颜值逆天!
数据挖掘的四类方法
树莓派基金会正式发布了一个全新的版本树莓派4
智能电子屏的应用将给智能家居带来全新的体验
工业机器人之码垛机器人的应用优势
CrowdSupply项目:便携式、开源RISC-V SoC开发套件Precursor
国芯思辰|拍字节新型3D铁电存储器(VFRAM)PB85RS128C用于医用额温枪,写入周期达100万次
智能传感器的种类和结构/特点/作用/实现
研究者如何研究SLS和IS的肿瘤免疫微环境