一、Redis中字符串Strings数据类型简介
1.1 字符串Strings底层数据结构详解
Redis中 字符串(String) 是一种最基本的值类型,Redis 没有直接使用 C 语言传统的字符串表示(以空字符结尾的字符数组,简称 C 字符串),而是构建了自己的字符串类型Strings,名为 简单动态字符串(simple dynamic string,SDS),并将其作为 Redis 默认的字符串表示。
在Redis中,Strings 类型的值是以字节数组的形式进行存储的,所以它可以存储任意类型的数据。当需要读取String类型的值时,Redis会将存储的字节数组转换为对应的数据类型返回。
Redis字符串是二进制安全的,这意味着一个Redis字符串能包含任意类型的数据;并且一个字符串类型的值最多能存储512M字节的内容。
在 Redis 里,C 字符串只会作为字符串字面量(string literal)用在一些无须对字符串值进行修改的地方,比如打印日志:
|
|
当 Redis 需要的不仅仅是一个字符串字面量,而是一个可以被修改的字符串值时,Redis 就会使用 SDS 来表示字符串值。它不仅包括了 String 类型的数据,也包含字符串值的键值对,还被用于缓冲区(buffer):AOF 模块中的 AOF 缓冲区,以及客户端状态中的输入缓冲区,都是由 SDS 实现的。
Redis 中 SDS 字符串(Strings) 是 Redis 自己构建的抽象字符串类型,其在 C 语言原生字符串类型的基础上进行了一些改进和扩展,其本质是一个byte数组。SDS 的结构如下: SDS的总体概览:
Redis 3.0 及之前版本,SDS 的结构定义如下:
|
|
Redis 3.2 版本开始,SDS 的结构定义如下:
|
|
通过上面代码可以看到,SDS有五种不同的头部,其中sdshdr5实际并未使用到,所以实际上有四种不同的头部,分别如下:
这种设计使得 SDS 在保持与 C 语言字符串兼容的同时,具有更高的效率和更好的安全性。
Redis中 字符、数值 也是使用String 表示,对于 数值类型的字符串,可以直接做加减法等运算。
Tips:如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值,超过了这个值,Redis 会报错。
Redis 中的 Strings 采用预分配冗余空间的方式来减少内存的频繁分配,Redis内部为当前字符串分配的空间 capacity 一般要高于字符串实际长度 len。
当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间,需要注意的是字符串最大长度为 512M。
Tips: 字符串是由多个字节组成,每个字节又是由 8 个 bit 组成,如此便可以将一个字符串看成很多 bit 的组合,这便是 bitmap(位图) 数据结构。
关于字符串类型需要注意以下几点:
- 在 Redis 中,所有的 key 的类型都是 Strings 类型的,并且其它几种数据类型也都是在字符串类型的基础上构建的,例如列表和集合的元素的类型都是字符串类型的。
- 在 Redis 中,字符串都是直接按照二进制的形式储存的,因此在使用 Redis 的时候,不需要像 MySQL 那样考虑编码问题(编码不匹配则会出现乱码)。所以 Redis 不会处理字符集的编码问题,客户端传入的命令中使用的是什么字符集编码,就存储什么字符集编码。
- Redis 中的 Strings 类型的值可以是字符串,JSON、XML格式的字符串,数字、整型、浮点数,甚至是二进制流数据,如图片、音频、视频等。不过一个 Strings 的最大值不能超过 512MB。
1.2 字符串(Strings)编码方式简介
在Redis中 字符串(Strings) 类型的值可以使用以下几种不同的编码方式存储,具体的编码方式是根据数据的内容和大小来动态选择的,以最大程度地节省内存和提高性能:
- EMBSTR(embstr-encoded string 嵌套字符串编码):用于存储较短(小于等于44字节)的字符串,但与RAW不同的是,EMBSTR的编码方式将字符串长度也一并存储在编码结构中,以节省内存。
- RAW(简单动态字符串):这是最常见的字符串编码方式,它用于存储较长(大于44字节)的字符串,长度不超过字符串编码结构的限制。这种编码方式不会对字符串进行压缩,因此在存储较小的字符串时效率高。
- INT(整数编码):当一个字符串可以被解释为整数时,Redis会将其编码为整数,以节省内存。整数编码分为以下几种子编码方式:
- int16_t:16位整数编码,存储16位以内的整数。
- int32_t:32位整数编码,存储32位以内的整数。
- int64_t:64位整数编码,存储64位以内的整数。
Tips: INT(整数编码) 编码方式的优点是存储空间小,操作效率高。缺点是只能存储整数,不支持字符串操作。
- RAW和EMBSTR共享编码:在某些情况下,Redis会使用一种特殊的编码方式,该方式可以共享RAW和EMBSTR编码方式的优点。这意味着它既可以存储较短的字符串,又可以高效地存储较大的字符串。
- SDS(简单动态字符串):SDS是一种用于表示字符串的数据结构,它具有动态大小,可以在不需要重新分配内存的情况下进行扩展,这种编码方式用于存储较大的字符串,以节省内存和提高性能。
1.3 简单动态字符串SDS 与 C字符串的区别
C 语言使用长度为 N + 1 的字符数组来表示长度为 N 的字符串,并且字符数组的最后一个元素总是空字符 ‘\0’。
SDS 针对 Redis 对字符串的安全性、效率以及功能方面的要求做了如下优化:
- 常数O(1)时间复杂度获取字符串长度
- C 字符串并不记录自身的长度信息,所以为了获取一个 C 字符串的长度,程序必须遍历整个字符串;而 SDS 由于存储字符串的长度(len 属性),可以将获取字符串长度的复杂度从 O(n) 降到 O(1)。
- 杜绝缓冲区溢出
- 这也是 C 字符串不记录自身长度带来的另一个问题,当 SDS API 需要对 SDS 进行修改时,API 会先检查 SDS 的buf空间是否满足修改所需的要求,如果不满足的话,API 会自动将 SDS 的空间扩展至足够执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现缓冲区溢出问题。
- 减少修改字符串时带来的内存重新分配次数
- Redis 作为数据库,经常被用于速度要求严苛、数据被频繁修改的场合,如果每次修改字符串的长度都需要执行一次内存重分配的话,那么光是执行内存重新分配的时间就会占去修改字符串所用时间的一大部分,如果这种修改频繁地发生的话,会对性能造成影响。
- 通过未使用空间,SDS 实现了 空间预分配 和 惰性空间释放 两种优化策略,提高了内存的使用效率。
- 空间预分配 用于优化 SDS 的字符串增长操作,当 SDS 的 API 对一个 SDS 进行修改,并且需要对 SDS 进行空间扩展的时候,程序不仅会为 SDS 分配修改所必要的空间,还会为 SDS 分配额外的未使用空间。
- 惰性空间释放 用于优化 SDS 的字符串缩短操作,当 SDS 的 API 需要缩短 SDS 保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用。
- 二进制安全:C 字符串是以空字符 ‘\0’ 作为结束标志,因此不能正确存储包含 ‘\0’ 的字符串。而 SDS 的每一个字符都可以是 ‘\0’,因此 SDS 可以存储任何二进制数据。
- 兼容部分 C 字符串函数:SDS 在保证自身特性的同时,仍然保留了对部分 C 字符串函数的兼容性,这样可以方便地在 SDS 和 C 字符串之间进行转换。
二、Redis中Strings字符串值类型命令
Redis 中与 Strings 类型相关的命令、作用 和 时间复杂度:
命令 | 作用 | 时间复杂度 |
---|---|---|
SET | 设置key的值为指定字符串 | O(1) |
GET | 获取key对应的字符串值 | O(1) |
MSET | 批量设置多个键值对 | O(N)(N为键值对数量) |
MGET | 批量获取多个key的值 | O(N)(N为键的数量) |
SETNX | 仅当key不存在时设置值 | O(1) |
SETEX | 设置key的值和过期时间(秒) | O(1) |
PSETEX | 设置key的值和过期时间(毫秒) | O(1) |
INCR | 将key对应的数字值加一 | O(1) |
INCRBY | 将key对应的数字值加上指定整数 | O(1) |
DECR | 将key对应的数字值减一 | O(1) |
DECRBY | 将key对应的数字值减去指定整数 | O(1) |
INCRBYFLOAT | 将key对应的浮点数值加上指定浮点数 | O(1) |
APPEND | 在key对应的字符串值后追加字符串 | O(1) |
GETRANGE | 获取key对应的字符串的子串 | O(N)(N为子串长度) |
SETRANGE | 覆盖key对应字符串的部分内容 | O(N)(N为替换字符串长度) |
STRLEN | 获取key对应的字符串的长度 | O(1) |
SETBIT | 设置或清除指定偏移量上的位(bit) | O(1) |
2.1 SET 类命令
1、SET 命令设置(添加、修改)一个键的字符串值 将字符串值 value 关联到 key。
如果 key 已经持有其它值,无论原来的数据类型是什么,新的 value 都会覆盖原来的 value(无视类型)。
对于某个原本带有生存时间(TTL)的 key 来说,当SET命令成功在这个键上执行时,这个键原有的TTL将被清除。
SET命令格式:
|
|
可选参数:
- EX seconds:表示设置key-value 并为 Key设置过期时间(单位:秒),等同于
SETEX key seconds value
- PX milliseconds:表示设置key-value 并为 Key设置过期时间(单位:毫秒),等同于
PSETEX key milliseconds value
- NX:表示键不存在,才能设置(添加)key-value,等同于
SETNX key value
- XX:表示键存在时,才能设置(修改)key-value
如果EX和PX同时写,则以后面的有效期为准。
返回值:
- 在Redis 2.6.12版本以前,SET命令总是返回OK。
- 从Redis 2.6.12版本开始,SET在设置操作成功完成时,才返回OK。
- 如果由于 SET 指定了 NX 或者 XX 选项但条件不满足,则执行失败,返回 nil。
Tpis: 1、SET命令可以通过参数来实现和SETNX、SETEX和PSETEX三个命令相同的效果; 2、所以将来的Redis版本可能会废弃并最终移除SETNX、SETEX和PSETEX这三个命令;
2、MSET/MSETNX 类命令设置多个键的字符串值 MSET 命令同时设置一个或多个 key-value 对。 MSET命令格式:
|
|
如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是所希望的效果,请考虑使用 MSETNX 命令,它只会在所有给定 key 都不存在的情况下进行设置操作。
MSET 是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置,某些给定 key 被更新而另一些给定 key 没有改变的情况,不可能发生。
MSETNX当所有指定的键不存在时,设置多个键的字符串值
|
|
注意:
MSETNX
是原子操作,只有当所有key
都不存在时,才可设置,只要有其中一个key存在,都不可添加。
命令返回值: 总是返回 OK(因为 MSET 不可能失败)。
2.2 GET 类命令
1、GET 命令获取指定 key 所关联的字符串值 如果指定的 key 不存在时,返回特殊值 nil。此外,如果指定的 key 存储的不是字符串类型的值,则该命令返回一个错误,因为 GET 命令只能用于处理字符串值。
|
|
命令返回值 GET 命令返回有 3 种情况:
- 当 key 存在且为字符串类型时,返回 key 的值;
- 当 key 不存在时,返回 nil;
- 当 key 不是字符串类型时,返回命令与类型不匹配的提示错误;
2、MGET 命令返回指定的一个或多个 key 的值 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回 nil。因此,该命令永不失败。
|
|
命令返回值:
- 一个包含所有给定 key 的值的列表(列表按key的顺序排列)。
- 如果 key 不存在 或者 key对应的数据类型不是 string,返回 nil。
3、GETSET 命令将给定key的值设为value,并返回key的旧值(old value)
|
|
当key存在,但不是字符串类型时,返回一个错误。
命令返回值:
- 返回给定key的旧值。
- 当key没有旧值时,返回nil,key不存在时,也返回nil。
Tips: GETSET可以和INCR组合使用,实现一个有原子性(atomic)复位操作的计数器(counter)。
举例来说,每次当某个事件发生时,进程可能对一个名为mycount的key调用INCR操作,通常还要在一个原子时间内同时完成获得计数器的值和将计数器值复位为0两个操作。
4、GETRANGE 命令截取子字符串 GETRANGE命令返回 key 中字符串值的子字符串,字符串的截取范围由 start 和 end 两个偏移量决定(包括 start 和 end 在内)。
|
|
负数偏移量表示从字符串最后开始计数,-1表示最后一个字符,-2表示倒数第二个,以此类推。
GETRANGE通过保证子字符串的值域(range)不超过实际字符串的值域来处理超出范围的值域请求。
命令返回值:截取得出的子字符串。
2.3 EXPIRE生存时间相关命令
1、设置多少秒或者毫秒后过期
|
|
2、设置在指定Unix时间戳过期
|
|
3、删除过期
|
|
4、查看剩余生存时间 Time To Live,指Key的剩余生存时间
|
|
- key存在但没有设置TTL,返回-1
- key存在,但还在生存期内,返回剩余的秒或者毫秒
- key曾经存在,但已经消亡,返回-2(2.8版本之前返回-1)
2.4 计数(Counter)相关命令
1、INCR / DECR 和 INCRBY / DECRBY 步长命令 INCR / DECR 命令的作用是将 key 对应的 string 表示的数字 加一 /减一。
|
|
- 如果 key 不存在,则视为 key 对应的 value 的值为 0,然后再 加一 / 减一;
- 如果 key 对应的 value 不是一个整数或者其范围超出了 64 位有符号整型,则会报错;
- 如果 INCR / DECR 执行成功,则返回 加一 / 减一 后的值,否则返回相应错误信息。
Tips: 字符串值会被解释成64位有符号的十进制整数来操作,结果依然转成字符串
INCRBY / DECRBY 命令的作用是为 key 对应的 string 表示的整数 加上 / 减去 一个指定的整数 integer。
|
|
- 如果 key 不存在,则视为 key 对应的 value 的值为 0,然后再 加上 / 减去 指定的 integer;
- 如果 key 对应的 value 不是一个整数或者其范围超出了 64 位有符号整型,则会报错;
- 如果 INCRBY / DECRBY 执行成功,则返回 加上 / 减去 integer后的值,否则返回相应错误信息;
- 如果 INCRBY 指定要减去的是一个负数,则表示减去这个数。
- 如果 DECRBY 指定要减去的是一个负数,则表示加上这个数。
Tips:
- 步长命令只能对整形的数据进行步长操作;
- 字符串值会被解释成64位有符号的十进制整数来操作,结果依然转成字符串;
2、INCRBYFLOAT 命令将 key 对应的 string 表示的浮点数加上指定的数 语法:
|
|
- 如果 key 不存在,则默认为0,然后在进行相加操作;
- 如果 key 对应的 string 不是一个数,则会报错;
- 如果指定的数是负数,则表示减去这个数;
- 如果 INCRBYFLOAT 执行成功,则返回运算结果,否则返回错误信息;
- 允许采用科学计数法表示浮点数。
2.5 字符串操作相关命令
1、APPEND 命令在 key 对应的 string 后面追加字符串 语法:FLOAT 将 key 对应的 string 表示的浮点数加上指定的数 语法:
|
|
- 如果 key 不存在,其效果等同于 SET 命令;
- 如果 APPEND 执行成功,则返回最终字符串的长度。
2、SETRANGE 命令覆盖(替换) key 对应 string 中从指定位置开始的一部分(子窜) 语法:
|
|
- 覆盖(替换)的长度为value字符串的长度,如果原字符串后面的长度不足,则将后面的全部替换;
- 如果 key 不存在,并且指定替换的位置大于 0,则前面的位置由十六进制的 0 替换;
- 替换成功则返回最终字符串的长度。
3、GETRANGE 命令截取key 对应的 string 中的子串 语法:
|
|
- 截取的内容由指定的偏移量 start 和 end 确定,并且区间是左右闭合的;
- 指定的偏移量可以是负数,当指定为负数时,表示的是倒数第几个字符,如 -1 表示倒数第一个字符;
- 超出范围的偏移量会根据 string 的长度调整成正确的值。
4、STRLEN 命令获取 key 对应的 string 的长度 语法:
|
|
- 其返回值为字符串的长度;
- 如果 key 不存在,则返回 0;
- 如果 key 对应的 value 的类型不是 string 则会报错;
- 字符串的长度由当前编码规则所决定。
2.6 SETBIT / GETBIT 命令 设置或清除 / 获取 指定偏移量上的位(bit)
1、SETBIT 命令对key所储存的字符串值,设置或清除指定偏移量上的位(bit)。 语法:
|
|
- 位的设置或清除取决于value参数,可以是0,也可以是1,设置其它的值会报错。
- 当key不存在时,自动生成一个新的字符串值。
- 字符串会进行伸展以确保它可以将value保存在指定的偏移量上。当字符串值进行伸展时,空白位置以0填充。
- offset参数必须大于或等于0,小于2^32(bit映射被限制在512MB之内)。
Tips: 对使用比较大的 offse t的SETBIT操作来说,内存分配可能造成Redis服务器被阻塞。
返回值:
- 指定偏移量原来储存的位。
2、GETBIT 命令对key所储存的字符串值,获取指定偏移量上的位(bit)。 语法:
|
|
- 当offset比字符串值的长度大,或者key不存在时,返回0。
返回值:
- 字符串值指定偏移量上的位(bit)。
三、BITMAP位图
3.1 BITMAP位图简介
位图不是真正的数据类型,它是定义在字符串类型中的,一个字符串类型的值最多能存储512M字节的内容,其中位上限:2^9^*2^20^*2^3^ = 2^32^bit
位图常用命令:
- 设置某一位上的值:
SETBIT key offset value
- offset偏移量,从0开始
- value不写,默认是0
- 获取某一位上的值:
GETBIT key offset
- 返回指定值0或者1在指定区间上第一次出现的位置:
BITPOS key bit [start] [end]
3.2 位操作命令
对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 dteskey 上:
- operation 可以是 AND 、 OR 、 NOT 、 XOR 这四种操作中的任意一种;
- BITOP AND destkey key [key …] ,对一个或多个 key 求逻辑并,并将结果保存到 destkey;
- BITOP OR destkey key [key …] ,对一个或多个 key 求逻辑或,并将结果保存到 destkey;
- BITOP XOR destkey key [key …] ,对一个或多个 key 求逻辑异或,并将结果保存到 destkey;
- BITOP NOT destkey key ,对给定 key 求逻辑非,并将结果保存到 destkey;
- 除了 NOT 操作之外,其它操作都可以接受一个或多个 key 作为输入;
- 当 BITOP 处理不同长度的字符串时,较短的那个字符串所缺少的部分会被看作 0;
- 空的 key 也被看作是包含 0 的字符串序列;
|
|
3.3 统计指定位区间上值为1的个数
使用格式1:BITCOUNT key [start] [end]
- 从左向右从0开始,从右向左从-1开始,
- 注意:官方start、end是位,测试后是字节
使用格式2:
BITCOUNT testkey 0 0
- 表示从索引为0个字节到索引为0个字节,也就是指对第一个字节的统计
- 注意:
BITCOUNT testkey 0 -1
等同于 `BITCOUNT testkey``
最常用的格式就是 BITCOUNT testkey
四、Strings 类型的使用场景
4.1 缓存(Cache)
由于 Redis 速度快的特点,因此常用于缓存功能。比较典型的缓存使用场景就是,Redis 作为缓冲层,MySQL 作为存储层,绝大部分请求的数据都是从 Redis 中获取。由于 Redis 具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
Redis + MySQL 组成的缓存存储架构: 下面通过伪代码模拟了上图的业务数据的访问过程:
|
|
通过增加缓存功能,在理想情况下,每个用户信息,一个小时期间只会有一次 MySQL 查询存在,极大地提升了查询效率,同时也降低了 MySQL 的访问数。
4.2 计数(Counter)
计数(Counter)功能是 Redis 中常见且有用的功能之一,它可以用来快速记录和查询某个对象的计数值。这种功能在许多应用中都非常有用,例如网站的访问计数、点赞数、评论数、播放次数等。 例如记录视频播放次数:
|
|
在实际开发一个成熟、稳定的计数系统时,会面临许多挑战和复杂性。以下是一些可能需要应对的挑战和考虑因素:
- 防作弊:确保计数系统不容易被恶意操纵是至关重要的,常见的防作弊措施包括限制每个用户或IP地址的计数速率,使用验证码或令牌来验证用户行为等。
- 按不同维度计数:有时需要按照不同的维度进行计数,例如按时间、地理位置、用户类型等,为了实现这种灵活性,需要设计适应性强的计数系统架构。
- 避免单点问题:单点故障可能会导致计数系统的不可用性,为了确保高可用性,可以考虑使用Redis的主从复制或集群模式,或者使用其它分布式计数系统。
- 数据持久化:Redis默认将数据存储在内存中,但为了持久化数据,可以将数据定期快照(RDB)到磁盘或使用持久化选项,如AOF(Append-Only File)。
- 性能优化:处理大量计数请求可能会对性能造成压力,需要优化Redis配置、考虑使用缓存层、分布式计数系统或负载均衡策略,以应对高负载情况。
- 并发控制:并发操作可能导致计数不一致。要确保计数的原子性,可以使用Redis的事务或乐观锁等技术。
- 监控和日志:建立监控和日志系统,以实时追踪计数系统的性能和运行状况,以及检测潜在的问题。
- 容量规划:考虑计数系统的容量规划,包括数据存储需求、内存和硬盘空间等,以支持未来的增长。
- 数据清理:定期清理不再需要的计数数据,以防止数据膨胀和内存占用过多。
总之,开发一个真实的计数系统是一个复杂的任务,需要考虑众多因素。选择合适的技术栈、设计良好的架构、实施安全性和防作弊措施、确保高可用性以及建立监控和维护策略都是成功实现计数系统的重要步骤。这些挑战需要仔细的规划和实施,以满足特定项目的需求。
4.3 共享会话(Session)
在一个分布式 Web 服务中,用户的会话信息通常存储在各自的服务器上,这包括用户的登录状态和其它会话相关数据。然而,由于负载均衡的需要,用户的请求会被分发到不同的服务器上,而不同服务器上的会话数据并不共享。
这就导致了一个问题:如果用户的请求被负载均衡到不同的服务器上,用户在刷新页面或发送下一个请求时可能会发现自己需要重新登录,这种体验对用户来说是不可接受的。
为了解决这个问题,可以使用Redis将用户的 Session 信息进行集中管理。在这种模式下,只要确保 Redis 是高可用和可扩展的,不论用户被均衡到哪台 Web 服务器上,都可以集中从 Redis 中查询、更新Session信息。
4.4 手机验证码 及 限速器(Limiter)
为了增强用户登录的安全性,许多应用会采取以下步骤:
- 在每次用户尝试登录时,要求用户输入其手机号,并通过向其手机发送验证码来进行二次验证。这个验证码需要用户再次输入,以确保登录请求来自于用户本人。
- 此外,为了防止滥用短信接口和提高安全性,通常会限制用户每分钟获取验证码的频率,例如,在一分钟内,同一手机号最多只能获取验证码5次。
这种流程可以有效地降低恶意登录和滥发验证码的风险,同时保障用户的账户安全。
此功能可以用以下伪代码说明基本实现思路:
|
|
4.5 分布式锁
参考:https://github.com/xiaoxuxiansheng/redis_lock