字符串 string 是Redis 最简单的数据结构。Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一 key 值来获取相应的 value 数据。

1.常用命令

1.增(set)

set key value 						// 存入字符串键值对
mset key value [key value...] 	    // 批量存储字符串键值对
setnx key value 					// 存入一个不存在的键值对(只有当前key不存在时才能增加成功)

2.删除与过期

del key [key...]					// 删除一个键
expire key seconds 				    // 设置过期时间(秒)

3.原子操作

incr key 							 // 将key中存储的数字加1
incrby key increment 				 // 将key锁存储的值加上increment

decr key							 // 将key中存储的数字减1
decr key decrement                   // 将key所存储的值减去decrement

4.查(get)

get key                               // 获取一个字符串的键值
mget key [key...]					  // 批量获取字符串键值

2.应用示例

1.对象缓存

  • 方式一:利用set命令,一次将对象的json字符串存入
set user:1 value(对象的json字符串)
  • 方式二:利用mset将对象的各个字段分开存储。这种方案更加灵活,可以对指定字段进行操作
mset user:1:name zhangsan user:1:age 18
mget user:1:name ~~ user:1:age ~~

2.简单分布式锁

通过setnx只能新增不存在的可以进行简单的分布式锁

PS:分布式锁问题就是放大版的单机线程同步问题,这不过这里不再是一个个线程,而是一台台独立的服务器(进程)。

setnx product true 			// 进程一设置成功 == 获取到锁
setnx product true 			// 进程二再来时设置失败 == 阻塞

......						// 操作临界资源

del product 				// 进程一释放锁

注意:一般最好设置过期时间来避免死锁

set product true ex 10 nx 	// 过期时间=10s

3.计数器

java redis hash 删除所有啊 redis 删除string_Redis

incr article:readcount:{文章id}
get article:readcount:{文章id}

4.Web集群Session共享

spring session + redis 实现session共享

3.存储原理

字符串类型的内部编码有三种:

  • int:存储 8 个字节的长整型(long,2^63-1)
  • embstr:代表 embstr 格式的 SDS, 存储小于 44 个字节的字符串
  • raw:代表 raw 格式的 SDS,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)

Redis 使用 C 语言编写,但是并没有直接使用 C 语言自带的字符串,而是使用了 SDS 来管理字符串。SDS 全称 Simple Dynamic String,即简单动态字符串。SDS 组成部分如下:

/* sds.h */
struct __attribute__ ((__packed__)) sdshdr8 {

	/* 当前字符数组的长度 */
    uint8_t len; 
    /* 表示 buf 中的空闲的空间大小 */
    uint8_t free; 
    /* 当前字符数组的属性、用来标识到底是 sdshdr8 还是 sdshdr16 等 */
    unsigned char flags; 
    /* char 类型的数组,用于存储实际字符串的内容 */
    char buf[]; 
    
};

注:在 3.2 以后的版本中,SDS 又有多种结构(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 2^5=32byte, 2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB

来看 SDS 的简单示意图:

java redis hash 删除所有啊 redis 删除string_字符数组_02

问题一:为什么 Redis 要用 SDS 实现字符串?

我们知道,C 语言本身没有字符串类型(只能用字符数组 char[]实现)

  • 使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。
  • 如果要获取字符长度,必须遍历字符数组,时间复杂度是 O(n)。
  • C 字符串长度的变更会对字符数组做内存重分配。
  • 通过从字符串开始到结尾碰到的第一个’\0’来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。

SDS 的特点:

  • 不用担心内存溢出问题,如果需要会对 SDS 进行扩容。
  • 获取字符串长度时间复杂度为 O(1),因为定义了 len 属性。
  • 通过“空间预分配”( sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存。
  • 判断是否结束的标志是 len 属性(它同样以’\0’结尾是因为这样就可以使用 C 语言中函数库操作字符串的函数了),可以包含’\0’。

空间预分配:;SDS 会预先分配一部分空闲空间,当字符串内容添加时不需要做空间申请的工作

java redis hash 删除所有啊 redis 删除string_字符数组_03

空间惰性释放:当字符串从 buf 数组中移除时,空闲出来的空间不会立马被内存回收,防止新增字符串的内容写入时空间不够而临时申请空间

java redis hash 删除所有啊 redis 删除string_Redis_04

C 字符串

SDS

获取字符串长度的复杂度为 O(N)

获取字符串长度的复杂度为 O(1)

API 是不安全的,可能会造成缓冲区溢出

API 是安全的,不会早晨个缓冲区溢出

修改字符串长度 N 次必然需要执行 N 次内存重分配

修改字符串长度 N 次最多需要执行 N 次内存重分配

只能保存文本数据

可以保存文本或者二进制数据

可以使用所有库中的函数

可以使用一部分库中的函数

问题二: embstr 和 raw 的区别?

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

因此与 raw 相比,embstr 的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。 而 embstr 的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个 RedisObject 和 SDS 都需要重新分配空间,因此 Redis 中的 embstr 实现为只读。

问题三:int 和 embstr 什么时候转化为 raw?

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

127.0.0.1:6379> set k1 1
OK
127.0.0.1:6379> append k1 a
(integer) 2
127.0.0.1:6379> object encoding k1
"raw"

问题四:明明没有超过阈值,为什么变成 raw?

127.0.0.1:6379> set k2 a
OK
127.0.0.1:6379> object encoding k2
"embstr" 
127.0.0.1:6379> append k2 b
(integer) 2
127.0.0.1:6379> object encoding k2
"raw"

对于 embstr,由于其实现是只读的,因此在对 embstr 对象进行修改时,都会先转化为 raw 再进行修改。 因此,只要是修改 embstr 对象,修改后的对象一定是 raw 的,无论是否达到了 44 个字节。

问题五:当长度小于阈值时,会还原吗?

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