Redis支持事务机制,但Redis的事务机制与传统关系型数据库的事务机制并不相同。

Redis事务的本质是一组命令的集合(命令队列)。事务可以一次执行多个命令,并提供以下保证:

(1)事务中的所有命令都按顺序执行。事务命令执行过程中,其他客户端提交的命令请求需要等待当前事务所有命令执行完成后再处理,不会插入当前事务命令队列中。

(2)事务中的命令要么都执行,要么都不执行,即使事务中有些命令执行失败,后续命令依然被执行。因此Redis事务也是原子的。

注意Redis不支持回滚,如果事务中有命令执行失败了,那么Redis会继续执行后续命令而不是回滚。

可能有读者疑惑Redis是否支持ACID?笔者认为,ACID概念起源于传统的关系型数据库,而Redis是非关系型数据库,而且Redis并没有声明是否支持ACID,所以本文不讨论该问题。

事务的应用示例

Redis提供了MULTI、EXEC、DISCARD和WATCH命令来实现事务功能:

> MULTI
OK
> SET points 1
QUEUED
> INCR points
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1
  • MULTI命令可以开启一个事务,后续的命令都会被放入事务命令队列。
  • EXEC命令可以执行事务命令队列中的所有命令,DISCARD命令可以抛弃事务命令队列中的命令,这两个命令都会结束当前事务。
  • WATCH命令可以监视指定键,当后续事务执行前发现这些键已修改时,则拒绝执行事务。

表17-1展示了一个WATCH命令的简单使用示例。



可以看到,在执行EXEC命令前如果WATCH的键被修改,则EXEC命令不会执行事务,因此WATCH常用于实现乐观锁。

事务的实现原理

server.h/multiState结构体负责存放事务信息:

typedef struct multiState {
multiCmd *commands;
...
} multiState;
  • commands:事务命令队列,存放当前事务所有的命令。

    客户端属性client.mstate指向一个multiState变量,该multiState作为客户端的事务上下文,负责存放该客户端当前的事务信息。

    下面看一下MULTI、EXEC和WATCH命令的实现。

WATCH命令的实现

提示:本章代码如无特殊说明,均在multi.c中。

WATCH命令的实现逻辑较独立,我们先分析该命令的实现逻辑。

redisDb中定义了字典属性watched_keys,该字典的键是数据库中被监视的Redis键,字典的值是监视字典键的所有客户端列表,如图17-1所示。

client中也定义了列表属性watched_keys,记录该客户端所有监视的键。

watchCommand函数负责处理WATCH命令,该函数会调用watchForKey函数处理相关逻辑:

void watchForKey(client *c, robj *key) {
...
// [1]
clients = dictFetchValue(c->db->watched_keys,key);
...
listAddNodeTail(clients,c); // [2]
wk = zmalloc(sizeof(*wk));
wk->key = key;
wk->db = c->db;
incrRefCount(key);
listAddNodeTail(c->watched_keys,wk);
}

【1】将客户端添加到redisDb.watched_keys字典中该Redis键对应的客户端列表中。

【2】初始化watchedKey结构体(wk变量),该结构体可以存储被监视键和对应的数据库。 将wk变量添加到client.watched_keys中。

Redis中每次修改数据时,都会调用signalModifiedKey函数,将该数据标志为已修改。

signalModifiedKey函数会调用touchWatchedKey函数,通知监视该键的客户端数据已修改:

void touchWatchedKey(redisDb *db, robj *key) {
...
clients = dictFetchValue(db->watched_keys, key);
if (!clients) return; listRewind(clients,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln); c->flags |= CLIENT_DIRTY_CAS;
}
}

从redisDb.wzatched_keys中获取所有监视该键的客户端,给这些客户端添加CLIENT_ DIRTY_CAS标志,该标志代表客户端监视的键已被修改。

MULTI、EXEC命令的实现

MULTI命令由multiCommand函数处理,该函数的处理非常简单,就是打开客户端CLIENT_MULTI标志,代表该客户端已开启事务。

前面说过,processCommand函数执行命令时,会检查客户端是否已开启事务。如果客户端已开启事务,则调用queueMultiCommand函数,将命令请求添加到客户端事务命令队列client.mstate.commands中:

int processCommand(client *c) {
...
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
{
queueMultiCommand(c);
addReply(c,shared.queued);
} ...
return C_OK;
}

可以看到,如果当前客户端开启了事务,则除了MULTI、EXEC、DISCARD和WATCH命令,其他命令都会放入到事务命令队列中。

EXEC命令由execCommand函数处理:

