百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

蜻蜓点水说说Redis的String的奥秘

mhr18 2024-11-21 17:53 24 浏览 0 评论

作 者: CodeBear

原文链接:https://www.cnblogs.com/CodeBear/p/13385727.html

如果面试官问你,单线程的Redis为什么那么快,你可能脱口而出,因为单线程,避免上下文切换;因为基于内存,比硬盘读写快很多;因为采用的是多路复用网络模型。不管你是否真的理解了,这个回答足以应付一半以上的面试官了,但是如果可以再进行补充就更好了:因为Redis对各种数据结构进行了精心的设计,比如String采用的是SDS,比如list采用的是ziplist,quicklist等等,可能这样的回答就比较出彩了,至少可以说出部分面试者不太清楚的事情。今天我们就来看看Redis中最常用的String数据结构的奥秘。

从位操作说起

bitmap的应用场景很多,比如大名鼎鼎的布隆过滤器(之前的博客有介绍过:《大白话布隆过滤器》),比如统计指定用户在一年内任意日期内的登录情况,统计任意日期内,所有用户的登录情况等等,都可以用bitmap来实现(之前的博客也有介绍过:《有点长的博客:Redis不是只有get set那么简单》),所以好好看看bitmap还是很有必要的,不过本篇博客不打算详细介绍bitmap,只是通过bitmap引出我们今天的话题,而bitmap的核心就是位操作。

如果我们要往Redis塞入一个value为“hello”的key,这个所有人都会:

test:1>set key hello
"OK"
test:1>get key
"hello"

如果我们要利用位操作实现这个需求呢?什么,我没听错把,位操作也可以实现这个需求吗?当然可以,因为在Redis中,String就是用byte数组来存储的。

什么,你不信?那请继续看下去。

要用位操作实现这个需求,我们要获得“hello”的ascii码,接着计算出二进制:

比如,“h”的ascii码是104,二进制是1101000:

"e"的ascii码是65,二进制是101,二进制是1100101:

然后形成如下的位图:

下面就需要利用位操作来进行设置:

test:1>setbit s 1 1
"0"
test:1>setbit s 2 1
"0"
test:1>setbit s 4 1
"0"
test:1>setbit s 10 1
"0"
test:1>setbit s 13 1
"0"
test:1>setbit s 9 1
"0"
test:1>setbit s 15 1
"0"

setbit的顺序可以随意调整,只要最终得到的位图是如上形式的就OK了。(我这里就调整了下seitbit的顺序,好吧,我承认其实我是打错了,又懒得再去打一遍,反正最终形成的位图是一样的)。

然后我们get一下:

test:1>get s
"he"

很神奇,有木有,这也说明了在Redis的底层,String就是一个数组,而且还是一个byte[]。

SDS

不管在什么编程语言、存储引擎中,String都是应用最广泛的,而在不同的编程语言、存储引擎中,String可能有不同的实现,在Redis中,String的底层就是SDS,它的全称是Simple Dynamic String。

Redis是C语言开发的,Redis为什么不直接利用C语言的字符串,而要“别出心裁”的自己构建SDS数据结构来实现字符串呢?

我们先来这个SDS是个什么鬼:

struct sdshdr {
    int len;
    int free;
    char buf[];
};

SDS的定义比较简单,只有3个字段,而且从字面上就可以看出是什么意思:

  • len:存储字符串的实际长度
  • free:存储剩余(空闲)的空间
  • buf[]:存储实际数据

