(一)万字长文系列,看这篇就够了! —— Redis对象分析:String是什么?String的常用操作?String有哪些应用场景?String底层到底是怎么存储的?

1 String是什么

Redis概念:Redis (REmote DIctionary Server) 是用 C 语言开发的一个开源的高性能键值对(key-value)数据库。

为什么会出现Redis呢?它的到来是为了解决什么样的问题?

image.png

Redis 是一个NOSQL类型数据库,是为了解决高并发、高扩展,大数据存储等一系列的问题而产生的数据库解决方案,是一个非关系型的数据库。

Redis中常见的数据类型我们一定要知道

如果你是redis的作者你会给redis设计什么数据类型呢?

Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

我知道很抽象,为此整理了一个思维导图

image.png

Redis数据存储格式
Redis自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储

数据类型指的是存储的数据的类型,也就是 value 部分的类型,key 部分永远都是字符串


Redis中string 类型就是字符串,它是Redist中最基本的数据对象,最大为512MB

存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型

Key是String类型的,Value是Java中所有的基本类型都可以。

本篇主要详解string类型简单使用和业务场景

2 适用场景

image.png

  • 缓存对象:例如可以用STRING缓存整个对象的]SON。
  • 计数:Redis处理命令是单线程,所以执行命令的过程是原子的,因此String数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
  • 分布式锁:可以利用SETNX1命令。
  • 共享Session信息:服务器都会去同一个Redis2获取相关的Session信息,解决了分布式系统下Session存储的问题

Redis中string的使用场景根据它自身特点决定

常见的有如下几种情况,我简单举例说明

2-1 缓存功能

部分数据第一次查询查询数据库,查询完后存入redis中,后续再获取可以从redis中获取

image.png

2-2 验证码

网站登录中常有验证码,我们可以用此数据类型,手机号作为key,验证码作为value存储在redis中,设置过期时间,后续如果用户输入验证码,我们从redis中取值对比,如果过期则无效

set 13030303300 123456

2-3 数字计数

比如帖子有点赞数,可以以帖子的id作为key,点赞总数作为value; 还比如访问量等,用户每次访问,访问总数可以加一,记录在redis中; 抖音的关注数,当大V注册抖音的时候,关注数会在非常短的时间内增加,这里我们可以用redis记录,一段时间后同步到mysql等数据库中;

user-id:10086:fans → 123456

user-id:10086:blogs → 999

user-id:10086:likes → 888

2-4 存储对象

以json形式存储,常见key=id value=json格式数据,如商品id为key,商品信息为value

{“id”:10086,“name”:“不白要努力”,“fans”:123456,“blogs”:999, “likes”:888}

介绍一个之前接触过的案例:电影座位的排片,电影排片id为key,此场次座位信息为value,主要记录此场次的座位排布情况,场次座位以json形式存储在redis中,可以设置过期时间等同步应用到电影购票中,用户看到的座位从情况根据电影排片的key从Redis中取出

2-5 共享session

如我们第一次访问 https://editor.csdn.net 这个域名,可能会对应这个IP 112.14.111.222的服务器,然后第二次访问,IP可能会变为112.13.121.219的服务器;负载均衡,一个域名对应多个服务器,将访问量分担到其他的服务器,这样很大程度的减轻了每个服务器上访问量

image.png

因为服务器都会有自己的会话session会导致用户每次刷新网页又要重新登录,为了解决这个问题,我们用redis将用户session集中管理,每次获取用户更新或查询登录信息都直接从redis中集中获取

这里的本质还是将某一个东西存入redis缓存中,和缓存功能类似,描述的是不同的应用场景

负载均衡:把众多的访问量分担到其他的服务器上,让每个服务器的压力减少

2-6 分布式锁

适用场景:在一个集群环境下,多个web应用时对同一个商品进行抢购和减库存操作时,可能出现超卖时会用到分布式锁

setnx key value //存入一个不存在的键值对,如果key不存在,同set;若存在,则不做任何操作