void execCommand(client *c) {
... // [1]
if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr : shared.nullarray[c->resp]);
discardTransaction(c);
goto handle_monitor;
} // [2]
unwatchAllKeys(c);
...
addReplyArrayLen(c,c->mstate.count);
for (j = 0; j < c->mstate.count; j++) {
c->argc = c->mstate.commands[j].argc;
c->argv = c->mstate.commands[j].argv;
c->cmd = c->mstate.commands[j].cmd; // [3]
if (!must_propagate &&
!server.loading &&
!(c->cmd->flags & (CMD_READONLY|CMD_ADMIN)))
{
execCommandPropagateMulti(c);
must_propagate = 1;
}
// [4]
int acl_keypos;
int acl_retval = ACLCheckCommandPerm(c,&acl_keypos);
if (acl_retval != ACL_OK) {
...
} else {
call(c,server.loading ? CMD_CALL_NONE : CMD_CALL_FULL);
}
...
}
// [5]
...
discardTransaction(c); // [6]
if (must_propagate) {
int is_master = server.masterhost == NULL;
server.dirty++;
...
}
...
}

【1】当客户端监视的键被修改(客户端存在CLIENT_DIRTY_CAS标志)或者客户端已拒绝事务中的命令(客户端存在CLIENT_DIRTY_EXEC标志)时,直接抛弃事务命令队列中的命令,并进行错误处理。

当服务器处于异常状态(如内存溢出)时,Redis将拒绝命令,并给开启了事务的客户端添加CLIENT_DIRTY_EXEC标志。

【2】取消当前客户端对所有键的监视,所以WATCH命令只能作用于后续的一个事务。

【3】在执行事务的第一个写命令之前,传播MULTI命令到AOF文件和从节点。MULTI命令执行完后并不会被传播(MULTI命令并不属于写命令),如果事务中执行了写命令,则在这里传播MULTI命令。

【4】检查用户的ACL权限,检查通过后执行命令。

【5】执行完所有命令,调用discardTransaction函数重置客户端事务上下文client.mstate,并删除CLIENT_MULTI、CLIENT_DIRTY_CAS、CLIENT_DIRTY_EXEC标志,代表当前事务已经处理完成。

【6】如果事务中执行了写命令,则修改server.dirty,这样会使server.c/call函数将EXEC命令传播到AOF文件和从节点,从而保证一个事务的MULTI、EXEC命令都被传播。

关于Redis不支持回滚机制,Redis在官网中给出了如下解释:

(1)仅当使用了错误语法(并且该错误无法在命令加入队列期间检测)或者Redis命令操作数据类型错误(比如对集合类型使用了HGET命令)时,才可能导致事务中的命令执行失败,这意味着事务中失败的命令是编程错误的结果,所以这些问题应该在开发过程中发现并处理,而不是依赖于在生产环境中的回滚机制来规避。

(2)不支持回滚,Redis事务机制实现更简单并且性能更高。

Redis的事务非常简单,即在一个原子操作内执行多条命令。Redis的Lua脚本也是事务性的,所以用户也可以使用Lua脚本实现事务。Redis Lua脚本会在后续章节详细分析。

总结:

  • Redis事务保证多条命令在一个原子操作内执行。
  • Redis提供了MULTI、EXEC、DISCARD和WATCH命令来实现事务功能。
  • 使用WATCH命令可以实现乐观锁机制。

本文内容摘自作者新书《Redis核心原理与实践》。本书通过深入分析Redis 6.0源码,总结了Redis核心功能的设计与实现。通过阅读本书,读者可以深入理解Redis内部机制及最新特性,并学习到Redis相关的数据结构与算法、Unix编程、存储系统设计,分布式系统架构等一系列知识。

经过该书编辑同意,我会继续在个人技术公众号(binecy)发布书中部分章节内容,作为书的预览内容,欢迎大家查阅,谢谢。

语雀平台预览:《Redis核心原理与实践》

京东链接

