python 操作redis之二(字符串)

字符串使用与内部实现原理

Redis 发展到现在已经有 9 种数据类型了,其中最基础、最常用的数据类型有 5 种,它们分别是:字符串类型、列表类型、哈希表类型、集合类型、有序集合类型,而在这 5 种数据类型中最常用的是字符串类型,所以本文我们先从字符串的使用开始说起。

字符串类型的全称是 Simple Dynamic Strings 简称 SDS,中文意思是:简单动态字符串。它是以键值对 key-value 的形式进行存储的,根据 key 来存储和获取 value 值,它的使用相对来说比较简单,但在实际项目中应用非常广泛。

1 字符串类型能做什么?

字符串类型的使用场景有很多,但从功能的角度来区分,大致可分为以下两种:

  • 字符串存储和操作;
  • 整数类型和浮点类型的存储和计算。

字符串最常用的业务场景有以下几个。

1)页面数据缓存

我们知道,一个系统最宝贵的资源就是数据库资源,随着公司业务的发展壮大,数据库的存储量也会越来越大,并且要处理的请求也越来越多,当数据量和并发量到达一定级别之后,数据库就变成了拖慢系统运行的“罪魁祸首”,为了避免这种情况的发生,我们可以把查询结果放入缓存(Redis)中,让下次同样的查询直接去缓存系统取结果,而非查询数据库,这样既减少了数据库的压力,同时也提高了程序的运行速度。

介于以上这个思路,我们可以把文章详情页的数据放入缓存系统。具体的做法是先将文章详情页序列化为字符串存入缓存,再从缓存中读取到字符串,反序列化成对象,然后再赋值到页面进行显示 (当然也可以用哈希类型进行存储,这会在下一篇文章中讲到),这样我们就实现了文章详情页的缓存功能,架构流程对比图如下所示。

原始系统运行流程图:

字符串类型使用-1.png

引入缓存系统后的流程图:

字符串类型使用-2.png

2)数字计算与统计

Redis 可以用来存储整数和浮点类型的数据,并且可以通过命令直接累加并存储整数信息,这样就省去了每次先要取数据、转换数据、拼加数据、再存入数据的麻烦,只需要使用一个命令就可以完成此流程,具体实现过程本文下半部分会讲。这样我们就可以使用此功能来实现访问量的统计,当有人访问时访问量 +1 就可以了。

3)共享 Session 信息

通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。

例如用户一的 Session 信息被存储在服务器一,但第二次访问时用户一被分配到服务器二,这个时候服务器并没有用户一的 Session 信息,就会出现需要重复登录的问题。分布式系统每次会把请求随机分配到不同的服务器,因此我们需要借助缓存系统对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去统一的缓存系统获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。

分布式系统单独存储 Session 流程图:

字符串类型使用-3.png

分布式系统使用同一的缓存系统存储 Session 流程图:

字符串类型使用-4.png

字符串如何使用? 

from redis import StrictRedis
redis_cli = StrictRedis(host="xx:xx:xx:xx", port=xxxx, password="xxx", db=xx)

"""单个键值对操作"""

# 添加键值对
redis_cli.set("name", "hello")

# 获取键值对
name = redis_cli.get("name")

# 给元素追加值
redis_cli.append("name", "china")

# 查看元素长度
name_len = redis_cli.strlen("name")

"""多个键值对操作"""

# 设置多个键值对
redis_cli.mset({"country": "china", "age": "1000"})

# 获取多个键值对
country, age = redis_cli.mget("country", "age")

"""数字统计"""

# .给整数类型的值加指定数值(不指定默认增加1)(如果 key 不存在,则会先初始化此 key 为 0 ,然后再执行减法操作)
redis_cli.incr("age", 12)

# 给整数类型的值减制定数值(不指定默认减1)(如果 key 不存在,则会先初始化此 key 为 0 ,然后再执行减法操作)
redis_cli.decr("age", 100)

# 根据 key 减去指定的值(如果 key 不存在,则会先初始化此 key 为 0 ,然后再执行减法操作)
redis_cli.decrby("age", 900)

