面试官:Redis的事务满足原子性吗?

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 面试官:Redis的事务满足原子性吗?

谈起数据库的事务来,估计很多同学的第一反应都是ACID,而排在ACID中首位的A原子性,要求一个事务中的所有操作,要么全部完成,要么全部不完成。熟悉redis的同学肯定知道,在redis中也存在事务,那么它的事务也满足原子性吗?下面我们就来一探究竟。

什么是Redis事务?

和数据库事务类似,redis事务也是用来一次性地执行多条命令。使用起来也很简单,可以用MULTI开启一个事务,然后将多个命令入队到事务的队列中,最后由EXEC命令触发事务,执行事务中的所有命令。看一个简单的事务执行例子:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name Hydra
QUEUED
127.0.0.1:6379> set age 18
QUEUED
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) (integer) 19

可以看到,在指令和操作数的数据类型等都正常的情况下,输入EXEC后所有命令被执行成功。

Redis事务满足原子性吗?

如果要验证redis事务是否满足原子性,那么需要在redis事务执行发生异常的情况下进行,下面我们分两种不同类型的错误分别测试。

语法错误

首先测试命令中有语法错误的情况,这种情况多为命令的参数个数不正确或输入的命令本身存在错误。下面我们在事务中输入一个存在格式错误的命令,开启事务并依次输入下面的命令:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name Hydra
QUEUED
127.0.0.1:6379> incr
(error) ERR wrong number of arguments for 'incr' command
127.0.0.1:6379> set age 18
QUEUED

输入的命令incr后面没有添加参数,属于命令格式不对的语法错误,这时在命令入队时就会立刻检测出错误并提示error。使用exec执行事务,查看结果输出:

127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

在这种情况下,只要事务中的一条命令有语法错误,在执行exec后就会直接返回错误,包括语法正确的命令在内的所有命令都不会被执行。对此进行验证,看一下在事务中其他指令执行情况,查看set命令的执行结果,全部为空,说明指令没有被执行。

127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> get age
(nil)

此外,如果存在命令本身拼写错误、或输入了一个不存在的命令等情况,也属于语法错误的情况,执行事务时会直接报错。

运行错误

运行错误是指输入的指令格式正确,但是在命令执行期间出现的错误,典型场景是当输入参数的数据类型不符合命令的参数要求时,就会发生运行错误。例如下面的例子中,对一个string类型的值执行列表的操作,报错如下:

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> lpush key1 value2
(error) WRONGTYPE Operation against a key holding the wrong kind of value

这种错误在redis实际执行指令前是无法被发现的,只能当真正执行才能够被发现,因此这样的命令是可以被事务队列接收的,不会和上面的语法错误一样立即报错。

具体看一下当事务中存在运行错误的情况,在下面的事务中,尝试对string类型数据进行incr自增操作:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name Hydra
QUEUED
127.0.0.1:6379> set age eighteen
QUEUED
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> del name
QUEUED

redis一直到这里都没有提示存在错误,执行exec看一下结果输出:

127.0.0.1:6379> exec
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) (integer) 1

运行结果可以看到,虽然incr age这条命令出现了错误,但是它前后的命令都正常执行了,再看一下这些key对应的值,确实证明了其余指令都执行成功:

127.0.0.1:6379> get name
(nil)
127.0.0.1:6379> get age
"eighteen"

阶段性结论

对上面的事务的运行结果进行一下分析:

存在语法错误的情况下,所有命令都不会执行

存在运行错误的情况下,除执行中出现错误的命令外,其他命令都能正常执行

通过分析我们知道了redis中的事务是不满足原子性的,在运行错误的情况下,并没有提供类似数据库中的回滚功能。那么为什么redis不支持回滚呢,官方文档给出了说明,大意如下:

redis命令失败只会发生在语法错误或数据类型错误的情况,这一结果都是由编程过程中的错误导致,这种情况应该在开发环境中检测出来,而不是生产环境

不使用回滚,能使redis内部设计更简单,速度更快

回滚不能避免编程逻辑中的错误,如果想要将一个键的值增加2却只增加了1,这种情况即使提供回滚也无法提供帮助

基于以上原因,redis官方选择了更简单、更快的方法,不支持错误回滚。这样的话,如果在我们的业务场景中需要保证原子性,那么就要求了开发者通过其他手段保证命令全部执行成功或失败,例如在执行命令前进行参数类型的校验,或在事务执行出现错误时及时做事务补偿。

