Redis开发与运维:SDS(redis sds原理)
mhr18 2024-11-15 22:11 22 浏览 0 评论
STRING
我们会经常打交道的string类型,在redis中拥有广泛的使用。也是开启redis数据类型的基础。
在我最最开始接触的redis的时候,总是以为字符串类型就是值的类型是字符串。
比如:SET key value
我的理解是value数据类型是stirng类型,现在来看呢,这句话说得不够具体全面。
- 所有的键都是字符串类型
- 字符串类型的值可以是字符串、数字、二进制
这里也就引出了,另一个概念:外部类型和内部类型
外部类型 vs 内部类型
这里的外部类型,就是我们所熟知的:字符串(string)、哈希(hash)、列表(list)、集合(set)、有序结合(zset)等
Q1:那么什么是内部类型呢?
Q2:外部类型和内部类型是什么时候出现的?
Q3:为什么要这样设计?
我们先来看问题1,可以这样理解,对外数据结构就像是我们的API,对外提供着一定组织结构的数据。
对内来说,我们可以更换里面的逻辑算法,甚至更换数据存储方式,比如将Mysql换成Redis.
内部类型其实就是数据存储的形式。举现在我们所讨论的stirng来说。
string的外部类型就是string,而它对应的数据内部存储结构分为三种。
int:8个字节的长整形
embstr:<=39个字节的字符串(3.2 版本变成了44)
raw:>39个字节的字符串(3.2 版本变成了44)
所以,string类型会根据当前字符串的长度来决定到底使用哪种内部数据结构。
现在我们再回到问题上:什么是内部类型?
就是数据真正存储在内存上的数据结构。
其实第二个问题:外部类型和内部类型是什么时候出现的?
这里也算是有答案了,外部类型就是对外公开的数据类型也可以说是API,内部类型根据长度判断哪种内部结构。
第三个问题:为什么这样设计?
前后分离,如果有更好地内部数据类型,我们可以替换后面的数据类型,但不影响前面的Api.
还有一点也是根据不同情况,选择更好地数据结构,节省内存。毕竟是内存数据库,资源珍贵。
如何查看外部类型和内部类型
查看外部类型:type
127.0.0.1:6999[1]> SET sc sunchong // 对外类型:string
OK
127.0.0.1:6999[1]> type sc
string
127.0.0.1:6999[1]> HSET hsc sun chong // 对外类型:hash
(integer) 1
127.0.0.1:6999[1]> type hsc
hash
127.0.0.1:6999> RPUSH rsc s un ch hong
(integer) 4
127.0.0.1:6999> TYPE rsc
list
查看内部类型:object
int
127.0.0.1:6999[1]> set sc 1234567890123456789 // 对内类型:int
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 19
127.0.0.1:6999[1]> OBJECT encoding sc
"int"
int -> embstr
(int 8位的长整形,最大存储十进制位数为19位)
127.0.0.1:6999[1]> set sc 12345678901234567890 // 对内类型:embstr
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 20
127.0.0.1:6999[1]> OBJECT encoding sc
"embstr"
embstr -> raw
127.0.0.1:6999[1]> set sc 123456789012345678901234567890123456789
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 39
127.0.0.1:6999[1]> OBJECT encoding sc
"embstr"
127.0.0.1:6999[1]> set sc 12345678901234567890123456789012345678901
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 41
127.0.0.1:6999[1]> OBJECT encoding sc
"embstr"
额,这里我看《Redis 开发与运维》一书
39字节,embstr 转raw。写错了?
我的本机redis版本是5.0+,这本书是3.0,中间肯定是有了版本更新。
试试看看源码和提交记录 (https://github.com/antirez/redis/commit/f15df8ba5db09bdf4be58c53930799d82120cc34#diff-43278b647ec38f9faf284496e22a97d5)
继续尝试 embstr -> raw
127.0.0.1:6999[1]> set sc 12345678901234567890123456789012345678901234
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 44
127.0.0.1:6999[1]> OBJECT encoding sc
"embstr"
127.0.0.1:6999[1]> set sc 123456789012345678901234567890123456789012345 // 对内类型:raw
OK
127.0.0.1:6999[1]> STRLEN sc
(integer) 45
127.0.0.1:6999[1]> OBJECT encoding sc
"raw"
常用命令
set key value [EX seconds] [PX milliseconds] [NX|XX]
-- ex 秒级过期时间
-- px 毫秒级过期时间
-- nx 不存在才能执行成功,类似添加
-- xx 必须存在才能执行成功,类似修改
nx
127.0.0.1:6999[1]> EXISTS bus
(integer) 0
127.0.0.1:6999[1]> SET bus Q xx
(nil)
127.0.0.1:6999[1]> SET bus Q nx
OK
xx
127.0.0.1:6999[1]> EXISTS car
(integer) 0
127.0.0.1:6999[1]> SET car B
OK
127.0.0.1:6999[1]> SET car C nx
(nil)
127.0.0.1:6999[1]> SET car C xx
OK
127.0.0.1:6999[1]> GET car
"C"
setnx / setxx
这两个命令会逐步弃用
String类型源码分析
SDS 数据结构
为什么Redis要自己实现一套简单的动态字符串?
1. 效率
2. 安全(二进制安全:C语言中的字符串已 “\0” 为结束标志。)
3. 扩容
如果说,有一辆车,到站前提前告知车站乘客,本次列车还有多少余座。
此时,如果有个计数器可以计算一下当前坐了多少乘客,同时还有多少空位就好了。
这样司机师傅就不必每次停车上客前,数数还有多少座位可以坐。可以专心开车。
同样,Redis SDS 也使用了这样一些小小的记录,
使用时候获取这个记录,时间复杂度是O(1),效率是很高的。不用每次都去统计。
redis做了这样的设计:
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
len 已用字节数
free 未用字节数
buf[] 字符数组
这样设计有什么好处?1. 方便统计当前长度等,时间复杂度是O(1)2. 有了长度这些关键属性,可以不依赖“\0” 终止符。二进制安全。3. 指针返回的是buf[],这样可以复用C字符串相关的函数。避免重复造轮子,兼容C字符串操作4. 前面的len和free以及数组指针buf,内存分配上地址是连续的。所以很容易使用buf地址找到len和free.
我们先来看看,这个数据结构:
问题来了,是否还有优化的空间呢?
这样问比较笼统。我们思考一种场景:是不是所有的字符串存储都需要这样的结构?
到这里,有经验的你已经想到,所有的情况用没问题,但是Redis是内存数据库,
内存是资源,如何在资源上斤斤计较是Redis必须权衡的问题。
现在我们坐下来仔细分析一下:
unsigned int len 可以存的数据范围是:0 ~ 4294967295 (4 Bytes)
Redis中的字符串长度往往不需要这么大,多大合适呢?
1字节(Byte)? 这样?
struct sdshdr {
char len;
char free;
char buf[];
};
呀, 1字节是0~255,一般长度的字符串足够用。
如果真的存储了1个字节的字符串,len和free加起来也占了两个字节。
本来数据就1字节大,我为了存数据,额外信息都占2字节。
再优化,只能使用位来存储长度
假设,我们从全局来看,将字符串长度(小于1KB,1KB,2KB,4KB,8KB)来表示。
对于1字节,至少要拿出3个位,才能覆盖这5种情况( 2^3=8),那么剩下的5位才能存储长度。
现在我们已经进入到了Redis5.0 数据结构时代:
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
3个低位标识类型,5个高位存长度(2^5=32)
说到这,长度大于31('\0'结束符)的字符串,1个字节是存不下的。
我们还是按照之前的逻辑 len和free再结合刚才的按位看长度类型,来看看大于1字节的数据结构:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
使用了多少(len)、分配了多大(alloc)、长度类型标识(flags) ---- 这些表头= 1字节+1字节+1字节 ,共3字节
所以Redis对:字符串大小的界限就有了对应的宏定义
#define SDS_TYPE_5 0 // 小于1KB
#define SDS_TYPE_8 1
#define SDS_TYPE_16 2
#define SDS_TYPE_32 3
#define SDS_TYPE_64 4
对应的数据结构就是:
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
创建字符串
sds sdsnewlen(const void *init, size_t initlen);
看看注释,非常明白:
/* Create a new sds string with the content specified by the 'init' pointer
* and 'initlen'.
* If NULL is used for 'init' the string is initialized with zero bytes.
* If SDS_NOINIT is used, the buffer is left uninitialized;
*
* The string is always null-termined (all the sds strings are, always) so
* even if you create an sds string with:
*
* mystring = sdsnewlen("abc",3);
*
* You can print the string with printf() as there is an implicit \0 at the
* end of the string. However the string is binary safe and can contain
* \0 characters in the middle, as the length is stored in the sds header. */
获取SDS类型
char type = sdsReqType(initlen);
SDS_TYPE_5 一般用于字符串追加,所以还是用8这个。
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
获取头长度
int hdrlen = sdsHdrSize(type);
申请内存(头+数据体+终止符)
sh = s_malloc(hdrlen+initlen+1);
s=数据体buf[]指针
s = (char*)sh+hdrlen;
buf[]指针-1,就找到了长度类型flag
fp = ((unsigned char*)s)-1;
最后缀上结束符,然后返回的是buf[]指针,兼容C语言字符串
s[initlen] = '\0';
return s;
追加字符串
sds sdscatlen(sds s, const void *t, size_t len) {
size_t curlen = sdslen(s); // 原字符串长度
s = sdsMakeRoomFor(s,len); // 调整空间
if (s == NULL) return NULL; // 内存不足
memcpy(s+curlen, t, len); // 追加
sdssetlen(s, curlen+len); // 设置新的长度
s[curlen+len] = '\0'; // 结束标志,兼容C字符串
return s;
}
释放字符串
void sdsfree(sds s)
直接释放内存
if (s == NULL) return;
s_free((char*)s-sdsHdrSize(s[-1]));
}
为了避免频繁申请关释放内存, 把使用量len重置为0,同时清空数据
void sdsclear(sds s) {
sdssetlen(s, 0);
s[0] = '\0';
}
好处数据可以复用,避免重新申请内存
应用场景
用户信息
最近用户中心的访问压力极大,数据库已经扛不住。
我们使用比家里快而且成熟的技术,就是再加一层缓存。
比如:
uid:ui01
username: sunchong
nickname:二中
roletype:01
level:0
需求是:用户中心的用户数据,可以用uid拿到,也可以根据username拿到(uid和username 都是唯一不重复的)
我根据uid可以获取查询到用户,也可以根据username获取到用户。
首先,使用哈希进行数据的缓存 — HSET user:ui01 key1 value1 key2 value2 key3 value3 ...
127.0.0.1:6999> HSET user:ui01 username sunchong nickname 二中 roletype 01 level 0
(integer) 4
127.0.0.1:6999> HKEYS user:ui01
1) "username"
2) "nickname"
3) "roletype"
4) "level"
然后创建映射关系:
127.0.0.1:6999> SET user:sunchong ui01
OK
127.0.0.1:6999> GET user:sunchong
"ui01"
通过 username 找到主键uid,然后根据主键获取用户信息。
数据量较多时,过期时间设置为一定区间内的随机数。避免缓存穿透。
接口请求次数
当前我们有对用户开放的API,用户充值后使用,使用次数累加,剩余次数递减。
127.0.0.1:6999> SET user-ui01:times 1000
OK
127.0.0.1:6999> INCR user-ui01:times
(integer) 1001
127.0.0.1:6999> GET user-ui01:times
"1001"
127.0.0.1:6999> DECR user-ui01:times
(integer) 1000
短信验证码
就在前几天,我们刚刚对接了阿里云短信码服务。
起初,我自己认为短信验证码为了实时性不需要进行实际的缓存处理。
但是完全可以根据实际情况进行设计魂村策略。
为了防止接口的频繁调用,我们可以像网关一样进行设置。
现在就有这样一个需求:1个手机号,1分钟最多获取10次验证码
SET Catch:Limit:13355222226 1 ex 60 nx
初始化手机号,起始次数是1,默认过期时间60秒
再剩下的就是代码判断次数即可。
string redisConn = "127.0.0.1:6378,defaultDatabase=0,prefix=";
using (var redis = new CSRedis.CSRedisClient(redisConn))
{
string key = "Catch:13355222222";
bool inserted = redis.SetAsync(key, 1, 60, CSRedis.RedisExistence.Nx).Result;
if (inserted || redis.IncrByAsync(key).Result <= 10)
{
// 新增或未达上限--发送验证码
}
else
{
// 受限
}
}
总结
字符串类型结合命令有很多的应用场景,这个有待去收集和发现。
Redis 比较容易上手,文档全,代码整洁高效。
当然更需要我们去深入其运行原理,来更好使用这个工具来服务我们的业务。
相关推荐
- 说说Redis的单线程架构(redis的单线程模型)
-
一句话总结Redis采用单线程处理命令请求,避免了多线程的上下文切换和锁竞争,保证原子性操作。其基于内存的高效执行和I/O多路复用模型支撑了高并发性能。网络I/O和持久化操作(如RDB/AOF)由后台...
- 答记者问之 - Redis 的高效架构与应用模式解析
-
问:极客程序员你好,请帮我讲一讲redis答:redis主要涉及以下核心,我来一一揭幕Redis的高效架构与应用模式解析Redis是一个开源的内存数据存储系统,因其高性能、丰富的数据结构和易用性...
- Redis的5种核心数据结构,及其最经典的“应用场景”
-
Redis凭什么稳坐缓存界头把交椅?全靠这五个“身怀绝技”的数据结构!在分布式系统的江湖里,Redis就像一位身怀绝技的武林高手,而它的五大核心数据结构正是克敌制胜的五套绝学。今天咱们就来拆解这些独门...
- 精准定位文件包含漏洞:代码审计中的实战思维
-
前言最近看到由有分析梦想cms的,然后也去搭建了一个环境看了一看,发现了一个文件包含漏洞的点,很有意思,下面是详细的复现和分析,以后代码审计又多了一中挖掘文件包含漏洞的新思路环境搭建下载https...
- ARDM:一款国产跨平台的Redis管理工具
-
ARDM(AnotherRedisDesktopManager)是一款免费开源的Redis桌面管理客户端,支持Windows、Mac、Linux跨平台。功能特性ARDM提供的主要功能如...
- SpringBoot的Web应用开发——Web缓存利器Redis的应用!
-
Web缓存利器Redis的应用Redis是目前使用非常广泛的开源的内存数据库,是一个高性能的keyvalue数据库,它支持多种数据结构,常用做缓存、消息代理和配置中心。本节将简单介绍Redis的使...
- Windows服务器部署CRMEB开源电商系统,详细教程来了!
-
安装PHP已经安装过PHP的可以跳过首先安装VC运行库下载地址https://docs.microsoft.com/zh-cn/cpp/windows/latest-supported-vc-redi...
- Windows系统下Redis各个安装包介绍与选择指南
-
简介Redis作为高性能的键值数据库,广泛应用于缓存、消息队列等场景。在Windows系统中部署Redis时,用户可以选择多种安装包以满足不同的需求。本文将详细介绍以下Redis8.0.3版本的安装...
- 从面试题入手,深度剖析Redis Cluster原理
-
揭开RedisCluster的神秘面纱**在当今数字化浪潮中,数据量呈爆炸式增长,应用程序对数据存储和处理的要求也日益严苛。Redis作为一款高性能的内存数据库,凭借其出色的读写速度和丰富的数...
- 给大家推荐些好的c语言代码的网站
-
C语言,那就来推荐几个吧,部分含有C++:1、TheLinuxKernelArchives(kernel.org)Linux内核源码,仅限于C,但内核庞大,不太适合新手;2、redis(redi...
- Redis String 类型的底层实现与性能优化
-
RedisString是Redis中最基础也是应用最广泛的数据类型,它能存储文本、数字、二进制数据等多种形式的信息。深入理解其底层实现对构建高性能分布式系统至关重要。Redis字符串的底层结...
- 阿里面试问:Redis 为什么把简单的字符串设计成 SDS?
-
分享了一道面阿里的redis题,我看了以后觉得挺有意思。题目大致是这样的面试官:了解redis的String数据结构底层实现嘛?铁子:当然知道,是基于SDS实现的面试官:redis是用C语言开发的,那...
- 编程语言那么多,为何C语言能成为最成功的语言?
-
编程语言那么多,为何C语言能成为最成功的语言?2025年嵌入式岗位暴增47%,新人却还在问"C语言过时了吗"。真相是连机器人关节驱动都得靠它写,不会指针连芯片手册都看不懂。见过用Pyt...
- go-zero 使用 redis 作为 cache 的 2 种姿势
-
在go-zero框架内,如在rpc的应用service中,其内部已经预置了redis的应用,所以我们只需要在配置中加入相关字段即可,另外,在svcContext声明redisc...
- Redis事务深度解析:ACID特性、执行机制与生产实践指南
-
一、Redis事务的本质与核心机制Redis事务通过MULTI、EXEC、WATCH等命令实现,其本质是将多个命令序列化后一次性执行,而非传统数据库的严格事务模型。核心特点如下:命令队列化:MULT...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (83)
- php redis (97)
- redis 存储 (67)
- redis 锁 (74)
- 启动 redis (73)
- redis 时间 (60)
- redis 删除 (69)
- redis内存 (64)
- redis并发 (53)
- redis 主从 (71)
- redis同步 (53)
- redis 哨兵 (52)
- redis结构 (53)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)