语法:SETNX key value
功能:当且仅当 key 不存在,将 key 的值设为 value ,并返回1;
若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

image.png

在实践的业务场景中:自己接触的项目中缓存,计数和存对象我使用过,其余的关于共享session,分布式锁的具体应用我暂时没有使用过,具体使用和实践代码可以参考更好的文章,初次学习理解的时候可能比较抽象,多看看图片会帮助我们理解,希望我的分享对能让你对Redis中的string存储模型有更深入的理解!

3 常用操作

  • 创建 SET SETNX
  • 查询 GET MGET
  • 更新 SET
  • 删除 DEL UNLINK

image.png

3-1 创建

SET

语法:SET key value

功能:设置一个key的值为特定的value,成功则返回OK。String对象的创建或者更新都是该命令。

set s1 v1

“OK”

SETNX

语法:SETNX key value

功能:用于在指定的key不存在时,为key设置指定的值,返回值0表示key存在不做操作,1表示设置成功。
如果对存在的Key,调用SETNX:

对已存在的key,调用setnx,插入失败

setnx s1 v1

(integer) 0

对不存在的key,调用setnx,插入成功

setnx s1 v2

(integer) 1

3-2 查询

GET

获取数据

get key

获取多个数据

mget key1 key2

3-3 更新

SET

set之前存在过的key会覆盖原值

set s1 v1

“OK”

3-4 删除

DEL

删除之前存在的值

del key

3-5 操作测试

建议初学者自己安装Redis测试,此处是截图demo

image.png
设置key-value,然后更加key获取value

image.png

批量获取、批量设置

image.png

获取数据字符个数(字符串长度)

strlen key

image.png

追加信息到原始信息后部(如果原始信息存在就追加,否则新建)

append key value

看看案例在手比较容易理解

image.png

del key [key…] //删除一个或多个键值对

image.png

setnx key value //存入一个不存在的键值对,如果key不存在,同set;若存在,则不做任何操作。

image.png

将key中存储的数字加1

INCR key

将key中存储的数字减1

DECR key

将key中所存储的值加上increment

INCRBY key increment

将key中所存在的值减去decrement

DECRBY key decrement

image.png

如上内容建议初学者在客户端实践

4 底层存储

4-1 底层三大编码

image.png

在 Redis 中,String 类型的数据结构并不是采用 C 语言中自带的字符串类型,C 语言中的数据结构存在很多问题,比如

  • 获取字符串长度的需要通过运算
  • 非二进制安全
  • 不可修改

对于不同的对象,Redis会使用不同的类型来存储。对于同一种类型type会有不同的存储形式encoding。对于string类型的字符串,其底层编码方式共有三种,分别为int、embstr和raw。其中, raw 和 embstr 类型,都是基于动态字符串(SDS)实现的

对于string类型的字符串,其底层编码方式共有三种,分别为int、embstr和raw。

  • int:当存储的字符串全是数字时,此时使用int方式来存储;
  • embstr:当存储的字符串长度小于44个字符时,此时使用embstr方式来存储;
  • raw:当存储的字符串长度大于44个字符时,此时使用raw方式来存储;

4-1-1 INT 编码

当存储的值为整数,且值的大小可以用 long 类型表示时,Redis 使用 int 编码。

在 int 编码中,String 对象的实际值会被存储在一个 long 类型的整数中。这种编码方式的优点是存储空间小,且无需进行额外的解码操作。( 只有整数才会使用int,如果是浮点数, Redis内部其实先将浮点数转化为字符串值,然后再保存)

命令示例:

set k1 123

Redis启动时会预先建立10000个分别存储0 - 9999的redisObject变量作为共享对象,这就意味着如果set字符串的键值在0~10000之间的话,则可以直接指向共享对象而不需要再建立新对象,此时键值不占空间

4-1-2 EMBSTR编码

