有人说 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。