一、String是什么
Redis概念:Redis (REmote DIctionary Server) 是用 C 语言开发的一个开源的高性能键值对(key-value)数据库。
为什么会出现Redis呢?它的到来是为了解决什么样的问题?
Redis 是一个NOSQL类型数据库,是为了解决高并发、高扩展,大数据存储等一系列的问题而产生的数据库解决方案,是一个非关系型的数据库。
Redis中常见的数据类型我们一定要知道
如果你是redis的作者你会给redis设计什么数据类型呢?
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
我知道很抽象,为此整理了一个思维导图
Redis数据存储格式 Redis自身是一个 Map,其中所有的数据都是采用 key : value 的形式存储
数据类型指的是存储的数据的类型,也就是 value 部分的类型,key 部分永远都是字符串
Redis中string 类型就是字符串,它是Redist中最基本的数据对象,最大为512MB
存储的数据:单个数据,最简单的数据存储类型,也是最常用的数据存储类型
Key是String类型的,Value是Java中所有的基本类型都可以。
本篇主要详解string类型简单使用和业务场景
二、适用场景
- 缓存对象:例如可以用STRING缓存整个对象的]SON。
- 计数:Redis处理命令是单线程,所以执行命令的过程是原子的,因此String数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
- 分布式锁:可以利用SETNX1命令。
- 共享Session信息:服务器都会去同一个Redis2获取相关的Session信息,解决了分布式系统下Session存储的问题
Redis中string的使用场景根据它自身特点决定
常见的有如下几种情况,我简单举例说明
2-1 缓存功能
部分数据第一次查询查询数据库,查询完后存入redis中,后续再获取可以从redis中获取
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
如我们第一次访问 editor.csdn.net 这个域名,可能会对应这个IP 112.14.111.222的服务器,然后第二次访问,IP可能会变为112.13.121.219的服务器;负载均衡,一个域名对应多个服务器,将访问量分担到其他的服务器,这样很大程度的减轻了每个服务器上访问量
因为服务器都会有自己的会话session会导致用户每次刷新网页又要重新登录,为了解决这个问题,我们用redis将用户session集中管理,每次获取用户更新或查询登录信息都直接从redis中集中获取
这里的本质还是将某一个东西存入redis缓存中,和缓存功能类似,描述的是不同的应用场景
负载均衡:把众多的访问量分担到其他的服务器上,让每个服务器的压力减少
2-6 分布式锁
适用场景:在一个集群环境下,多个web应用时对同一个商品进行抢购和减库存操作时,可能出现超卖时会用到分布式锁
setnx key value //存入一个不存在的键值对,如果key不存在,同set;若存在,则不做任何操作
语法:SETNX key value 功能:当且仅当 key 不存在,将 key 的值设为 value ,并返回1; 若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
在实践的业务场景中:自己接触的项目中缓存,计数和存对象我使用过,其余的关于共享session,分布式锁的具体应用我暂时没有使用过,具体使用和实践代码可以参考更好的文章,初次学习理解的时候可能比较抽象,多看看图片会帮助我们理解,希望我的分享对能让你对Redis中的string存储模型有更深入的理解!
三、常用操作
- 创建 SET SETNX
- 查询 GET MGET
- 更新 SET
- 删除 DEL UNLINK
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
设置key-value,然后更加key获取value
批量获取、批量设置
获取数据字符个数(字符串长度)
strlen key
追加信息到原始信息后部(如果原始信息存在就追加,否则新建)
append key value
看看案例在手比较容易理解
del key [key...] //删除一个或多个键值对
setnx key value //存入一个不存在的键值对,如果key不存在,同set;若存在,则不做任何操作。
将key中存储的数字加1
INCR key
将key中存储的数字减1
DECR key
将key中所存储的值加上increment
INCRBY key increment
将key中所存在的值减去decrement
DECRBY key decrement
如上内容建议初学者在客户端实践
四、底层存储
4-1 底层三大编码
在 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示意图
4-1-3 RAW 编码
当存储的值为字符串,且长度小于等于 44 字节时,Redis 使用 raw 编码。
在 raw 编码中,String 对象的实际值会被存储在一个简单的字符串对象中,该对象包含了字符串的长度和字符数组的指针。这种编码方式的优点是存储空间小,且无需进行额外的解码操作。
这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了
RAW示意图
明明没有超过阈值,为什么变成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)。
看有关信息: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成员记录的数据的长度,因此可以正确保存图片等二进制数据。
五、小结
1:String 类型对象三种实现方式,int,embstr,raw
2:应用场景
Session利用redis做session共享内存。
自增和自减,用于做一些网站的请求数量,或者论坛的点赞数,评论数。最终会将这些统计数据放到硬盘中。
在功能中,除非是必要的情况,除了上述的这几个需求,尽量不要使用string类型,底层会浪费大量的内存空间。
3:如果使用embstr,每次最多开辟64个字节的空间,只有44个字节用于存储数据。
如果使用raw编码,则每次开辟空间都会留一些空间,如果数据成都变大了,则内存也会继续变大,进而浪费空间。而int只是针对数据是数值,只有整型才是int类型。
4:SDS 是Redis自己构建的一种简单动态字符串的抽象类型,并将 SDS 作为 Redis 的默认字符串表示。
感谢大家的观看!!!创作不易,如果觉得我写的好的话麻烦点点赞👍支持一下,谢谢!!!