【高频 Redis 面试题】Redis 事务是否具备原子性?
一、Redis 事务的实现原理
一个事务从开始到结束通常会经历以下三个阶段:
1、事务开始
客户端发送 MULTI 命令,服务器执行 MULTI 命令逻辑。
服务器会在客户端状态(redisClient)的 flags
属性打开 REDIS_MULTI 标识,将客户端从非事务状态切换到事务状态。
void multiCommand(redisClient *c) {
// 不能在事务中嵌套事务
if (c->flags & REDIS_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
// 打开事务 FLAG
c->flags |= REDIS_MULTI;
addReply(c,shared.ok);
}
2、命令入队
接着,用户可以在客户端输入当前事务要执行的多个命令。
当客户端切换到事务状态时,服务器会根据客户端发来的命令来执行不同的操作。
如果客户端发送的命令为 EXEC、DISCARD、WATCH、MULTI 四个命令的其中一个,那么服务器立即执行这个命令。
与此相反,如果客户端发送的命令是 EXEC、DISCARD、WATCH、MULTI 四个命令以外的其他命令,那么服务器并不立即执行这个命令。
- 首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的
flags
属性打开 REDIS_MULTI 标识,并且返回错误信息给客户端。 - 如果正确将这个命令放入一个事务队列里面,然后向客户端返回 QUEUED 回复。
- 首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的
我们先看看事务队列是如何实现的?
每个 Redis 客户端都有自己的事务状态,对应的是客户端状态(redisClient)的 mstate
属性。
typeof struct redisClient{
// 事务状态
multiState mstate;
}redisClient;
事务状态(mstate)包含一个事务队列(FIFO 队列),以及一个已入队命令的计数器。
/*
* 事务状态
*/
typedef struct multiState {
// 事务队列,FIFO 顺序
multiCmd *commands; /* Array of MULTI commands */
// 已入队命令计数
int count; /* Total number of MULTI commands */
int minreplicas; /* MINREPLICAS for synchronous replication */
time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */
} multiState;
事务队列是一个 multiCmd
类型数组,数组中每个 multiCmd
结构都保存了一个如入队命令的相关信息:指向命令实现函数的指针,命令的参数,以及参数的数量。
/*
* 事务命令
*/
typedef struct multiCmd {
// 参数
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
} multiCmd;
最后我们再看看入队列的源码:
/* Add a new command into the MULTI commands queue
*
* 将一个新命令添加到事务队列中
*/
void queueMultiCommand(redisClient *c) {
multiCmd *mc;
int j;
// 为新数组元素分配空间
c->mstate.commands = zrealloc(c->mstate.commands,
sizeof(multiCmd)*(c->mstate.count+1));
// 指向新元素
mc = c->mstate.commands+c->mstate.count;
// 设置事务的命令、命令参数数量,以及命令的参数
mc->cmd = c->cmd;
mc->argc = c->argc;
mc->argv = zmalloc(sizeof(robj*)*c->argc);
memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);
for (j = 0; j < c->argc; j++)
incrRefCount(mc->argv[j]);
// 事务命令数量计数器增一
c->mstate.count++;
}
当然了,还有我们上面提到的,如果命令入队出错时,会打开客户端状态的 REDIS_DIRTY_EXEC
标识。
/* Flag the transacation as DIRTY_EXEC so that EXEC will fail.
*
* 将事务状态设为 DIRTY_EXEC ,让之后的 EXEC 命令失败。
*
* Should be called every time there is an error while queueing a command.
*
* 每次在入队命令出错时调用
*/
void flagTransaction(redisClient *c) {
if (c->flags & REDIS_MULTI)
c->flags |= REDIS_DIRTY_EXEC;
}
3、事务执行
客户端发送 EXEC 命令,服务器执行 EXEC 命令逻辑。
- 如果客户端状态的 flags 属性不包含
REDIS_MULTI
标识,或者包含REDIS_DIRTY_CAS
或者REDIS_DIRTY_EXEC
标识,那么就直接取消事务的执行。 - 否则客户端处于事务状态(flags 有
REDIS_MULTI
标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端;
void execCommand(redisClient *c) {
int j;
robj **orig_argv;
int orig_argc;
struct redisCommand *orig_cmd;
int must_propagate = 0; /* Need to propagate MULTI/EXEC to AOF / slaves? */
// 客户端没有执行事务
if (!(c->flags & REDIS_MULTI)) {
addReplyError(c,"EXEC without MULTI");
return;
}
/* Check if we need to abort the EXEC because:
*
* 检查是否需要阻止事务执行,因为:
*
* 1) Some WATCHed key was touched.
* 有被监视的键已经被修改了
*
* 2) There was a previous error while queueing commands.
* 命令在入队时发生错误
* (注意这个行为是 2.6.4 以后才修改的,之前是静默处理入队出错命令)
*
* A failed EXEC in the first case returns a multi bulk nil object
* (technically it is not an error but a special behavior), while
* in the second an EXECABORT error is returned.
*
* 第一种情况返回多个批量回复的空对象
* 而第二种情况则返回一个 EXECABORT 错误
*/
if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr :
shared.nullmultibulk);
// 取消事务
discardTransaction(c);
goto handle_monitor;
}
/* Exec all the queued commands */
// 已经可以保证安全性了,取消客户端对所有键的监视
unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
// 因为事务中的命令在执行时可能会修改命令和命令的参数
// 所以为了正确地传播命令,需要现备份这些命令和参数
orig_argv = c->argv;
orig_argc = c->argc;
orig_cmd = c->cmd;
addReplyMultiBulkLen(c,c->mstate.count);
// 执行事务中的命令
for (j = 0; j < c->mstate.count; j++) {
// 因为 Redis 的命令必须在客户端的上下文中执行
// 所以要将事务队列中的命令、命令参数等设置给客户端
c->argc = c->mstate.commands[j].argc;
c->argv = c->mstate.commands[j].argv;
c->cmd = c->mstate.commands[j].cmd;
/* Propagate a MULTI request once we encounter the first write op.
*
* 当遇上第一个写命令时,传播 MULTI 命令。
*
* This way we'll deliver the MULTI/..../EXEC block as a whole and
* both the AOF and the replication link will have the same consistency
* and atomicity guarantees.
*
* 这可以确保服务器和 AOF 文件以及附属节点的数据一致性。
*/
if (!must_propagate && !(c->cmd->flags & REDIS_CMD_READONLY)) {
// 传播 MULTI 命令
execCommandPropagateMulti(c);
// 计数器,只发送一次
must_propagate = 1;
}
// 执行命令
call(c,REDIS_CALL_FULL);
/* Commands may alter argc/argv, restore mstate. */
// 因为执行后命令、命令参数可能会被改变
// 比如 SPOP 会被改写为 SREM
// 所以这里需要更新事务队列中的命令和参数
// 确保附属节点和 AOF 的数据一致性
c->mstate.commands[j].argc = c->argc;
c->mstate.commands[j].argv = c->argv;
c->mstate.commands[j].cmd = c->cmd;
}
// 还原命令、命令参数
c->argv = orig_argv;
c->argc = orig_argc;
c->cmd = orig_cmd;
// 清理事务状态
discardTransaction(c);
/* Make sure the EXEC command will be propagated as well if MULTI
* was already propagated. */
// 将服务器设为脏,确保 EXEC 命令也会被传播
if (must_propagate) server.dirty++;
handle_monitor:
/* Send EXEC to clients waiting data from MONITOR. We do it here
* since the natural order of commands execution is actually:
* MUTLI, EXEC, ... commands inside transaction ...
* Instead EXEC is flagged as REDIS_CMD_SKIP_MONITOR in the command
* table, and we do it here with correct ordering. */
if (listLength(server.monitors) && !server.loading)
replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}
二、为什么很多人说 Redis 事务为何不支持原子性?
1、Redis 事务不支持事务回滚机制
Redis 事务执行过程中,如果一个命令执行出错,那么就返回错误,然后还是会接着继续执行下面的命令。
下面我们演示一下:
正是因为 Redis 事务不支持事务回滚机制,如果事务执行中出现了命令执行错误(例如对 String 类型的数据库键执行 LPUSH 操作),只会返回当前命令执行的错误给客户端,并不会影响下面的命令的执行。所以很多人觉得和关系型数据库(MySQL) 不一样,而 MySQL 的事务是具有原子性的,所以大家都认为 Redis 事务不支持原子性。
2、但是其实 Redis 意义上是支持原子性的。
正常情况下,它也是要不所有命令执行成功,要不一个命令都不执行。
我们下面演示一下:
全部执行成功的:
一个都不执行:
这就是上面提到的,在事务开始后,用户可以输入事务要执行的命令;在命令入事务队列前,会对命令进行检查,如果命令不存在或者是命令参数不对,则会返回错误可客户端,并且修改客户端状态。
当后面客户端执行 EXEC 命令时,服务器就会直接拒绝执行此事务了。
所以说,Redis 事务其实是支持原子性的!即使 Redis 不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。
但是我们要注意一个点就是:Redis 事务不支持检查那些程序员自己逻辑错误。例如对 String 类型的数据库键执行对 HashMap 类型的操作!
我很赞同 Redis 作者的想法:
首先,MySQL 和 Redis 的定位不一样,一个是关系型数据库,一个是 NoSQL。
MySQL 的 SQL 查询是可以相当复杂的,而且 MySQL 没有事务队列这种说法,SQL 真正开始执行才会进行分析和检查,MySQL 不可能提前知道下一条 SQL 是否正确。所以支持事务回滚是非常有必要的~
但是,Redis 使用了事务队列来预先将执行命令存储起来,并且会对其进行格式检查的,提前就知道命令是否可执行了。所以如果只要有一个命令是错误的,那么这个事务是不能执行的。
Redis 作者认为基本只会出现在开发环境的编程错误其实在生产环境基本是不可能出现的(例如对 String 类型的数据库键执行 LPUSH 操作),所以他觉得没必要为了这事务回滚机制而改变 Redis 追求简单高效的设计主旨。
所以最后,其实 Redis 事务真正支持原子性的前提:开发者不要傻不拉几的写有逻辑问题的代码!
参考资料:《Redis 设计与实现》、《Redis 源码》
【高频 Redis 面试题】Redis 事务是否具备原子性?的更多相关文章
- Redis系列(七)Redis面试题
Redis 系列: Redis系列(一)Redis入门 Redis系列(二)Redis的8种数据类型 Redis系列(三)Redis的事务和Spring Boot整合 Redis系列(四)Redis配 ...
- Web 全栈开发 Redis 面试题
Web 全栈开发 Redis 面试题 Redis 大 key 问题 https://www.cnblogs.com/xgqfrms/p/13601959.html Redis 数据结构 Redis 初 ...
- 面试前必知Redis面试题—缓存雪崩+穿透+缓存与数据库双写一致问题
今天来分享一下Redis几道常见的面试题: 如何解决缓存雪崩? 如何解决缓存穿透? 如何保证缓存与数据库双写时一致的问题? 一.缓存雪崩 1.1什么是缓存雪崩? 回顾一下我们为什么要用缓存(Redis ...
- python面试题:redis数据库
来源链接: https://www.cnblogs.com/jasontec/p/9699242.html https://www.cnblogs.com/Java3y/p/10266306.html ...
- Redis 面试题 - 收藏版 (持续更新、吐血推荐)
文章很长,建议收藏起来,慢慢读! 疯狂创客圈为小伙伴奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : <Netty Zookeeper Redis 高并发实战> 面试必备 + 大厂必备 ...
- 史上最全Redis面试题(2020最新版)
一个执着于技术的公众号 导读:2020 年最新版 Redis面试题,两万字干货,为方便读者阅读,已整理为PDF文档,后台回复『redis』即可领取.希望对大家有帮助! 概述 1. 什么是Redis? ...
- Redis 的事务到底是不是原子性的
ACID 中关于原子性的定义: 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节.事务在执行过程中发生错误,会被恢复(Rollback)到事 ...
- 面试题-Redis、MongoDB、Memcached[转]
https://blog.csdn.net/gangsijay888/article/details/81213811 一.缓存 搞懂缓存那些事:https://blog.csdn.net/a7248 ...
- Redis 面试题(持续更新)
前言 看了一圈,发现Redis的面试题主要问的是如下几块: 原理 用处(缓存/队列 包括Pub.Sub/计数器/排行榜等) 基本操作与数据类型 消息队列 且与其它消息队列的区别 主从备份 宕机如何处理 ...
随机推荐
- stm32CubeMx+TrueSTUDIO+uc/os-III移植开发(二)
(三)复制相关文件 (1)继上次的代码生成后会显示如下的图 点击第一个,打开文件夹如下 (2)新建文件夹UCOSIII 在UCOSIII文件夹下,新建如下的文件夹 (3)将uc/os源文件中 Micr ...
- 旷视6号员工范浩强:高二开始实习,“兼职”读姚班,25岁在CVPR斩获第四个世界第一...
初来乍到,这个人说话容易让人觉得"狂". "我们将比赛结果提交上去,果不其然,是第一名的成绩."当他说出这句话的时候,表情没有一丝波澜,仿佛一切顺理成章. 他说 ...
- Centos 8 上安装 Consul
/* 1. 下载二进制安装文件 */下载地址:https://www.consul.io/downloads.html /* 2. 解压缩安装包 */unzip consul_1.6.2_linux_ ...
- python opencv 图片缺陷检测(讲解直方图以及相关系数对比法)
一.利用直方图的方式进行批量的图片缺陷检测(方法简单) 二.步骤(完整代码见最后) 2.1灰度转换(将原图和要检测对比的图分开灰度化) 灰度化的作用是因为后面的直方图比较需要以像素256为基准进行相关 ...
- KA 接口表
一.建表 1.年日均销量表 drop table app.app_basic_dashboard_goods_avg_year_sellnum; CREATE TABLE app.app_basic_ ...
- js数组的常用方法(10种)
数组的常用方法 数组在JavaScript中经常使用,如何对数组进行增删改非常重要,下面为数组的常用方法: 在数组末尾插入元素 arr.push(value),在数组的末尾添加一个或多个元素,并返回数 ...
- springboot集成通用mapper详细配置
通常,我们利用mybatis写持久层方法.要么按照传统定义mapper方法,定义xml文件的方式,全部手写.要么需要通过mybatis-generator逆向工程插件生成大量的xxxExample文件 ...
- Django-使用 include() 配置 URL
如果项目非常庞大,应用非常多,应用的 URL 都写在根 urls.py 配置文件中的话,会显的非常杂乱,还会出现名称冲突之类的问题,这样对开发整个项目是非常不利的. 可以这样解决,把每个应用的 URL ...
- C 对抗赛
时间限制 : - MS 空间限制 : - KB 评测说明 : 1s,256m 问题描述 某校有n只信竞队伍,队伍编号1到n,每只队伍都有一定数量的队员,队伍中每个人都有一个CF积分,积分越高,意 ...
- C 电压
时间限制 : 10000 MS 空间限制 : - KB 评测说明 : 1s,256m 问题描述 JOI社的某个实验室中有着复杂的电路.电路由n个节点和m根细长的电阻组成.节点被标号为1~N ...