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开发与运维:SDS的更多相关文章

  1. Redis开发与运维学习笔记

    <Redis开发与运维>读书笔记   一.初始Redis 1.Redis特性与优点 速度快.redis所有数据都存放于内存:是用C语言实现,更加贴近硬件:使用了单线程架构,避免了多线程竞争 ...

  2. Redis实战(七)Redis开发与运维

    Redis用途 1.缓存 Redis提供了键值过期时间设置, 并且也提供了灵活控制最大内存和内存溢出后的淘汰策略. 可以这么说, 一个合理的缓存设计能够为一个网站的稳定保驾护航. 2.排行榜系统 Re ...

  3. Redis 开发与运维

    Getting Start 高性能 性能优势的体现 C语言实现的内存管理 epoll的I/O多路复用技术+IO连接/关闭/读写通过事件实现异步的非阻塞IO TCP协议 单线程架构,不会因为高并发对服务 ...

  4. 《Redis开发与运维》快速笔记(一)

    1.前言&基本介绍 在原始的系统架构中,我们都由程序直接连接DB,随着业务的进一步开展,DB的压力越来越大,为了缓解DB的这一压力,我们引入了缓存,在程序连接DB中加入缓存层, 从而减轻数据库 ...

  5. 《Redis开发与运维》

    第1章 初识Redis 1. Redis介绍: Redis是一种基于键值对(key-value)的NoSQL数据库. 与很多键值对数据库不同的是,Redis中的值可以是由string(字符串).has ...

  6. Redis开发与运维:SDS与embstr、raw 深入理解

    对于上一篇文章,我又自己总结归纳并补充了一下,有了第二篇. 概览 <<左移 开始之前,我们先准备点东西:位运算 i<<n 总结为 i*2^n 所以 1<<5 = 2 ...

  7. 《Redis开发与运维》读书笔记

    一.初始Redis 1.Redis特性与优点 速度快.redis所有数据都存放于内存:是用C语言实现,更加贴近硬件:使用了单线程架构,避免了多线程竞争问题 基于键值对的数据结构,支持的数据结构丰富.它 ...

  8. Redis开发与运维

    常用命令 redis-server启动redis redis-server /opt/redis/redis.conf    配置启动 redis-server --port 6379 --dir / ...

  9. redis 开发与运维 学习心得1

    主要是命令相关 第一章 初识Redis 1.redis是基于键值对的NoSQL. 2.redis的值可以是 string, hash, list, set, zset, bitmaps, hyperl ...

随机推荐

  1. Vue:获取当前定位城市名

    实现思想:通过定位获取到当前所在城市名: 1.在工程目录index.html中引入: <script type="text/javascript" src="htt ...

  2. 【原】git如何撤销已提交的commit(未push)

    输入git log,我们可以看到最近的3次提交,最近一次提交是test3,最早的一次是test1,其中一大串类似黄色的字母是commit id(版本号) 如果嫌输出信息太多,可加上--pretty=o ...

  3. 05-04 scikit-learn库之主成分分析

    目录 scikit-learn库之主成分分析 一.PCA 1.1 使用场景 1.2 代码 1.3 参数 1.4 属性 1.5 方法 二.KernelPCA 三.IncrementalPCA 四.Spa ...

  4. HTML5 video视频字幕的使用和制作

    一.video支持视频格式: 以下是三种最常用的格式 1. ogg格式:带有Theora视频编码(免费)+Vorbis音频编码的Ogg文件(免费) 支持的浏览器:firefox.chrome.oper ...

  5. P3105 [USACO14OPEN]公平的摄影Fair Photography

    题意翻译 在数轴上有 NNN 头牛,第 iii 头牛位于 xi(0≤xi≤109)x_i\:(0\le x_i\le 10^9)xi​(0≤xi​≤109) .没有两头牛位于同一位置. 有两种牛:白牛 ...

  6. 各种常见文件的hex文件头

    我们在做ctf时,经常需要辨认各种文件头,跟大家分享一下一些常见的文件头.   扩展名 文件头标识(HEX) 文件描述 123 00 00 1A 00 05 10 04 Lotus 1-2-3 spr ...

  7. idea2019版与maven3.6.2版本不兼容引发的血案

    昨天遇到了点问题解决浪费了一些时间(导致更新内容较少)回顾下问题 项目出现Unable to import maven project: See logs for details 翻了好多博客 莫名的 ...

  8. [网络流 24 题] luoguP4016 负载平衡问题

    [返回网络流 24 题索引] 题目描述 有成环状的 nnn 堆纸牌,现将一张纸牌移动到其邻堆称为一次操作.求使得所有堆纸牌数相等的最少移动次数. Solution 4016\text{Solution ...

  9. Math中ceil中为什么会有负零

    double c=Math.ceil(-0.5); double d=Math.floor(0.5); System.out.println(c); System.out.println(d); Sy ...

  10. SpringBoot之ConfigurationProperties 源码解读

    前言 ConfigurationProperties 是SpringBoot引入的一个和外部配置文件相关的注解类.它可以帮助我们更好的使用外置的配置文件属性. 源码解析 属性注入到Java类 @Tar ...