IT小栈

  • 主页
  • Java基础
  • RocketMQ
  • Kafka
  • Redis
  • Shiro
  • Spring
  • Spring Boot
  • Spring Cloud
  • 资料链接
  • 关于
所有文章 友链

IT小栈

  • 主页
  • Java基础
  • RocketMQ
  • Kafka
  • Redis
  • Shiro
  • Spring
  • Spring Boot
  • Spring Cloud
  • 资料链接
  • 关于

Redis基本数据类型之String

2020-05-15

Redis的基本数据类型有五种,string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。字符串String又是最简单、最常用的数据类型,本节我们重点探讨其基本使用、底层存储原理、使用场景等。

1、简单介绍

Redis中所有的数据结构都是以唯一的key字符串作为名称,然后通过这个唯一的key值来获取相应的value数据。不同类型的数据结构的差异就在于value的结构不同。

Redis的字符串其底层实现是简单动态字符串SDS(simple dynamic string),是可以修改的字符串。内部实现类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,在字符串长度小于 1MB 时,按所需长度的 2 倍来分配,超过 1MB,则按照每次额外增加 1MB 的容量来预分配。需要注意的是字符串最大长度为512MB。

2、常用命令

2.1、SET-设置值

SET key value [EX seconds] [PX milliseconds] [NX|XX]

我们发现前两个参数是比必填的后面的参数都是选填的;EX指的是该键值对的过期时间单位是秒;PX指的是该键值对的过期时间单位是毫秒;NX指代键值对不存在则设置成功,存在则设置失败;XX指代键值对存在则设置成功,不存在则设置失败。下面我们逐一验证。

2.2、SETNX-KEY不存在设置,存在不设置

SETNX key value

我们发现这个命令和我们上面的说的SET key value NX是一个意思

2.3、SETEX-设置键值对并设置过期时间(秒)

SETEX key seconds value

将键 key 的值设置为 value , 并将键 key 的生存时间设置为 seconds 秒钟。如果键 key 已经存在, 那么 SETEX 命令将覆盖已有的值。

2.4、PSETEX-设置键值对并设置过期时间(毫秒)

PSETEX key milliseconds value

这个命令和 SETEX 命令相似, 但它以毫秒为单位设置 key 的生存时间, 而不是像 SETEX 命令那样以秒为单位进行设置。

2.5、GET-获取某个键的值

GET key

如果键 key 不存在, 那么返回特殊值 nil ; 否则, 返回键 key 的值。如果键 key 的值并非字符串类型, 那么返回一个错误, 因为 GET 命令只能用于字符串值

2.6、GETSET-设置键值对并返回被设置之前的旧值

GETSET key value

返回给定键 key 的旧值。如果键 key 没有旧值, 也即是说, 键 key 在被设置之前并不存在, 那么命令返回 nil 。当键 key 存在但不是字符串类型时, 命令返回一个错误。

2.7、STRLEN-返回字符串的长度

STRLEN key

返回键 key 储存的对应的value字符串值的长度。

2.8、APPEND-末尾追加字符串

APPEND key value

如果键 key 已经存在并且它的值是一个字符串, APPEND 命令将把 value 追加到键 key 现有值的末尾。如果 key 不存在, APPEND 就简单地将键 key 的值设为 value , 就像执行 SET key value 一样。追加 value 之后, 返回键 key 的对应的value字符串的值的长度。

2.9、GETRANGE-获取偏移量范围内的值

GETRANGE key start end

返回键 key 储存的字符串值的指定部分, 字符串的截取范围由 start 和 end 两个偏移量决定 (包括 start 和 end 在内)。负数偏移量表示从字符串的末尾开始计数, -1 表示最后一个字符, -2 表示倒数第二个字符, 以此类推。

2.10、INCR-自增

INCR key

为键 key 储存的数字值加上一。如果键 key 不存在, 那么它的值会先被初始化为 0 , 然后再执行 INCR 命令。如果键 key 储存的值不能被解释为数字, 那么 INCR 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。

2.11、INCRBY-设置自增的数字

INCRBY key increment

为键 key 储存的数字值加上增量 increment 。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 INCRBY 命令。如果键 key 储存的值不能被解释为数字, 那么 INCRBY 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。

2.12、DECR-递减

DECR key

为键 key 储存的数字值减去一。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 DECR 操作。如果键 key 储存的值不能被解释为数字, 那么 DECR 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内。

2.13、DECRBY-设置递减的数字

DECRBY key decrement