当存储的值为字符串,且长度大于 44 字节时,Redis 使用 embstr 编码。在 embstr 编码中,String 对象的实际值会被存储在一个特殊的字符串对象中,该对象包含了字符串的长度和字符数组的指针,但是不包含额外的空间。这种编码方式的优点是存储空间小,且无需进行额外的解码操作,但是由于需要额外的内存分配,可能会影响性能。

EMBSTR顾名思义即:embedded string,表示嵌入式的String。从内存结构上来讲即字符串sds结构体与其对应的 redisObject对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样

EBSTR示意图
image.png

4-1-3 RAW 编码

当存储的值为字符串,且长度小于等于 44 字节时,Redis 使用 raw 编码。

在 raw 编码中,String 对象的实际值会被存储在一个简单的字符串对象中,该对象包含了字符串的长度和字符数组的指针。这种编码方式的优点是存储空间小,且无需进行额外的解码操作。

这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了

RAW示意图
image.png

明明没有超过阈值,为什么变成raw?

对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先 转化为 raw 再进行修改。因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44 个字节。

4-1-4 EMBSTR vs RAW

对于embstr和raw这两种encoding类型,其存储方式还不太一样。

对于embstr类型,它将RedisObject对象头和SDS对象在内存中地址是连在一起的,但对于raw类型,二者在内存地址不是连续的。具体参照上面的两张示意图

4-1-5 如何查看底层编码类型?

看类型 Type

命令用于返回 key 所储存的值的类型。

type key

返回 key 的数据类型,数据类型有:

  • none (key不存在)
  • string (字符串)
  • list (列表)
  • set (集合)
  • zset (有序集)
  • hash (哈希表)

看编码 object encoding

OBJECT ENCODING key

返回给定 key 锁储存的值所使用的内部表示(representation)。

image.png

看有关信息:Debug Object

Redis Debug Object 命令是一个调试命令,它不应被客户端所使用。

redis 127.0.0.1:6379> DEBUG OBJECT key
返回值:当 key 存在时,返回有关信息。 当 key 不存在时,返回一个错误。

4-2 SDS是个什么?

4-2-1 概述

Redis的数据类型都是Key-Value键值对,Key永远都是String类型,而我们常说的Redis五大数据类型是指的Value的类型。Redis没用使用传统的C风格字符串作为String的实现,而是自定义了SDS用来作为redis的默认字符串表示。Redis的SDS除了用于String数据的存储之外,还用作缓冲区,如AOF的缓冲区,客户端状态的输入缓冲区等。

4-2-2 底层实现

Redis3.0源码中SDS的实现(sds.h)

/*
 * 类型别名,用于指向 sdshdr 的 buf 属性
 */
typedef char *sds;
/*
 * 保存字符串对象的结构
 */
struct sdshdr {
    // buf 中已占用空间的长度
    int len;
    // buf 中剩余可用空间的长度
    int free;
    // 变长数组,存储数据空间
    char buf[];
};

SDS结构体中的 len + free 的长度是整个SDS字符串的空间大小 - 1,因为在字符串末尾填充了’\0’,这个填充的作用是为了让redis的字符串能兼容部分C语言字符串的API,起到代码重用。

使用object encoding key可以查看key对应的encoding类型:

  • len 保存了SDS保存字符串的长度
  • buf[] 数组用来保存字符串的每个元素
  • free 记录了 buf 数组中未使用的字节数量

4-2-3 源码实现

Redis的SDS的实现思想其实很类似于Cpp中的vector或者Java中的ArrayList,相信大家一看到这个结构就明白大概是如何进行操作的了。这里就不详细介绍SDS中的所有API,只说一下关键的要点,有需要的朋友可以查看sds.h和sds.c源码文件。

1.不使用结构体指针传递,而使用变长数组传递参数

不过在查阅Redis源码中关于SDS结构体传递有一个注意点。就是所有SDS的传递都是通过直接传递SDS结构体中变长数组buf的地址来传递的(注意SDS结构体定义上方的typedef char *sds)。

那么只通过buf数据的地址如何得知整个结构体的数据呢?

