侧边栏壁纸
  • 累计撰写 244 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论
隐藏侧边栏

Redis 指令执行的原子性以及对管道和事务的支持

kaixindeken
2021-04-28 / 0 评论 / 1 点赞 / 110 阅读 / 2,764 字

通过 Lua 脚本保证操作序列的原子性

由于 Redis 通过单线程处理客户端请求,所以所有单个 Redis 指令的执行都是原子操作,但是从业务代码角度来说,获取指定键,然后对其进行简单判断和更新操作,就会至少涉及到两个 Redis 指令的执行。

我们通过伪代码演示这个操作流程:

value1 = get key
if (value1 >= num) {
    set key value2
}

可以看到,其中涉及到 GET 和 SET 两个 Redis 指令,这个整体的代码块就不是原子操作了,在执行这段代码的客户端获取到键值后,并进行更新前这段时间内,另一个客户端也更新了这个键值,则可能会导致数据的并发安全问题。

要解决这个问题,可以使用 Redis 支持的 Lua 脚本保证整个操作序列的原子性,因为 Redis 服务端会原子性地执行这个 Lua 脚本。我们可以通过 Redis 官方提供的 EVAL 指令执行 Lua 脚本,对应的命令格式如下:

EVAL script numkeys key [key ...] arg [arg ...]

其中 script 部分就是脚本内容,numkeys 表示键的数量,key 表示具体操作的键,arg 表示传入脚本的参数,键和参数都可以是多个,在脚本中通过数组下标进行访问(索引从 1 开始)。我们以上面的伪代码为例,通过 Lua 脚本可以这么实现:

eval 'if redis.call("get",KEYS[1]) >= ARGV[1] then return redis.call("set",KEYS[1],ARGV[2]) else return 0 end' 1 key num value2

这其实就是一个基于 Redis Lua 脚本实现的简单原子性 CAS 操作。

更多细节可以参考 Redis 官方文档

通过管道批量执行指令提升性能

我们知道在正常情况下,都是客户端每发送一个指令,服务端执行一次并将其返回,每次建立客户端与服务端的 TCP 连接开销很大,如果连续执行多个指令会降低程序的性能,所以 Redis 支持通过管道(Pipeline)一次性从客户端发送多个指令给服务端执行,然后再将处理结果批量返回。

不过需要注意的是,Redis 管道是一个客户端技术,与服务端无关,客户端将多个指令合并发送过来(由单个指令变成指令列表),服务端还是正常按照 RESP 协议去解析每个指令然后依次执行,并未感知到有什么异样,因此管道中的指令列表也不是像 Lua 脚本那样整体是个原子操作。

可以看到,管道主要是通过减少 TCP 连接来提升性能的,这在 Web 应用这种 IO 密集型项目中,效果非常显著,管道中的指令越多,性能提升越明显。我们可以通过 Redis 自带的压力测试工具 redis-benchmark 来对比测试下管道对性能的提升。

先来看普通单个 SET 指令的性能:

redis-benchmark -h 127.0.0.1 -p 6379 -a password -t set -q

QPS 大概是 6k/s(Redis 跑在内存为 2G 的 Docker 容器中):

1.jpeg

然后在上述基准测试命令中加入管道参数 -P,其默认值是 1,表示不使用管道,如果值为 2 表示单个管道内并行请求数量是 2:

redis-benchmark -h 127.0.0.1 -p 6379 -a password -t set -P 2 -q

现在 QPS 翻了近一倍,达到了 10k/s:

1.jpeg

性能提升非常显著确实不是浪得虚名!不过,随着 P 值的不断提高,最终 QPS 会趋于平缓,不再上升,因为 CPU 数量有限,意味着能够并行处理的请求数量也是也上限的:

1.jpeg

Redis 对事务的简单支持

作为一个 NoSQL 数据库,Redis 也和关系型数据库一样,提供了对事务的基本支持,不过,Redis 的事务并不严格遵循 ACID 特性,只是实现了事务的隔离性而已,并且这个隔离性还是基于 Redis 的单线程模型串行化执行指令保证的,连原子性都没有支持,即事务序列中的某个指令执行失败,并不会回滚整个操作序列,而是仍然可以执行后续指令。

接下来,我们来看一下 Redis 事务的简单实现。

在 MySQL 中,数据库事务通过 begin 开启,通过 commit 提交,通过 rollback 回滚,Redis 也提供了类似的指令,只是指令名称不一样,在 Redis 中,通过 multi 指令开启事务,通过 exec 提交事务并执行 multi 之后的指令序列:

1.jpeg

如果想要放弃该事务的执行,可以通过 discard 指令实现:

1.jpeg

我们也可以验证下 Redis 事务中某个指令执行失败并不影响后续指令执行:

1.jpeg

虽然第三条指令执行失败,依然不影响第四条指令的执行,所以 Redis 事务并不是原子性的。如果想要让指令执行失败后不再继续执行后续指令,可以通过前面介绍的 Lua 脚本来实现。

0

评论区