将键 key 储存的整数值减去减量 decrement 。如果键 key 不存在, 那么键 key 的值会先被初始化为 0 , 然后再执行 DECRBY 命令。如果键 key 储存的值不能被解释为数字, 那么 DECRBY 命令将返回一个错误。本操作的值限制在 64 位(bit)有符号数字表示之内

2.14、MSET-批量设值

MSET key value [key value …]

如果某个给定键已经存在, 那么 MSET 将使用新值去覆盖旧值, 如果这不是你所希望的效果, 请考虑使用 MSETNX 命令, 这个命令只会在所有给定键都不存在的情况下进行设置。MSET 是一个原子性(atomic)操作, 所有给定键都会在同一时间内被设置, 不会出现某些键被设置了但是另一些键没有被设置的情况。

注意:mset,mget 具有原子性不可切割,当时操作的键值 可能不在一个槽里面,所以失败;可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去。

2.15、MGET-批量获取值

MGET key [key …]

返回给定的一个或多个字符串键的值。如果给定的字符串键里面, 有某个键不存在, 那么这个键的值将以特殊值 nil 表示

3、内部实现

3.1、Redis内部存储结构

dictEntry

因为 Redis 是 KV 的数据库,它是通过 hashtable 实现的(我们把这个叫做外层的哈希)。所以每个键值对都会有一个 dictEntry,里面指向了 key 和 value 的指针。next 指向下一个 dictEntry。源码如下:

1
2
3
4
5
6
7
8
9
10
typedef struct dictEntry {
void *key; //关键字
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v; //val
struct dictEntry *next; //next
} dictEntry;

key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在自定义的 SDS中。

value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在redisObject 中。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。

redisObject

1
2
3
4
5
6
7
typedef struct redisObject {
unsigned type:4; /* 对象的类型, 包括: OBJ_STRING、 OBJ_LIST、 OBJ_HASH、 OBJ_SET、 OBJ_ZSET */
unsigned encoding:4; /* 具体的数据结构 */
unsigned lru:LRU_BITS; /* 24 位, 对象最后一次被命令程序访问的时间, 与内存回收有关 */
int refcount; /* 引用计数。 当 refcount 为 0 的时候, 表示该对象已经不被任何对象引用, 则可以进行垃圾回收了*/
void *ptr; /* 指向对象实际的数据结构 */
} robj;

3.2、String类型内部编码

Redis中的字符串类型,有三种内部编码:raw、embstr、int。当值小于44字节(Redis 3.2+,之前版本小于39字节),使用embstr,否则使用raw

int类型就是指的是数字,没有什么说的,那么raw、embstr都代表的是字符串有什么异同吗,下面我们分析下。

图中展示了两者的区别,可以看到embstr将redisObject和SDS保存在连续的64字节空间内,这样可以只需要一次内存分配,而对于raw来说,SDS和redisObject分离,需要两次内存分配,而且占用更多的内存空间。

可以看到embstr在3.2+中使用了叫sdshdr8的结构,在该结构下,元数据只需要3个字节,而Redis需要8个字节,所以总共64个字节,减去redisObject(16字节),再减去SDS的原信息,最后的实际内容就变成了44字节和39字节。

embstr 和raw 比较

embstr 的使用只分配一次内存空间(因为 RedisObject 和 SDS 是连续的),而 raw需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间)。

因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。

而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个RedisObject 和 SDS 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读。

对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44个字节。如(append操作,set操作不会导致这种问题)

注意:

当 int 数 据 不 再 是 整 数 , 或 大 小 超 过 了 long 的 范 围(2^63-1=9223372036854775807)时,自动转化为 embstr。

关于 Redis 内部编码的转换,都符合以下规律:编码转换在 Redis 写入数据时完成,且转换过程不可逆,只能从小内存编码向大内存编码转换(但是不包括重新 set)

3.3、Redis的SDS

Redis 3.2+ (3.2 4.0 5.0):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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本质上就是char *,因为有了表头sdshdr结构的存在,所以SDS比传统C字符串在某些方面更加优秀,并且能够兼容传统C字符串。

sds在Redis中是实现字符串对象的工具,并且完全取代char*..sds是二进制安全的,它可以存储任意二进制数据,不像C语言字符串那样以‘\0’来标识字符串结束,

因为传统C字符串符合ASCII编码,这种编码的操作的特点就是:遇零则止 。即,当读一个字符串时,只要遇到’\0’结尾,就认为到达末尾,就忽略’\0’结尾以后的所有字符。因此,如果传统字符串保存图片,视频等二进制文件,操作文件时就被截断了。

SDS表头的buf被定义为字节数组,因为判断是否到达字符串结尾的依据则是表头的len成员,这意味着它可以存放任何二进制的数据和文本数据,包括’\0’

SDS 和传统的 C 字符串获得的做法不同,传统的C字符串遍历字符串的长度,遇零则止,复杂度为O(n)。而SDS表头的len成员就保存着字符串长度,所以获得字符串长度的操作复杂度为O(1)。

参考文档:

【1】https://blog.csdn.net/qq193423571/article/details/81637075

【2】https://segmentfault.com/a/1190000020767973?utm_source=tag-newest

本文作者: 顾 明 训
本文链接: https://www.itzones.cn/2020/05/15/Redis基本数据类型之String/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!
  • redis String
  • redis SDS
  • redis

扫一扫,分享到微信

微信分享二维码
RocketMQ推送消费源码分析(一)-消息拉取
Redis整体数据存储说明
  1. 1. 1、简单介绍
  2. 2. 2、常用命令
    1. 2.1. 2.1、SET-设置值
    2. 2.2. 2.2、SETNX-KEY不存在设置,存在不设置
    3. 2.3. 2.3、SETEX-设置键值对并设置过期时间(秒)
    4. 2.4. 2.4、PSETEX-设置键值对并设置过期时间(毫秒)
    5. 2.5. 2.5、GET-获取某个键的值
    6. 2.6. 2.6、GETSET-设置键值对并返回被设置之前的旧值
    7. 2.7. 2.7、STRLEN-返回字符串的长度
    8. 2.8. 2.8、APPEND-末尾追加字符串
    9. 2.9. 2.9、GETRANGE-获取偏移量范围内的值
    10. 2.10. 2.10、INCR-自增
    11. 2.11. 2.11、INCRBY-设置自增的数字
    12. 2.12. 2.12、DECR-递减
    13. 2.13. 2.13、DECRBY-设置递减的数字
    14. 2.14. 2.14、MSET-批量设值
    15. 2.15. 2.15、MGET-批量获取值
  3. 3. 3、内部实现
    1. 3.1. 3.1、Redis内部存储结构
    2. 3.2. 3.2、String类型内部编码
    3. 3.3. 3.3、Redis的SDS
© 2020 IT小栈
载入天数...载入时分秒... || 本站总访问量次 || 本站访客数人次
Hexo Theme Yilia by Litten
  • 所有文章
  • 友链

tag:

  • jvm
  • Java基础
  • kafka HW
  • kafka Leader Epoch
  • kafka
  • kafka位移主题
  • kafka位移提交
  • kafka副本机制
  • kafka ISR
  • zookeeper
  • kafka消息丢失
  • kafka日志存储
  • kafka Log Clean
  • kafka Log Compaction
  • kafka消费位移设置
  • kafka Rebalance
  • kafka分区算法
  • kafka生产者拦截器
  • kafka SASL/SCRAM
  • kafka ACL
  • redis
  • redis Ziplist
  • redis Hashtable
  • redis LinkedList
  • redis QuickList
  • redis intset
  • redis String
  • redis SDS
  • redis SkipList
  • redisDb
  • redisServer
  • redis 简介
  • Redis Cluster
  • 主从同步
  • RocketMQ高可用HA
  • 事务消息
  • 内存映射
  • MMAP
  • 同步刷盘
  • 异步刷盘
  • 消息存储文件
  • RocketMQ安装
  • 延迟消息
  • RocketMQ入门
  • 推拉模式
  • PushConsumer
  • 消费结果处理
  • rebalance
  • RocketMQ权限控制
  • RocketMQ ACL
  • 消息过滤
  • 消息重试
  • 消费位置
  • 集群消费
  • 广播消费
  • 运维命令
  • shiro源码分析
  • shiro入门
  • IOC和DI
  • Spring创建Bean
  • Bean生命周期
  • Sping属性注入
  • 异常
  • SpringMVC
  • springCloud
  • Eureka

    缺失模块。
    1、请确保node版本大于6.2
    2、在博客根目录(注意不是yilia根目录)执行以下命令:
    npm i hexo-generator-json-content --save

    3、在根目录_config.yml里添加配置:

      jsonContent:
        meta: false
        pages: false
        posts:
          title: true
          date: true
          path: true
          text: false
          raw: false
          content: false
          slug: false
          updated: false
          comments: false
          link: false
          permalink: false
          excerpt: false
          categories: false
          tags: true
    

  • 我的OSCHINA
  • 我的CSDN
  • 我的GITHUB
  • 一生太水