C语言的变长数组的大小是不计入结构体的大小中的,因为数组名实际上不是指针,它就是个地址偏移。并且变长数组的地址是连续衔接在结构体的后方。那么我们使用数组的首地址减去结构体的大小,就得到了结构体的首地址,就可以对结构体数据进行操作了。

struct sdshdr sh = (void)(s-(sizeof(struct sdshdr)));

2.底层数组扩容规则

当我们对SDS的字符串进行添加操作的时候,首先会判断当前剩余的长度是否足够,如果足够则不进行扩容,则进行扩容。(对应zmalloc.c文件中的zrealloc函数底层实际上使用realloc实现)

void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZE
    void *realptr;
#endif
    size_t oldsize;
    void *newptr;

    if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZE
    oldsize = zmalloc_size(ptr);
    newptr = realloc(ptr,size);
    if (!newptr) zmalloc_oom_handler(size);

    update_zmalloc_stat_free(oldsize);
    update_zmalloc_stat_alloc(zmalloc_size(newptr));
    return newptr;
#else
    realptr = (char*)ptr-PREFIX_SIZE;
    oldsize = *((size_t*)realptr);
    newptr = realloc(realptr,size+PREFIX_SIZE);
    if (!newptr) zmalloc_oom_handler(size);

    *((size_t*)newptr) = size;
    update_zmalloc_stat_free(oldsize);
    update_zmalloc_stat_alloc(size);
    return (char*)newptr+PREFIX_SIZE;
#endif
}

4-2-4 扩容规则

当扩容之后的newlen小于1MB的时候,多分配和newlen大小相同的冗余空间,扩容为 2 * newLen + 1的大小,即SDS的结构体的成员 len == free ==newLen
当扩容之后的newLen大于1MB的时候,则多分配1MB的空间,即扩容为 newLen + 1MB + 1的大小。

4-2-5 SDS相比于C原生存储有什么好处?

使用O(1)的时间获取字符串长度

因为SDS结构体中存储了字符串的长度,因此在获取字符串长度的时候无需调用strlen函数,直接就可以获取到。

防止缓冲区溢出

传统C语言的字符串拼接函数strcat(dest,source),需要我们程序员保证dest的空间足以容下拼接后的字符串长度,而SDS的free字段记录了当前SDS还有多少可用的空间。如果空间足够则直接拷贝内容,不足则先进行扩容,再执行操作。

减少字符串修改带来的内存分配次数

空间预先分配,SDS的空间总是预先分配足够大小的空间,防止String修改频繁申请和释放空间

惰性空间释放,当程序需要减少SDS的字符串长度的时候,redis并不会直接释放多余的空间,而是使用free字段进行记录,以便下次增加长度时候使用。当然不用担心这部分空间的冗余,如果有需要的话,redis底层会回收这段空间。

SDS是二进制安全

传统C字符串以’\0’作为字符串的结束标志,但是二进制流等数据中可能就会包含’\0’等特殊字符,使用传统C的字符串会导致数据识别失败。而SDS采用len成员记录的数据的长度,因此可以正确保存图片等二进制数据。

5:小结

1:String 类型对象三种实现方式,int,embstr,raw

2:应用场景

Session利用redis做session共享内存。

自增和自减,用于做一些网站的请求数量,或者论坛的点赞数,评论数。最终会将这些统计数据放到硬盘中。

在功能中,除非是必要的情况,除了上述的这几个需求,尽量不要使用string类型,底层会浪费大量的内存空间。

3:如果使用embstr,每次最多开辟64个字节的空间,只有44个字节用于存储数据。

如果使用raw编码,则每次开辟空间都会留一些空间,如果数据成都变大了,则内存也会继续变大,进而浪费空间。而int只是针对数据是数值,只有整型才是int类型。

4:SDS 是Redis自己构建的一种简单动态字符串的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。


image.png

感谢大家的观看!!!创作不易,如果觉得我写的好的话麻烦点点赞👍支持一下,谢谢!!!

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值