# 根据 key 加指定的整数值(如果 key 不存在,则会先初始化此 key 为 0 ,然后再执行加整数值的操作)
redis_cli.incrby("age", 123)

# 根据 key 加上指定的浮点数(如果 key 不存在,则会先初始化此 key 为 0 ,然后再执行加浮点数的操作)
redis_cli.incrbyfloat("age", 3.1415)

# redis_cli.flushdb()

"""键值对过期操作"""

# 添加键值对并设置过期时间
redis_cli.set("ex", "ex", ex=100)

# 赋值字符串,并设置过期时间(单位/秒)
redis_cli.setex("setex", 100, "setex")

# 如果 key 已经存在,setex 命令将会覆写原来的旧值

# 赋值字符串,并设置过期时间(单位/毫秒)
redis_cli.psetex("psetex", 10000, "psetex")

print(redis_cli.ttl("ex"))
print(redis_cli.ttl("setex"))
print(redis_cli.ttl("psetex"))

"""字符串操作进阶"""

# 根据指定的范围截取字符串
name_range = redis_cli.getrange("name", 0, 3)
name_r = redis_cli.getrange("name", -3, -1)
print(name_range)
print(name_r)

# 设置字符串新值并返回旧值
name_gset = redis_cli.getset("name", "newhello")
print(name_gset)

# 赋值(创建)键值对,当 key 不存在时(如果 key 已经存在,则执行命令无效,不会修改原来的值,否则会创建新的键值对)
n = redis_cli.setnx("m", "myname")
nn = redis_cli.setnx("m", "hisname")
m = redis_cli.get("m")
print(m)

# 设置一个或多个键值,当所有键值都不存在时
redis_cli.msetnx({"her": "yes", "his": "no"})
redis_cli.msetnx({"her": "her", "his": "his"})
her, his = redis_cli.mget("her", "his")
print(her, his)

# 截取字符串并赋值(如果待截取的键不存在,会当作空白字符串处理)
redis_cli.setrange("exist", 3, "workd")
print(redis_cli.get("exist"))

# 数据类型(我们可以使用 object encoding key 命令来查看对象(键值对)存储的数据类型,当我们使用此命令来查询 SDS 对象时,发现 SDS 对象竟然包含了三种不同的数据类型:int、embstr 和 raw)
print(redis_cli.object("encoding", "name"))
print(redis_cli.object("encoding", "wo"))
print(redis_cli.object("encoding", "her"))

字符串的内部实现

1)源码分析

Redis 3.2 之前 SDS 源码如下:

struct sds{
    int len; // 已占用的字节数
    int free; // 剩余可以字节数
    char buf[]; // 存储字符串的数据空间
}

可以看出 Redis 3.2 之前 SDS 内部是一个带有长度信息的字节数组,存储结构如下图所示:

字符串存储结构图.png

为了更加有效的利用内存,Redis 3.2 优化了 SDS 的存储结构,源码如下:

typedef char *sds;