下面我们就来看下SDS和C语言的字符串有什么区别:

  • 求字符串长度
    在C语言中,求字符串的长度只能遍历,时间复杂度是o(n),单线程的Redis表示鸭梨山大,但是现在引入了一个字段来存储字符串的实际长度,时间复杂度瞬间降低成了o(1)。
  • 二进制安全
    在C语言中,读取字符串遵循的是“遇零则止”,即,读取字符串,当读取到“\0”,就认为已经读到了结尾,哪怕后面还有字符串也不会读取了,像图片、音频等二进制数据,经常会穿插“\0”在其中,好端端的图片、音频就毁了...但是现在有了一个字段来存储字符串的实际长度,读取字符串的时候,先看下这个字符串的长度是多少,然后往后读多少位就可以了。
  • 缓冲区溢出
    字符串拼接是开发中常见的操作,C语言的字符串是不记录字符串长度的,一旦我们调用了拼接函数,而没有提前计算好内存,就会产生缓冲区溢出的情况,但是现在引入了free字段,来记录剩余的空间,做拼接操作之前,先去看下还有多少剩余空间,如果够,那就放心的做拼接操作,不够,就进行扩容。
  • 减少内存重分配次数
  1. 空间预分配:当对字符串进行拼接操作的时候,Redis会很贴心的分配一定的剩余空间,这块剩余空间现在看起来是有点浪费,但是我们如果继续拼接,这块剩余空间的作用就出来了。
  2. 惰性空间释放:当我们做了字符串缩减的操作,Redis并不会马上回收空间,因为你可能即将又要做字符串的拼接操作,如果你再次操作,还是没有用到这部分空间,Redis也会去回收这部分空间。

扩容策略

字符串小于1M,采用的是加倍扩容的策略,也就是多分配100%的剩余空间,当大于1M,每次扩容,只会多分配1M的剩余空间。

最大长度

Redis 规定字符串的长度不得超过 512M 字节。

embstr raw

Redis的字符串有两种存储方式,一种是embstr,一种是raw,当长度<=44,采用embstr 来存储:

set codebear abcdefghijklmnopqrstuvwxyz012345678912345678
"OK"
debug object codebear
"Value at:0x7f4050476880 refcount:1 encoding:embstr serializedlength:45 lru:1999016 lru_seconds_idle:36"

当长度>44,改用raw来存储:

set codebear abcdefghijklmnopqrstuvwxyz0123456789123456781
"OK"
debug object codebear
"Value at:0x7f404ac30100 refcount:1 encoding:raw serializedlength:46 lru:1999188 lru_seconds_idle:3"

网上也有一些博客说是以39为分界线,为什么会有两种答案呢?继续看下去就明白了。

我们先来看看Redis的对象头,查看:

#define LRU_BITS 24
typedef struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
} robj;

Redis对象头占 4bit+4bit+24bit+4byte+8byte(指针,在64 bit system下,占8byte)=32bit+12byte=4byte+12byte=16byte。

再来看看这两种存储形式有什么区别:

embstr的存储形式比较紧凑,Redis的对象头和SDS对象存在一起(连续)。

一般来说,在raw的存储形式下,Redis的对象头和SDS对象不存在一起(不连续)。

我们可以简单的理解为,一块内存的大小为64byte。

好了,前置内容介绍完毕了,我们来看看Redis3.0版本的SDS的定义,查看:

struct sdshdr {
    unsigned int len;
    unsigned int free;
    char buf[];
};

Redis对象头占了16byte,SDS对象的len和free又占了8byte,64-16-8=40,同时保存的字符串会以\0结尾,又占用了1byte,所以实际存储的字符串只能<=39位,所以在低版本的Redis下,embstr、raw的分界线为39。

再来看看Redis5.0版本的SDS的定义,查看:

可以看到变化很大,为什么要做那么大的改变?更节省内存,当字符串长度比较小的时候,会用sdshdr8来存储,len和alloc共占用2byte,flags占用1byte,\0结尾占用1byte,一共是4byte,64byte-16byte(对象头)-4byte=44byte,所以在高版本的Redis下,embstr、raw的分界线为44。

怎么样,没想到吧,我们Redis经常使用的String竟然牵扯到那么多东西,而这些东西就可以区分平庸开发和优秀开发,成为一个优秀的开发,要学习的东西还有很多很多。

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: