咱们会常常打交道的string类型,在redis中拥有普遍的使用。也是开启redis数据类型的基础。git
在我最最开始接触的redis的时候,老是觉得字符串类型就是值的类型是字符串。github
好比:SET key valueredis
个人理解是value数据类型是stirng类型,如今来看呢,这句话说得不够具体全面。算法
全部的键都是字符串类型sql
字符串类型的值能够是字符串、数字、二进制数据库
这里也就引出了,另外一个概念:外部类型和内部类型数组
这里的外部类型,就是咱们所熟知的:字符串(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. 还有一点也是根据不一样状况,选择更好地数据结构,节省内存。毕竟是内存数据库,资源珍贵。
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
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"
-- 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"
这两个命令会逐步弃用
为何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[]; };
#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;
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秒
再剩下的就是代码判断次数便可。
字符串类型结合命令有不少的应用场景,这个有待去收集和发现。
Redis 比较容易上手,文档全,代码整洁高效。
固然更须要咱们去深刻其运行原理,来更好使用这个工具来服务咱们的业务。