最全【 Redis五大数据类型实现原理】(1),2024年最新这些新技术你们都知道吗

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

//编码:比如字符串的编码有int编码,embstr编码,raw编码

unsigned encoding:4;

//指向底层数据结构的指针,prt是个指针变量,存放地址,指向数据存储的位置

void *ptr;

//引用计数,类似java里的引用计数

int refcount;

//记录最后一次被程序访问的时间

unsigned lru:22;

}robj

type属性

redisObject 对象的type属性记录了对象的类型(string,list,hash,set,zset),可以通过type key命令来判断对象类型,从而区分redis中key-value的类型

127.0.0.1:6379> set testString testValue

OK

127.0.0.1:6379> lpush testList testValue1 testValue2 testValue3

(integer) 3

127.0.0.1:6379> hmset testhash 1:testvalue 2:testvalue2

OK

127.0.0.1:6379> sadd testset testvalue

(integer) 1

127.0.0.1:6379> zadd testzset 1 testvalue

(integer) 1

127.0.0.1:6379> type testString

string

127.0.0.1:6379> type testList

list

127.0.0.1:6379> type testhash

hash

127.0.0.1:6379> type testset

set

127.0.0.1:6379> type testzset

zset

prt和encoding属性

redisObject 对象的 prt 指针,存放数据的地址,指向对象底层的数据结构,通过它可以找到数据的位置。

refcount 属性

由于C语言跟贴近操作系统,直接跟操作系统交互,命令执行响应比较快,所以Redis选择C语言进行编写可以提高性能,但是C 语言不具备自动回收内存功能,于是乎Redis自己构建了一个内存回收机制。

创建一个新对象,redisObject 对象中的refcount属性就会加1,对象被一个新程序使用,调用incrRefCount函数进行加 1,如果有对象不再被应用程序使用了,那么它就会调用decrRefCount函数进行减 1,当对象的引用计数值为 0 的时候,那么这个对象所占用的内存就会被释放。

从这里可以看出来,这其实就是Java虚拟机中引用计数的内存回收机制,在Java中这种回收机制不被使用,因为它不能解决循环引用的问题。

循环引用举例:A引用B,B引用C,C引用A。

Redis通过在配置文件中修改相关的配置,来达到解决循环引用的问题,在Redis的配置文件里,Windows的配置文件是redis.windows.conf,Linux系统的配置文件是redis.conf。

在配置文件中有一个配置:maxmemory-policy,当内存使用达到最大值时,redis使用的清楚策略,默认配置是noeviction

1)volatile-lru 删除已有的过期时间的key

2)allkeys-lru 删除所有的key

3)volatile-random 已有过期时间的key 随机删除

4)allkeys-random 随机删除key

5)volatile-ttl 删除即将过期的key

6)noeviction 不删除任何key,只是返回一个写错误,这个是默认选项 对于整数值的字符串对象(例如:1,2,3这种的)可实现内存共享。

问题:什么是内存共享?

定义:键不同,值相同。

举例:输入命令set key1 1024,键为 key1,值为1024的字符串对象,接着输入命令 set key2 1024 ,键为 key2,值为1024 的字符串对象。这个时候,有二个不同的键,一个相同的值。

实现原理:键的值,指针指向一个有值的对象,被共享的值对象引用refcount 加 1。

局限性:判断两个对象是否相等需要消耗运算的额外的时间。整数值,判断操作复杂度低;普通字符串,判断复杂度相比较而已是高的;哈希、列表、集合和有序集合,判断的复杂度更高,所以内存共享只适用于整数值的字符串。

lru 属性

Lru属性是redisObject 记录对象最后一次被命令程序访问的时间,用来辅助lru算法删除过期内存的。

在Redis 配置文件中有三个配置,最大内存配置 maxmemory,触发数据淘汰后的淘汰策略 maxmemory_policy,随机采样的精度maxmemory_samples。