struct __attribute__ ((__packed__)) sdshdr5 { // 对应的字符串长度小于 1<<5
    unsigned char flags;
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { // 对应的字符串长度小于 1<<8
    uint8_t len; /* 已使用长度,1 字节存储 */
    uint8_t alloc; /* 总长度 */
    unsigned char flags; 
    char buf[]; // 真正存储字符串的数据空间
};
struct __attribute__ ((__packed__)) sdshdr16 { // 对应的字符串长度小于 1<<16
    uint16_t len; /* 已使用长度,2 字节存储 */
    uint16_t alloc; 
    unsigned char flags; 
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 { // 对应的字符串长度小于 1<<32
    uint32_t len; /* 已使用长度,4 字节存储 */
    uint32_t alloc; 
    unsigned char flags; 
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 { // 对应的字符串长度小于 1<<64
    uint64_t len; /* 已使用长度,8 字节存储 */
    uint64_t alloc; 
    unsigned char flags; 
    char buf[];
};

这样就可以针对不同长度的字符串申请相应的存储类型,从而有效的节约了内存使用。

2)数据类型 

我们可以使用 object encoding key 命令来查看对象(键值对)存储的数据类型,当我们使用此命令来查询 SDS 对象时,发现 SDS 对象竟然包含了三种不同的数据类型:int、embstr 和 raw 

① int 类型

127.0.0.1:6379> set key 666
OK
127.0.0.1:6379> object encoding key
"int"

② embstr 类型

127.0.0.1:6379> set key abc
OK
127.0.0.1:6379> object encoding key
"embstr"

③ raw 类型

127.0.0.1:6379> set key abcdefghigklmnopqrstyvwxyzabcdefghigklmnopqrs
OK
127.0.0.1:6379> object encoding key
"raw"

int 类型很好理解,整数类型对应的就是 int 类型,而字符串则对应是 embstr 类型,当字符串长度大于 44 字节时,会变为 raw 类型存储。

为什么是 44 字节?

在 Redis 中,如果 SDS 的存储值大于 64 字节时,Redis 的内存分配器会认为此对象为大字符串,并使用 raw 类型来存储,当数据小于 64 字节时(字符串类型),会使用 embstr 类型存储。既然内存分配器的判断标准是 64 字节,那为什么 embstr 类型和 raw 类型的存储判断值是 44 字节?

这是因为 Redis 在存储对象时,会创建此对象的关联信息,redisObject 对象头和 SDS 自身属性信息,这些信息都会占用一定的存储空间,因此长度判断标准就从 64 字节变成了 44 字节。

在 Redis 中,所有的对象都会包含 redisObject 对象头。我们先来看 redisObject 对象的源码:

typedef struct redisObject {
    unsigned type:4; // 4 bit
    unsigned encoding:4; // 4 bit
    unsigned lru:LRU_BITS; // 3 个字节
    int refcount; // 4 个字节
    void *ptr; // 8 个字节
} robj;

它的参数说明如下:

  • type:对象的数据类型,例如:string、list、hash 等,占用 4 bits 也就是半个字符的大小;
  • encoding:对象数据编码,占用 4 bits;
  • lru:记录对象的 LRU(Least Recently Used 的缩写,即最近最少使用)信息,内存回收时会用到此属性,占用 24 bits(3 字节);
  • refcount:引用计数器,占用 32 bits(4 字节);
  • *ptr:对象指针用于指向具体的内容,占用 64 bits(8 字节)。

redisObject 总共占用 0.5 bytes + 0.5 bytes + 3 bytes + 4 bytes + 8 bytes = 16 bytes(字节)。

了解了 redisObject 之后,我们再来看 SDS 自身的数据结构,从 SDS 的源码可以看出,SDS 的存储类型一共有 5 种:SDSTYPE5、SDSTYPE8、SDSTYPE16、SDSTYPE32、SDSTYPE64,在这些类型中最小的存储类型为 SDSTYPE5,但 SDSTYPE5 类型会默认转成 SDSTYPE8,以下源码可以证明,如下图所示:

SDS-0116-1.png

那我们直接来看 SDSTYPE8 的源码:

struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 1 byte
    uint8_t alloc; // 1 byte
    unsigned char flags; // 1 byte
    char buf[];
};

可以看出除了内容数组(buf)之外,其他三个属性分别占用了 1 个字节,最终分隔字符等于 64 字节,减去 redisObject 的 16 个字节,再减去 SDS 自身的 3 个字节,再减去结束符 \0 结束符占用 1 个字节,最终的结果是 44 字节(64-16-3-1=44),内存占用如下图所示:

44字节说明图.png

5 小结

本文介绍了字符串的定义及其使用,它的使用主要分为:单键值对操作、多键值对操作、数字统计、键值对过期操作、字符串操作进阶等。同时也介绍了字符串使用的三个场景,字符串类型可用作为:页面数据缓存,可以缓存一些文章详情信息等;数字计算与统计,例如计算页面的访问次数;也可以用作 Session 共享,用来记录管理员的登录信息等。同时我们深入的介绍了字符串的五种数据存储结构,以及字符串的三种内部数据类型,如下图所示:

字符串总结图.png

同时我们也知道了 embstr 类型向 raw 类型转化,是因为每个 Redis 对象都包含了一个 redisObject 对象头和 SDS 自身属性占用了一定的空间,最终导致数据类型的判断长度是 44 字节。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值