Redis核心原理与实践--事务实践与源码分析的更多相关文章

  1. vue双向绑定的原理及实现双向绑定MVVM源码分析

    vue双向绑定的原理及实现双向绑定MVVM源码分析 双向数据绑定的原理是:可以将对象的属性绑定到UI,具体的说,我们有一个对象,该对象有一个name属性,当我们给这个对象name属性赋新值的时候,新值 ...

  2. Hadoop之HDFS原理及文件上传下载源码分析(下)

    上篇Hadoop之HDFS原理及文件上传下载源码分析(上)楼主主要介绍了hdfs原理及FileSystem的初始化源码解析, Client如何与NameNode建立RPC通信.本篇将继续介绍hdfs文 ...

  3. Hadoop之HDFS原理及文件上传下载源码分析(上)

    HDFS原理 首先说明下,hadoop的各种搭建方式不再介绍,相信各位玩hadoop的同学随便都能搭出来. 楼主的环境: 操作系统:Ubuntu 15.10 hadoop版本:2.7.3 HA:否(随 ...

  4. spring事务概念与获取事务时事务传播行为源码分析

    一.事务状态:org.springframework.transaction.TransactionStatus isNewTransaction 是否是新事务 hasSavepoint 是否有保存点 ...

  5. Spring笔记(5) - 声明式事务@EnableTransactionManagement注解源码分析

    一.背景 前面详解了实现Spring事务的两种方式的不同实现:编程式事务和声明式事务,对于配置都使用到了xml配置,今天介绍Spring事务的注解开发,例如下面例子: 配置类:注册数据源.JDBC模板 ...

  6. Mybatis整合Spring实现事务管理的源码分析

    一:前言 没有完整看完,但是看到了一些关键的地方,这里做个记录,过程会有点乱,以后逐渐补充最终归档为完整流程:相信看过框架源码的都知道过程中无法完全确定是怎样的流程,毕竟不可能全部都去测试一遍 ,但是 ...

  7. spring事务传播实现源码分析

    转载. https://blog.csdn.net/qpfjalzm123/article/details/83717367 本文只是对spring事务传播实现的流程进行简单的分析,如有不对之处请指出 ...

  8. 分布式事务中间件 TCC-Transaction 源码分析 —— 项目实战

    https://blog.csdn.net/lldouble/article/details/79455172

  9. redis源码分析之事务Transaction(上)

    这周学习了一下redis事务功能的实现原理,本来是想用一篇文章进行总结的,写完以后发现这块内容比较多,而且多个命令之间又互相依赖,放在一篇文章里一方面篇幅会比较大,另一方面文章组织结构会比较乱,不容易 ...

  10. 20、Task原理剖析与源码分析

    一.Task原理 1.图解 二.源码分析 1. ###org.apache.spark.executor/Executor.scala /** * 从TaskRunner开始,来看Task的运行的工作 ...

随机推荐

  1. 鸿蒙内核源码分析(fork篇) | 一次调用,两次返回 | 百篇博客分析OpenHarmony源码 | v45.03

    百篇博客系列篇.本篇为: v45.xx 鸿蒙内核源码分析(Fork篇) | 一次调用,两次返回 | 51.c.h .o 进程管理相关篇为: v02.xx 鸿蒙内核源码分析(进程管理篇) | 谁在管理内 ...

  2. 鸿蒙内核源码分析(自旋锁篇) | 当立贞节牌坊的好同志 | 百篇博客分析OpenHarmony源码 | v26.02

    百篇博客系列篇.本篇为: v26.xx 鸿蒙内核源码分析(自旋锁篇) | 当立贞节牌坊的好同志 | 51.c.h .o 进程通讯相关篇为: v26.xx 鸿蒙内核源码分析(自旋锁篇) | 当立贞节牌坊 ...

  3. WPF实现截图(仿微信截图)

    WPF开发者QQ群: 340500857  | 微信群 -> 进入公众号主页 加入组织 每日一笑 肚子疼,去厕所排便,结果什么都没拉出来.看着自己坐在马桶上痛苦又努力却一无所获的样子,仿佛看到了 ...

  4. Anaconda和canda简介及区别

    Anaconda简介: 1.是一个开源的Python发行版本,其包含了conda.Python等软件包,numpy,pandas(数据分析),scipy等科学计算包,而无需再单独下载配置. 可以在同一 ...

  5. MySQL的详细讲解

    目录 Mysql的架构与历史 MySQL的逻辑架构 更新中---- Mysql的架构与历史 MySQL的逻辑架构 第二层的架构是所有的跨引擎的功能实现的地方,例如:存储,触发器,视图等. 第三层半酣了 ...

  6. 项目问题记录------Mabatis动态sql语句

    现在在做一个模糊查询功能,使用两个查询条件: 条件1:下拉框选择的产品名 条件2:输入框输入的用户名 需求1:下拉框的选项是从数据库里导出来的产品名,此外,添加一个选项"全部产品" ...

  7. Python 面向对象笔记

    Python 面向对象课程笔记 前言 Python 面向对象 正文 基本概念 什么是对象: 万物皆对象 对象是具体物体: 拥有属性 拥有行为 封装零散为整体 OOP(Object Oriented P ...

  8. C 字符串相关的库函数

    字符串操作函数 size_t strlen( char *string ); 返回字符串长度 char* strcpy( char *dst, char const *src ); 将src复制到ds ...

  9. CORS+XSS的漏洞利用payload

    之前有人问我有没有CORS+XSS的利用姿势,翻了一下国内貌似都没有利用姿势于是就写了这篇文章!!! 首先找到一个反射xss,然后使用xss加载javascript代码达到跨域劫持目的payload如 ...

  10. linux系统(centos)下su和sudo命令的区别

    linux系统(centos)下su和sudo命令的区别 区别 我们在日常使用过程中,这2个命令很多时候能达到相同的效果,对细节区别十分模糊,这里进行简单的解释和区分.希望大家能够正确使用这2个命令, ...