当有条件符合配置文件中三个配置的时候,继续往Redis中加key时,会触发执行 lru 策略,进行内存清除。最近最少使用,lru算法根据数据的历史访问记录进行数据淘汰。

Lru策略的运行原理是数据插入到链表头部,当缓存数据被访问之后,数据会移到链表头,链表满的时候,链表尾部的数据会被丢弃。

redis配置中的淘汰策略(maxmemory_policy)对应的值:

  • Noeviction:缓存里的数据超过maxmemory值,这个时候如果客户端正在执行命令,会让内存分配,给客户端返回错误响应
  • allkeys-lru: 所有的key都用LRU进行淘汰。
  • volatile-lru: LRU策略淘汰已经设置过过期时间的键。
  • allkeys-random:随机淘汰使用的。
  • key volatile-random:随机淘汰已设置过过期时间的key
  • volatile-ttl:只回收设置了过期时间的key

从redis缓存中淘汰数据,我们的需求是淘汰一些不可能被使用的数据,保留有些以后可能会频繁访问的数据,频繁访问的数据,将来被访问的可能性大很多,所以redis它记录每个数据的最后一次访问时间(lru记录的时间),通过当前时间减去键值对象lru记录的时间,最后可以计算出最少空闲时间,最少空闲时间的数据是最有可能被访问到,这就是LRU淘汰策略的设计思想,是不是很棒。

举例说明:

A数据每10s访问一次,B数据每5s访问一次,C数据每50s访问一次,|代表计算空闲时间的截止点。

在这里插入图片描述

预测被访问的概率是B > A > C。

过期key的删除策略有两种:

惰性删除:每次获取键时,都检查键是否过期,过期的话,就删除该键;未过期,就返回该键。

定期删除:每隔一段时间,进行一次检查,删除里面的过期键。

encoding属性

数据结构由 encoding 属性,也就是编码,由它来决定,可以通过object encoding key命令查看一个值对象的编码。

127.0.0.1:6379> object encoding testString

“embstr”

127.0.0.1:6379> object encoding testList

“quicklist”

127.0.0.1:6379> object encoding testhash

“ziplist”

127.0.0.1:6379> object encoding testset

“hashtable”

127.0.0.1:6379> object encoding testzset

“ziplist”

String类型编码

我们最常使用的redis的一个数据类型就是String类型,实现单值缓存,分布式锁,计数器,分布式系统全局序列号等等功能。

它的底层编码分为三种,int,raw或者embstr。

int编码:存储整数值(例如:1,2,3),当 int 编码保存的值不再是整数值,又或者值的大小超过了long的范围,会自动转化成raw。例如:(1,2,3)->(a,b,c)

embstr编码:存储短字符串。

它只分配一次内存空间,redisObject和sds是连续的内存,查询效率会快很多,也正是因为redisObject和sds是连续在一起,伴随了一些缺点:当字符串增加的时候,它长度会增加,这个时候又需要重新分配内存,导致的结果就是整个redisObject和sds都需要重新分配空间,这样是会影响性能的,所以redis用embstr实现一次分配而后,只允许读,如果修改数据,那么它就会转成raw编码,不再用embstr编码了。

raw编码:用来存储长字符串。

它可以分配两次内存空间,一个是redisObject,一个是sds,二个内存空间不是连续的内存空间。和embstr编码相比,它创建的时候会多分配一次空间,删除时多释放一次空间。

版本区别:

embstr编码版本之间的区别:在redis3.2版本之前,用来存储39字节以内的数据,在这之后用来存储44字节以内的数据。

raw编码版本之间的区别:和embstr相反,redis3.2版本之前,可用来存储超过39字节的数据,3.2版本之后,它可以存储超过44字节的数据。

问题一:为什么是39字节?

从上面可以得知,embstr是一块连续的内存区域,由redisObject和sdshdr组成。

embstr最多占64字节场景:

redisObject占16个字节

