有人说 Redis 的 set 命令是一个危险命令。为什么会有这样的理解,我们一起来看看老外怎么说!



前言



Helicoprion(旋齿鲨)是一种现已灭绝但奇怪的动物,它生活在二叠纪早期的海洋中,它在尺寸和形状上与现代大白鲨看起来或多或少相似,它曾经是海洋中强大的掠夺者,因其下颌上长着排列成螺旋状的牙齿而得名,这有点类似于在下颚内部放置一个带有鲨鱼齿的圆锯,但这不符合生物的进化论,因此我们现在看不到这样的物种了。

在某些方面,Redis SET 命令就像旋齿鲨,但它仍然在全球范围内的大量 Redis 服务器上被使用。SET 是一个非常早期的命令,具有一些非常有用、不寻常的功能,但是在深度使用的同时可能存在风险。

SET 命令看起来很简单直接,我们在学习 Redis 的时候通常将它用作第一个命令,我们使用它进行简单的测试以确保 Redis 正常工作。比如:



> SET foo bar



表面上没什么特别的,但它隐藏了什么吗?



SET命令:一种数据的破坏者



回到我们简单的 SET 示例。让我们模拟一个更复杂的场景:



> UNLINK foo	
(integer) 1	
> HSET foo bar 123	
(integer) 1	
> SET foo bar	
OK



有没有用 SET 发现这里的怪异?现有的键 foo 是 hash 类型(由于 HSET),但是当我之后立即运行 SET 时它仍然可以正常工作。与其他Redis命令相比,这实际上非常奇怪。让我们采取相同的命令,但对最后两个命令使用相反的顺序:



> UNLINK foo	
(integer) 1	
> SET foo bar	
OK	
> HSET foo bar 123	
(error) WRONGTYPE Operation against a key holding thewrong kind of value



您可以看到 SET 忽略键的存在或类型,并始终写入,另一方面,哈希在面对不同类型的非空键时会抛出错误。除了字符串之外的所有数据类型都是如此,特别是 SET 命令和一些衍生的命令(PSETEX,SETEX,MSET),例如:



> HSET foo bar 123	
(integer) 1	
> APPEND foo bar	
(error) WRONGTYPE Operation against a key holding thewrong kind of value	
> INCR foo	
(error) WRONGTYPE Operation against a key holding thewrong kind of value	
> SETBIT foo 1 1	
(error) WRONGTYPE Operation against a key holding thewrong kind of value	
> BITFIELD foo SET u8 0 1	
(error) WRONGTYPE Operation against a key holding thewrong kind of value	
> INCRBY foo 1	
(error) WRONGTYPE Operation against a key holding thewrong kind of value	
> INCRBYFLOAT foo 1	
(error) WRONGTYPE Operation against a key holding thewrong kind of value	
> SETRANGE foo 1 barbar	
(error) WRONGTYPE Operation against a key holding thewrong kind of value



SETNX 和 SET ... NX(稍后会详细介绍)是一个有趣的旁注,如果键不存在,它们将设置 SET,如果设置了则返回 1,否则返回 0。因此,它不会进行类型检查,而是进行状态检查。

总而言之,SET 不关心键是什么类型,它都会覆盖,很少有其他命令能够挡住它前进的道路。



一种TYPE,三种类型



如果您已经在 Redis 中遇到过几次,那么您就知道可以使用 TYPE 命令查询存储在键中的数据类型。举个例子,让我们回到 foo:



> SET foo bar	
OK	
> TYPE foo	
string



您可以很容易地在键 foo 上设置值“bar”。现在让我们看看别的东西:



> SET foo 1234	
OK	
> TYPE foo	
String	
> GETRANGE foo 2 3	
"34"



因此,您可能会认为数字 1234 被存储为字符,这时候可以说你的想法或多或少是正确的,但是,请继续往下看:



> INCR foo	
(integer) 1235	
> GETRANGE foo 2 3	
"35"



这说明 Redis 将字符理解为文本和数字 - 您可以将其视为松散的类型, 但它很奇怪:



> SET foo "hello world"	
OK	
> INCR foo	
(error) ERR value is not an integer or out of range



显然,Redis 不能对非数字执行 INCR。但请继续看, Redis 也能理解浮点值,举个例子:



> SET foo 1.2	
OK	
> INCR foo	
(error) ERR value is not an integer or out of range	
> INCRBYFLOAT foo 0.8	
"2"	
> INCR foo	
(integer) 3



您可以看到初始值是作为 float 放入的,因此 INCR(对于整数)将不起作用,但是,INCRBYFLOAT 确实有效,这 1.2 + 0.8 = 2 会将值更改为前一个 INCR 命令允许使用的整数。



一个命令,多种参数



该命令的另一个独特之处在于能够提供两类可选参数:一类用于到期,另一类用于存在检查。我们来看看第一个类别:到期参数。

对于大多数命令,如果要立即使键过期,则需要立即发出 EXPIRE 或 PEXPIRE,最常见的是在 MULTI / EXEC 事务中。例如:



> MULTI	
OK	
> SADD baz alpha beta gamma	
QUEUED	
> EXPIRE baz 10	
QUEUED	
> EXEC	
1) (integer) 3	
2) (integer) 1



这样可以确保在 SADD 和 EXPIRE 命令之间不会有其他命令执行,在 EXEC 执行完成之后你会有一个在 10 秒之后过期的集合。但是,使用SET,您可以在没有事务的情况下达到这样的效果。



> SET foo bar EX 10	
OK



或者,您可以使用 PX 而不是 EX 参数,使得来以毫秒而不是秒为单位到期。这是一个小技巧,也可以用 SETEX 和 PSETEX 实现, 我认为这些命令在提供便利的同时会降低可读性和灵活性。

另一类参数 NX / XX 可以控制SET命令在键存在或不存在时的行为,仅当键不存在时,NX 的键才会设置值。举个例子:



> UNLINK foo	
(integer) 0	
> SET foo 1234 NX	
OK	
> GET foo	
"1234"	
> SET foo 5678 NX	
(nil)	
> GET foo	
"1234"



您可以看到第 4 个命令实际上没有做任何事情,因为键 foo 已经存在。这有很多用途:设置默认值而不覆盖现有数据,防止在用户在误操作输入时对键的以外覆盖。

与此相反的是 XX 命令,这仅在键已存在时设置值:



> UNLINK foo	
(integer) 1	
> SET foo 1234 XX	
(nil)	
> set foo 1234	
OK	
> SET foo 5678 XX	
OK



这可用于将写入限制在已经定义过的键上。一种使用这个命令的场景是输入检查,只有在已经输入的情况下,才会把 Key 覆盖。



那么SET是一种危险的、不好的、不建议使用的命令?



绝对不是这样的,SET 是 Redis 中许多优秀实践的基础,但是它同时也具有许多与 Redis 其他命令根本不同的特性,重要的是要了解这些功能和命令如何工作,在此基础之上才能更好地组织 Redis 的键空间,以及在应用程序中正确的使用 Redis。