提到其他方式,相信很多小伙伴都听说使用Lua脚本来保证操作的原子性,例如在分布式锁中通常使用的就是Lua脚本,那么,神奇的Lua脚本真的能保证原子性吗?

简单的Lua脚本入门

在验证lua脚本的原子性之前,我们需要对它做一个简单的了解。redis从2.6版本开始支持执行lua脚本,它的功能和事务非常类似,一段lua脚本被视作一条命令执行,这样将多条redis命令写入lua,即可实现类似事务的执行结果。我们先看一下下面几个常用的命令。

EVAL 命令

最常用的EVAL用于执行一段脚本,它的命令的格式如下:

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

简单解释一下其中的参数:

script是一段lua脚本程序

numkeys指定后续参数有几个key,如没有key则为0

key [key …]表示脚本中用到的redis中的键,在lua脚本中通过KEYS[i]的形式获取

arg [arg …]表示附加参数,在lua脚本中通过ARGV[i]获取

看一个简单的例子:

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 value1 vauel2
1) "key1"
2) "key2"
3) "value1"
4) "vauel2"

在上面的命令中,双引号中是lua脚本程序,后面的2表示存在两个key,分别是key1key2,之后的参数是附加参数value1value2

如果想要使用lua脚本执行set命令,可以写成这样:

127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]);" 1 name Hydra
(nil)

这里使用了redis内置的lua函数redis.call来完成set命令,这里打印的执行结果nil是因为没有返回值,如果不习惯的话,其实我们可以在脚本中添加return 0;的返回语句。

SCRIPT LOAD 和 EVALSHA命令

这两个命令放在一起是因为它们一般成对使用。先看SCRIPT LOAD,它用于把脚本加载到缓存中,返回SHA1校验和,这时候只是缓存了命令,但是命令没有被马上执行,看一个例子:

127.0.0.1:6379> SCRIPT LOAD "return redis.call('GET', KEYS[1]);"
"228d85f44a89b14a5cdb768a29c4c4d907133f56"

这里返回了一个SHA1的校验和,接下来就可以使用EVALSHA来执行脚本了:

127.0.0.1:6379> EVALSHA "228d85f44a89b14a5cdb768a29c4c4d907133f56" 1 name
"Hydra"

这里使用这个SHA1值就相当于导入了上面缓存的命令,在之后再拼接numkeyskeyarg等参数,命令就能够正常执行了。

其他命令

使用SCRIPT EXISTS命令判断脚本是否被缓存:

127.0.0.1:6379> SCRIPT EXISTS 228d85f44a89b14a5cdb768a29c4c4d907133f56
1) (integer) 1

使用SCRIPT FLUSH命令清除redis中的lua脚本缓存:

127.0.0.1:6379> SCRIPT FLUSH
OK
127.0.0.1:6379> SCRIPT EXISTS 228d85f44a89b14a5cdb768a29c4c4d907133f56
1) (integer) 0

可以看到,执行了SCRIPT FLUSH后,再次通过SHA1值查看脚本时已经不存在。最后,还可以使用SCRIPT KILL命令杀死当前正在运行的 lua 脚本,但是只有当脚本没有执行写操作时才会生效。

从这些操作看来,lua脚本具有下面的优点:

多次网络请求可以在一次请求中完成,减少网络开销,减少了网络延迟

客户端发送的脚本会存在redis中,其他客户端可以复用这一脚本,而不需要再重复编码完成相同的逻辑

Java代码中使用lua脚本

在Java代码中可以使用Jedis中封装好的API来执行lua脚本,下面是一个使用Jedis执行lua脚本的例子:

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    String script="redis.call('SET', KEYS[1], ARGV[1]);"
            +"return redis.call('GET', KEYS[1]);";
    List<String> keys= Arrays.asList("age");
    List<String> values= Arrays.asList("eighteen");
    Object result = jedis.eval(script, keys, values);
    System.out.println(result);
}

执行上面的代码,控制台打印了get命令返回的结果:

eighteen

简单的铺垫完成后,我们来看一下lua脚本究竟能否实现回滚级别的原子性。对上面的代码进行改造,插入一条运行错误的命令:

