String 类型
一、String 数据类型与结构
String 类型简介
String类型是redis的最基础的数据结构,也是最经常使用到的类型。而且其他的四种类型多多少少都是在字符串类型的基础上构建的,所以String类型是redis的基础。
String 类型的值最大能存储 512MB,这里的String类型可以是简单字符串、复杂的xml/json的字符串、二进制图像或者音频的字符串、以及可以是数字的字符串。
常用命令
set 命令
描述:该命令用于设置给定 key 的值。如果 key 已经存储其他值, SET 就覆写旧值,且无视类型。
127.0.0.1:6379> set name starsky
OK
127.0.0.1:6379>
通过set命令给restkey这个key值绑定value,当SET在设置操作成功完成时,才返回 OK。
get 命令
描述:该命令用于获取指定 key 的值。如果 key不存在,返回 nil 。如果key对应储存的值不是字符串类型,返回一个错误。
127.0.0.1:6379> get name
"starsky"
127.0.0.1:6379>
getset 命令
描述:该命令用于获取指定的key的旧值,然后按照新值对key进行赋值。当key中没有旧值的时候返回nil。
127.0.0.1:6379> getset name will
"starsky"
127.0.0.1:6379> get name
"will"
127.0.0.1:6379>
mget 命令
描述:该命令用于返回多个key的值,当其中某一个KEY的值不存在,返回nil。
127.0.0.1:6379> set age 10
OK
127.0.0.1:6379> set sex 1
OK
127.0.0.1:6379> mget name age sex
1) "will"
2) "10"
3) "1"
127.0.0.1:6379>
decr 命令
描述:对key对应的数字做减1操作。如果key不存在,那么在操作之前,这个key对应的值会被置为0。如果key有一个错误类型的value或者是一个不能表示成数字的字符串,就返回错误。
127.0.0.1:6379> decr age
(integer) 9
127.0.0.1:6379> get age
"9"
127.0.0.1:6379>
incr 命令
描述:对存储在指定key的数值执行原子的加1操作,如果指定的key不存在,那么在执行incr操作之前,会先将它的值设定为0。如果指定的key中存储的值不是字符串类型(fix:)或者存储的字符串类型不能表示为一个整数,那么执行这个命令时服务器会返回一个错误(eq:(error) ERR value is not an integer or out of range)。
127.0.0.1:6379> get age
"9"
127.0.0.1:6379> incr age
(integer) 10
127.0.0.1:6379>
更多 String 命令,参考这里:http://www.redis.cn/commands.html#string
二、String 简单字符结构
SDS 动态字符串
SDS(Simple Dynamic Strings, 简单动态字符串)是 Redis 的一种基本数据结构,主要是用于存储字符串和整数。
SDS数据结构实现(Redis3):
struct sdshdr {
unsigned int len;
unsigned int free;
char buf[];
};
其中,buf 表示数据空间,用于存储字符串;len 表示 buf 中已占用的字节数,也即字符串长度;free 表示 buf 中剩余可用字节数。
好处
- 用单独的变量 len 和 free,可以方便地获取字符串长度和剩余空间;
- 内容存储在动态数组 buf 中,SDS 对上层暴露的指针指向 buf,而不是指向结构体 SDS。因此,上层可以像读取 C 字符串一样读取 SDS 的内容,兼容 C 语言处理字符串的各种函数,同时也能通过 buf 地址的偏移,方便地获取其他变量;
- 读写字符串不依赖于 \0,保证二进制安全。
坏处
- 对于不同长度的字符串,没有必要使用 len 和 free 这 2 个 4 字节的变量?
- 4 字节的 len,可表示的字符串长度为 2^32,而在实际应用中,存放于 Redis 中的字符串往往没有这么长,因此,空间的使用上能否进一步压缩?
新的SDS结构
Redis 增加了一个 flags 字段来标识类型,用一个字节(8 位)来存储。
其中:前 3 位表示字符串的类型;剩余 5 位,可以用来存储长度小于 32 的短字符串。
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 前3位存储类型,后5位存储长度 */
char buf[]; /* 动态数组,存放字符串 */
};
而对于长度大于 31 的字符串,仅仅靠 flags 的后 5 位来存储长度明显是不够的,需要用另外的变量来存储。sdshdr8、sdshdr16、sdshdr32、sdshdr64 的数据结构定义如下,其中 :
- len 表示已使用的长度。
- alloc 表示总长度。
- buf 存储实际内容。
- flags 的前 3 位依然存储类型,后 5 位则预留。
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 已使用长度,1字节 */
uint8_t alloc; /* 总长度,1字节 */
unsigned char flags; /* 前3位存储类型,后5位预留 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* 已使用长度,2字节 */
uint16_t alloc; /* 总长度,2字节 */
unsigned char flags; /* 前3位存储类型,后5位预留 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* 已使用长度,4字节 */
uint32_t alloc; /* 总长度,4字节 */
unsigned char flags; /* 前3位存储类型,后5位预留 */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* 已使用长度,8字节 */
uint64_t alloc; /* 总长度,8字节 */
unsigned char flags; /* 前3位存储类型,后5位预留 */
char buf[];
};
Redis创建字符串流程
sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
// 根据字符串长度计算相应类型
char type = sdsReqType(initlen);
// 如果创建的是""字符串,强转为SDS_TYPE_8
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
// 根据类型计算头部所需长度(头部包含 len、alloc、 flags)
int hdrlen = sdsHdrSize(type);
// 指向flags的指针
unsigned char *fp;
// 创建字符串,+1是因为 `\0` 结束符
sh = s_malloc(hdrlen+initlen+1);
if (sh == NULL) return NULL;
if (init==SDS_NOINIT)
init = NULL;
else if (!init)
memset(sh, 0, hdrlen+initlen+1);
// s指向buf
s = (char*)sh+hdrlen;
// s减1得到flags
fp = ((unsigned char*)s)-1;
...
// 在s末尾添加\0结束符
s[initlen] = '\0';
// 返回指向buf的指针s
return s;
}
创建 SDS 的大致流程是这样的:首先根据字符串长度计算得到 type,根据 type 计算头部所需长度,然后动态分配内存空间。
注意:
- 创建空字符串时,SDS_TYPE_5 被强制转换为 SDS_TYPE_8(原因是创建空字符串后,内容可能会频繁更新而引发扩容操作,故直接创建为 sdshdr8)。
- 长度计算有 +1 操作,因为结束符 \0 会占用一个长度的空间。
- 返回的是指向 buf 的指针 s。
String数据类型的应用
session共享
如下图:
一个分布式web服务将用户的Session信息(比如:登录信息)记录到各自服务器中,这样会出现一个问题,在负载均衡的情况下,服务器会将用户的访问均衡到不同的服务器上,用户刷新一次访问可能就会发现需要重新登录,这个问题对于用户体验来说是无法容忍的。
为了解决这个问题我们会是使用Redis将用户的Session进行集中管理,这样就只需要保证redis的高可用以及扩展性,每次用户的登录或者查询登录都从Redis中获取Session信息。
如下图:
计数器
例如一个商品的流量量;如下代码所示:
<?php
// 组装连接信息
$config = [
"redis" => [
"host" => '192.168.188.190',
"port" => 6379,
"password" => "root" //密码需要自己去配置文件设置
]
];
// 实例化对象
$Redis = new Redis();
// 使用函数进行Redis的连接
$Redis->connect($config['redis']['host'],$config['redis']['port']);
// 输入密码
$Redis->auth($config['redis']['password']);
// 商品ID
$key = "product:".$_GET['product_id'];
// 判断 $key 商品ID 非存在
if (!$Redis->exists($key)){
// 新生成商品ID的key
$Redis->set($key,1);
}else{ // 否则 存在
// 将 商品ID key的流量量 自增1
$Redis->incr($key);
}
// 跳转到对应的商品详情
header("Location:http://blog-login.com/view/SteelSeries_zxr.php?id=".$_GET['id']."&product_id=".$_GET['product_id']);
?>
Redis 限速
在一些项目中为了保证安全会要求用户在登录的时候输入手机号进行验证码验证,但是为了保证短信接口不被频繁访问,会进行一定的限制。
<?php
// 连接 Redis
$Redis = new Redis("192.168.29.108",6379);
// 连接密码
$Redis->auth("root");
// 请求的手机号码
$phonename="176xxxx0888";
// 定义一个 key
$key = "info:".$phonename;
// 检查这个key 是否存在
$restful = $Redis->exists($key);
// 判断key等于空,或者小于5次
if ($restful != null || $Redis->incr($key)<=5) {
return "OK"; // 发送验证信息
}else { // 否则
echo "1分钟不能请求5次"; // 返回错误信息
}
?>
上面的代码就是使用Redis实现了限速的功能,例如一些网站限制一个IP地址不能在1秒内访问超过n次也可以使用类似的思路。