struct RedisObject {

int4 type; // 4bits,不同的redis对象会有不同的数据类型(string、list、hash等),type记录类型,会用到4bits。

int4 encoding; // 4bits,存储编码形式,用4bits。

int24 lru; // 24bits,用24bits记录对象的LRU信息

int32 refcount; // 4bytes = 32bits,引用计数器,用到32bits

void *ptr; // 8bytes,64-bit system,指针指向对象的具体内容,需要64bits

}

计算: 4 + 4 + 24 + 32 + 64 = 128bits = 16bytes

sdshdr占48字节

struct sdshdr {

unsigned int len;//4个字节

unsigned int free;//4个字节

char buf[];//假设buf里面是39个字节

};

if (ptr) {

memcpy(sh->buf,ptr,len);

sh->buf[len] = ‘\0’;//一个字节

sdshdr的大小为8+39+1=48

那么一个embstr最多占64字节:16+48(4+4+1+39)=64

从2.4版本开始,redis用jemalloc内存分配器,比glibc的malloc要好一些,省内存,jemalloc会分配8,16,32,64等类型字节的内存。

embstr最小为33字节场景:

从上面我们可以得知redisObject占16个字节,现在buf中取8字节。

struct sdshdr {

unsigned int len;//4个字节

unsigned int free;//4个字节

char buf[];//假设buf里面是8个字节

};

if (ptr) {

memcpy(sh->buf,ptr,len);

sh->buf[len] = ‘\0’;//一个字节

sdshdr的大小为4+4+8+1=17

计算得出:16+17(4+4+1+8)=33

8,16,32都比33字节小,所以最小分配64字节。

通过对比:

16+17(4+4+1+8)=33

16+48(4+4+1+39)=64

当字符数大于8时,会分配64字节。当字符数小于39时,会分配64字节。这个默认39就是这样来的。

问题二:为什么分界值由39字节会变成44字节?

被暴打的回答是:REDIS_ENCODING_EMBSTR_SIZE_LIMIT值被换成了44了。

##define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39

##define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 44

正经的回答是:

每个sds都有一个sdshdr,里面的len和free记录了这个sds的长度和空闲空间。

struct sdshdr {

unsigned int len;

unsigned int free;

用的unsigned int可以表示很大的范围,短的sds空间被浪费了(unsigned int len和unsigned int free 8个字节)

commit之后,unsigned int 变成了uint8_t,uint16_t,uint32_t

struct attribute ((packed)) sdshdr8 {

uint8_t len; /* used */

uint8_t alloc; /* excluding the header and null terminator */

char flags; /* 2 lsb of type, and 6 msb of refcount */

char buf[];

};

struct attribute ((packed)) sdshdr16 {

uint16_t len; /* used */

uint16_t alloc; /* excluding the header and null terminator */

char flags; /* 2 lsb of type, and 6 msb of refcount */

char buf[];

};

struct attribute ((packed)) sdshdr32 {

uint32_t len; /* used */

uint32_t alloc; /* excluding the header and null terminator */

char flags; /* 2 lsb of type, and 6 msb of refcount */

char buf[];

};

struct attribute ((packed)) sdshdr64 {

uint64_t len; /* used */

uint64_t alloc; /* excluding the header and null terminator */

char flags; /* 2 lsb of type, and 6 msb of refcount */

除此之外还将原来的sdshdr改成了sdshdr16,sdshdr32,sdshdr64

sizes = sdscatprintf(sizes,“sdshdr:%d”, (int)sizeof(struct sdshdr));

改成了

sizes = sdscatprintf(sizes,“sdshdr8:%d”, (int)sizeof(struct sdshdr8));

sizes = sdscatprintf(sizes,“sdshdr16:%d”, (int)sizeof(struct sdshdr16));

sizes = sdscatprintf(sizes,“sdshdr32:%d”, (int)sizeof(struct sdshdr32));

sizes = sdscatprintf(sizes,“sdshdr64:%d”, (int)sizeof(struct sdshdr64));

unsigned int占四个字节

uint8_t 占1个字节

Char 占一个字节

我们通过计算可以得出为什么优化之后会多出5个字节了,短字符串的embstr用最小的sdshdr8。

sdsdr8 = uint8_t * 2 + char = 1*2+1 = 3

sdshdr = unsigned int * 2 = 4 * 2 = 8

这么一算是不是少了五个字节了,所以3.2版本更新之后,由于优化小sds的内存使用,使得原本39个字节可以多使用5个字节,这就变成了44字节了。

问题三:Redis字符串最大长度是多少?

512M,查看源码可知。

static int checkStringLength(redisClient *c, long long size) {

if (size > 51210241024) {

addReplyError(c,“string exceeds maximum allowed size (512MB)”);

return REDIS_ERR;

}

return REDIS_OK;

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

int checkStringLength(redisClient *c, long long size) {

if (size > 51210241024) {

addReplyError(c,“string exceeds maximum allowed size (512MB)”);

return REDIS_ERR;

}

return REDIS_OK;

[外链图片转存中…(img-RbsEsUuD-1715471411462)]
[外链图片转存中…(img-DpPMmKfm-1715471411462)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis 五大数据类型: 1. String:用于存储字符串、整数或者浮点数等类型的值,可以对字符串进行一些操作,如追加、截取等。 2. List:用于存储一个有序的字符串列表,可以对列表进行插入、删除、修改、查找等操作,还可以通过下标来访问列表中的元素。 3. Hash:用于存储一个字符串字段和字符串值之间的映射关系,可以对哈希表进行添加、删除、修改、查找等操作。 4. Set:用于存储一组字符串元素,可以对集合进行添加、删除、查找、交集、并集、差集等操作。 5. Sorted Set:用于存储一组有序的字符串元素,每个元素都有一个对应的分数,可以对有序集合进行添加、删除、查找、排序等操作。 下面是一个简单的Python代码示例,实现了一个旁路缓存功能,即在访问某个数据时,先在 Redis 中查找,如果 Redis 中不存在该数据,则从本地缓存中获取,如果本地缓存也不存在,则从数据库中获取,并将数据同时保存到本地缓存和 Redis 中: ```python import redis class Cache(object): def __init__(self, redis_client, local_cache): self.redis_client = redis_client self.local_cache = local_cache def get(self, key): value = self.redis_client.get(key) if value is not None: return value else: value = self.local_cache.get(key) if value is not None: self.redis_client.set(key, value) return value else: value = self.get_from_database(key) self.local_cache.set(key, value) self.redis_client.set(key, value) return value def get_from_database(self, key): # 查询数据库并返回数据 pass if __name__ == '__main__': redis_client = redis.Redis(host='localhost', port=6379, db=0) local_cache = {} cache = Cache(redis_client, local_cache) value = cache.get('key') ``` 持久化是指将 Redis 中的数据存储到磁盘中,以便在服务重启后可以恢复数据。Redis 支持两种持久化方式: 1. RDB:在指定的时间间隔内,将 Redis 中的数据集快照写入磁盘中,即生成一个 RDB 文件。RDB 文件是一个二进制文件,包含了 Redis 数据库的所有键值对。使用 RDB 持久化可以快速恢复大型数据集。可以通过配置文件中的 save 和 bgsave 指令来设置 RDB 持久化的规则。 2. AOF:将 Redis 中的写命令追加到 AOF 文件中,以便在服务重启后重放这些写命令,从而恢复 Redis 数据库中的数据。AOF 文件是一个文本文件,其内容为 Redis 的写命令。使用 AOF 持久化可以确保数据的完整性和可靠性。可以通过配置文件中的 appendonly 指令来开启 AOF 持久化,并通过 appendfsync 指令来设置 AOF 文件的同步策略。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值