public static void main(String[] args) {
    Jedis jedis = new Jedis("127.0.0.1", 6379);
    String script="redis.call('SET', KEYS[1], ARGV[1]);"
            +"redis.call('INCR', KEYS[1]);"
            +"return redis.call('GET', KEYS[1]);";
    List<String> keys= Arrays.asList("age");
    List<String> values= Arrays.asList("eighteen");
    Object result = jedis.eval(script, keys, values);
    System.out.println(result);
}

查看执行结果:

image.png

再到客户端执行一下get命令:

127.0.0.1:6379> get age
"eighteen"

也就是说,虽然程序抛出了异常,但异常前的命令还是被正常的执行了且没有被回滚。再试试直接在redis客户端中运行这条指令:

127.0.0.1:6379> flushall
OK
127.0.0.1:6379> eval "redis.call('SET', KEYS[1], ARGV[1]);redis.call('INCR', KEYS[1]);return redis.call('GET', KEYS[1])" 1 age eight
(error) ERR Error running script (call to f_c2ea9d5c8f60735ecbedb47efd42c834554b9b3b): @user_script:1: ERR value is not an integer or out of range
127.0.0.1:6379> get age
"eight"

同样,错误之前的指令仍然没有被回滚,那么我们之前经常听说的Lua脚本保证原子性操作究竟是怎么回事呢?

其实,在redis中是使用的同一个lua解释器来执行所有命令,也就保证了当一段lua脚本在执行时,不会有其他脚本或redis命令同时执行,保证了操作不会被其他指令插入或打扰,实现的仅仅是这种程度上的原子操作。

但是遗憾的是,如果lua脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了lua脚本,也不能实现类似数据库回滚的原子性。

image.png


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
27天前
|
缓存 NoSQL Redis
Redis 事务
10月更文挑战第18天
24 1
|
8天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
1月前
|
存储 NoSQL Java
可能是最漂亮的Redis面试基础详解
我是南哥,相信对你通关面试、拿下Offer有所帮助。敲黑板:本文总结了Redis基础最常见的面试题!包含了Redis五大基本数据类型、Redis内存回收策略、Redis持久化等。相信大部分Redis初学者都会忽略掉一个重要的知识点,Redis其实是单线程模型。我们按直觉来看应该是多线程比单线程更快、处理能力更强才对,比如单线程一次只可以做一件事情,而多线程却可以同时做十件事情。但Redis却可以做到每秒万级别的处理能力,主要是基于以下原因:(1)Redis是基于内存操作的,Redis所有的数据库状态都保存在
可能是最漂亮的Redis面试基础详解
|
1月前
|
SQL 关系型数据库 MySQL
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
尼恩,一位40岁的资深架构师,通过其丰富的经验和深厚的技術功底,为众多读者提供了宝贵的面试指导和技术分享。在他的读者交流群中,许多小伙伴获得了来自一线互联网企业的面试机会,并成功应对了诸如事务ACID特性实现、MVCC等相关面试题。尼恩特别整理了这些常见面试题的系统化解答,形成了《MVCC 学习圣经:一次穿透MYSQL MVCC》PDF文档,旨在帮助大家在面试中展示出扎实的技术功底,提高面试成功率。此外,他还编写了《尼恩Java面试宝典》等资料,涵盖了大量面试题和答案,帮助读者全面提升技术面试的表现。这些资料不仅内容详实,而且持续更新,是求职者备战技术面试的宝贵资源。
阿里面试:MYSQL 事务ACID,底层原理是什么? 具体是如何实现的?
|
1月前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
1月前
|
NoSQL 算法 Redis
Redis面试篇
Redis面试篇
36 5
|
1月前
|
缓存 NoSQL Java
Java中redis面试题
Java中redis面试题
37 1
|
1月前
|
SQL 分布式计算 NoSQL
大数据-42 Redis 功能扩展 发布/订阅模式 事务相关的内容 Redis弱事务
大数据-42 Redis 功能扩展 发布/订阅模式 事务相关的内容 Redis弱事务
25 2
|
20天前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
1月前
|
NoSQL Java 关系型数据库
阿里 P7二面:Redis 执行 Lua,到底能不能保证原子性?
Redis 和 Lua,两个看似风流马不相及的技术点,为何能产生“爱”的火花,成为工作开发中的黄金搭档?技术面试中更是高频出现,Redis 执行 Lua 到底能不能保证原子性?今天就来聊一聊。 
83 1