redis setnx原子性_不支持原子性的 Redis 事务也叫事务吗?

79b9e9ae7a6106dc6fa400bc39b6c4cf.png
文章收录在 GitHub JavaKeeper ,N线互联网开发必备技能兵器谱
假设现在有这样一个业务,用户获取的某些数据来自第三方接口信息,为避免频繁请求第三方接口,我们往往会加一层缓存,缓存肯定要有时效性,假设我们要存储的结构是 hash(没有String的'SET anotherkey "will expire in a minute" EX 60'这种原子操作),我们既要批量去放入缓存,又要保证每个 key 都加上过期时间(以防 key 永不过期),这时候事务操作是个比较好的选择

为了确保连续多个操作的原子性,我们常用的数据库都会有事务的支持,Redis 也不例外。但它又和关系型数据库不太一样。

每个事务的操作都有 begin、commit 和 rollback,begin 指示事务的开始,commit 指示事务的提交,rollback 指示事务的回滚。它大致的形式如下

begin();
try {command1();command2();....commit();
} catch(Exception e) {rollback();
}

Redis 在形式上看起来也差不多,分为三个阶段

  1. 开启事务(multi)
  2. 命令入队(业务操作)
  3. 执行事务(exec)或取消事务(discard)
> multi
OK
> incr star
QUEUED
> incr star
QUEUED
> exec
(integer) 1
(integer) 2

上面的指令演示了一个完整的事务过程,所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。

Redis 事务可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞。

可以保证一个队列中,一次性、顺序性、排他性的执行一系列命令(Redis 事务的主要作用其实就是串联多个命令防止别的命令插队)

官方文档是这么说的

事务可以一次执行多个命令, 并且带有以下两个重要的保证:
  • 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行

这个原子操作,和关系型 DB 的原子性不太一样,它不能完全保证原子性,后边会介绍。

Redis 事务的几个命令

2e893aec718f3482a4b3f02ff7052b4c.png

MULTI 命令用于开启一个事务,它总是返回 OK 。

MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被执行。

另一方面, 通过调用 DISCARD , 客户端可以清空事务队列, 并放弃执行事务。

废话不多说,直接操作起来看结果更好理解~

一帆风顺

正常执行(可以批处理,挺爽,每条操作成功的话都会各取所需,互不影响)

faade02b4ea758dd8b9513f49e3a987c.png

放弃事务(discard 操作表示放弃事务,之前的操作都不算数)

6fc49b8c74f65ef37b4d0b8398ca96dc.png

思考个问题:假设我们有个有过期时间的 key,在事务操作中 key 失效了,那执行 exec 的时候会成功吗?

事务中的错误

上边规规矩矩的操作,看着还挺好,可是事务是为解决数据安全操作提出的,我们用 Redis 事务的时候,可能会遇上以下两种错误:

  • 事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
  • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。

Redis 针对如上两种错误采用了不同的处理策略,对于发生在 EXEC 执行之前的错误,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务(Redis 2.6.5 之前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败)

对于那些在 EXEC 命令执行之后所产生的错误, 并没有对它们进行特别处理: 即使事务中有某个/某些命令在执行时产生了错误, 事务中的其他命令仍然会继续执行。

全体连坐(某一条操作记录报错的话,exec 后所有操作都不会成功)

2d46a27d479dd43d8f9382026d306571.png

冤头债主(示例中 k1 被设置为 String 类型,decr k1 可以放入操作队列中,因为只有在执行的时候才可以判断出语句错误,其他正确的会被正常执行)

c81a380a673e4d65be5b6fd735b46ac0.png

为什么 Redis 不支持回滚

如果你有使用关系式数据库的经验,那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种做法可能会让你觉得有点奇怪。

以下是官方的自夸

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。
鉴于没有任何机制能避免程序员自己造成的错误, 并且这类错误通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。

87ec590d81832e0fad2b417bd0ab96a2.gif

带 Watch 的事务

WATCH 命令用于在事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务将被打断,不再执行, 直接返回失败。

WATCH命令可以被调用多次。 对键的监视从 WATCH 执行之后开始生效, 直到调用 EXEC 为止。

用户还可以在单个 WATCH 命令中监视任意多个键, 就像这样:

redis> WATCH key1 key2 key3 
OK 

EXEC 被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。

我们看个简单的例子,用 watch 监控我的账号余额(一周100零花钱的我),正常消费

e39504fea2ad9667eb3ee92c1333270b.png

但这个卡,还绑定了我媳妇的支付宝,如果在我消费的时候,她也消费了,会怎么样呢?

犯困的我去楼下 711 买了包烟,买了瓶水,这时候我媳妇在超市直接刷了 100,此时余额不足的我还在挑口香糖来着,,,

65e197df933c2b0886aa6006cbff7118.png

这时候我去结账,发现刷卡失败(事务中断),尴尬的一批

e4f60be7ecf552e48e9f5fa23943c724.png

你可能没看明白 watch 有啥用,我们再来看下,如果还是同样的场景,我们没有 watch balance ,事务不会失败,储蓄卡成负数,是不不太符合业务呢

adcead0bc673e9edbc5a93ae15d03675.png

使用无参数的 UNWATCH 命令可以手动取消对所有键的监视。 对于一些需要改动多个键的事务,有时候程序需要同时对多个键进行加锁, 然后检查这些键的当前值是否符合程序的要求。 当值达不到要求时, 就可以使用 UNWATCH 命令来取消目前对键的监视, 中途放弃这个事务, 并等待事务的下次尝试。

watch指令,类似乐观锁,事务提交时,如果 key 的值已被别的客户端改变,比如某个 list 已被别的客户端push/pop 过了,整个事务队列都不会被执行。(当然也可以用 Redis 实现分布式锁来保证安全性,属于悲观锁)

通过 watch 命令在事务执行之前监控了多个 keys,倘若在 watch 之后有任何 key 的值发生变化,exec 命令执行的事务都将被放弃,同时返回 Null 应答以通知调用者事务执行失败。

悲观锁
悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁乐观锁
乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。乐观锁策略:提交版本必须大于记录当前版本才能执行更新

WATCH 命令的实现原理[1]

在代表数据库的 server.h/redisDb 结构类型中, 都保存了一个 watched_keys 字典, 字典的键是这个数据库被监视的键, 而字典的值是一个链表, 链表中保存了所有监视这个键的客户端,如下图。

e3acfbf7ec3de7ae726fdcfc1702d0b8.png
typedef struct redisDb {dict *dict;                 /* The keyspace for this DB */dict *expires;              /* Timeout of keys with a timeout set */dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/dict *ready_keys;           /* Blocked keys that received a PUSH */dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */int id;                     /* Database ID */long long avg_ttl;          /* Average TTL, just for stats */unsigned long expires_cursor; /* Cursor of the active expire cycle. */list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;list *watched_keys;     /* Keys WATCHED for MULTI/EXEC CAS */

WATCH 命令的作用, 就是将当前客户端和要监视的键在 watched_keys 中进行关联。

举个例子, 如果当前客户端为 client99 , 那么当客户端执行 WATCH key2 key3 时, 前面展示的 watched_keys 将被修改成这个样子:

87641e9cbb1627311743e4a61ab2ce3a.png

通过 watched_keys 字典, 如果程序想检查某个键是否被监视, 那么它只要检查字典中是否存在这个键即可; 如果程序要获取监视某个键的所有客户端, 那么只要取出键的值(一个链表), 然后对链表进行遍历即可。

在任何对数据库键空间(key space)进行修改的命令成功执行之后 (比如 FLUSHDB、SET 、DEL、LPUSH、 SADD,诸如此类), multi.c/touchWatchedKey 函数都会被调用 —— 它会去 watched_keys 字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开:

9614838b88eee936efb659b37cde12e2.png
void multiCommand(client *c) {// 不能在事务中嵌套事务if (c->flags & CLIENT_MULTI) {addReplyError(c,"MULTI calls can not be nested");return;}// 打开事务 FLAGc->flags |= CLIENT_MULTI;addReply(c,shared.ok);
}/* "Touch" a key, so that if this key is being WATCHed by some client the* next EXEC will fail. */
void touchWatchedKey(redisDb *db, robj *key) {list *clients;listIter li;listNode *ln;// 字典为空,没有任何键被监视if (dictSize(db->watched_keys) == 0) return;// 获取所有监视这个键的客户端clients = dictFetchValue(db->watched_keys, key);if (!clients) return;// 遍历所有客户端,打开他们的 CLIENT_DIRTY_CAS 标识listRewind(clients,&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);c->flags |= CLIENT_DIRTY_CAS;}
}

当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查:

  • 如果客户端的 CLIENT_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。
  • 如果 CLIENT_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。

小总结:

3 个阶段

  • 开启:以 MULTI 开始一个事务
  • 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
  • 执行:由 EXEC 命令触发事务

3 个特性

  • 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际的被执行,因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这个让人万分头痛的问题
  • 不保证原子性:Redis 同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的安全性。Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。

最后

Redis 事务在发送每个指令到事务缓存队列时都要经过一次网络读写,当一个事务内部的指令较多时,需要的网络 IO 时间也会线性增长。所以通常 Redis 的客户端在执行事务时都会结合 pipeline 一起使用,这样可以将多次 IO 操作压缩为单次 IO 操作。

参考资料

[1]

Redis设计与实现: https://redisbook.readthedocs.io/en/latest/feature/transaction.html#id3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/504323.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【LeetCode笔记】剑指 Offer 67. 把字符串转换成整数 (Java、字符串)

文章目录题目描述思路 && 代码二刷题目描述 老阅读题了,感觉这道题有点类似写业务接口。 思路 && 代码 首先,把情况划分一下: 非正常值,直接给 0(这个实际上可以划分到2的代码中)可取…

详细描述三个适于瀑布模型的项目_信息系统项目管理师-第二三章:信息系统项目管理基础与立项管理2...

三、信息系统项目的生命周期1、项目生命周期的定义—项目从启动、组织与准备、执行到结束所经历的一系列阶段2、★项目生命周期的特征①成本与人力投入在开始时较低,在工作执行期间达到最高,并在项目快要结束时迅速回落②风险与不确定性在项目开始时最大,并在项目的整个生命周期…

【LeetCode笔记】剑指 Offer 64. 求1 + 2 + ... + n (Java、限制语法、二刷)

文章目录题目描述思路 && 代码注释版代码(方便理解):无注释版代码(方便背诵 )二刷题目描述 鬼鬼,又到了我最喜欢的戴着脚镣跳舞环节。 讲道理,我觉得这类题应该整个归纳啥的。。&#x…

虚拟dom_从0到1实现一个虚拟DOM

来源 | https://segmentfault.com/a/1190000021331850要构建自己的虚拟 DOM,需要知道两件事。你甚至不需要深入 React 的源代码或者深入任何其他虚拟 DOM 实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟 DOM 的主要部分只需不到 50…

【LeetCode笔记】剑指 Offer 31. 栈的压入、弹出序列 (Java、栈)

文章目录题目描述代码 && 思路二刷题目描述 打卡第三天!加油加油~ 数据结构笔试题貌似也会考 ,不过考法不一样,有点规律。总的来说还是挺有意思的,我们直接来看代码吧~ 代码 && 思路 O(…

【LeetCode笔记】剑指 Offer 20. 表示数值的字符串(Java、字符串)

文章目录题目描述思路 && 代码题目描述 呃…比较恶心的一道题,需要读好题,分完情况再下手有看到dalao用有限状态机来做,不过这边还是直接用了我自己的做法 思路 && 代码 ps:这里没有参考题解的写法&#xff…

【LeetCode笔记】剑指 Offer 16. 数值的整数次方(Java、分治)

文章目录题目描述思路 && 代码二刷题目描述 很棒的题目!便于分治思想的理解! 思路 && 代码 分情况是大头~递归结束的情况: x0x^0x0 1x1x^1x1 xx−1^-1−1 1 / x 偶数情况:直接二分递归奇数情况…

网站域名解析端口_环境测试必备,无需注册域名,免费动态IP解析一键解析本地网站...

localtunnel是基于Nodejs 开发的一个工具,开发者同时提供了Go语言版本。这个工具可以提供给你一个随机生成的二级域名,动态域名解析到你执行这个命令的服务器上,可以将域名解析到指定端口上。而且只需要一行代码。localtunnel安装在安装这个工…

【LeetCode笔记】剑指 Offer 33. 二叉树的后序遍历序列(Java、递归、栈)

文章目录题目描述思路 && 代码1. 递归做法2. 辅助栈做法二刷打卡第四天~昨天没来得及写博客 题目描述 无须多言,直接冲思路吧! 思路 && 代码 1. 递归做法 缺点:最差情况下,可能会退化成链表&…

LeetCode笔记】剑指 Offer 35. 复杂链表的复制(Java、哈希表、原地算法)

文章目录题目描述思路 && 代码1. 哈希表法2. 原地算法二刷题目描述 主要有两个考虑点: 不能改变原链表新链表赋予 next、random 时,复制结点不一定存在 思路 && 代码 1. 哈希表法 O(n)、O(n)参考了dalao的写法,这里哈希表…

adam算法效果差原因_干货|快来get中央空调冬天制热效果差的十大原因!

作为调节室内温度的重要电器,中央空调在人们的生活中扮演着至关重要的角色。目前,市场上许多中央空调都是冷暖两用型,每到冬季,绝大部分家用中央空调都被纷纷派上用场;然而,有些用户在使用空调制热时&#…

linux查看日志命令_查看log日志基础命令

一.tail命令tail -f test.log 可以动态的查看服务器运行状态的日志head -n 5 test.log 显示top 5行tail -n 5 test.log 显示last 5行tail -n 5 test.log 从第5行开始显示,显示第5行以后的二.cat 命令cat 文件名 | head -n 数量,查看log.log前200行cat log.log | hea…

【LeetCode笔记】剑指 Offer 36. 二叉搜索树与双向链表(Java、二叉树、链表、原地算法)

文章目录题目描述思路 && 代码1. 非原地算法2. 原地算法二刷题目描述 谈到二叉搜索树,那就得考虑考虑中序遍历啦~这道题对中序遍历的理解提升很有好处! 思路 && 代码 1. 非原地算法 最简单的做法,依赖Array…

sqlserver2000给账户授予所有的权限_内网渗透 | 域内权限解读

域内权限解读目录域本地组全局组通用组A-G-DL-P策略内置组几个比较重要的域本地组几个比较重要的全局组、通用组的权限域本地组多域用户访问单域资源(访问同一个域)可以从任何域添加用户账户、通用组和全局组,但只能在其所在域内指派权限。域本地组不能嵌套于其他组…

【LeetCode笔记】剑指 Offer 38. 字符串的排列(Java、字符串、DFS)

文章目录题目描述思路 && 代码二刷打卡第五天&#xff5e; 题目描述 和这道题很相似&#xff0c;只是数据结构不同 思路 && 代码 DFS 辅助访问数组 Set 减枝通过 Set 实现去重 class Solution {Set<String> res new HashSet<>();char[] ar…

lombok依赖_使用Lombok 前你需要知道这些

转自&#xff1a;树下魅狐链接&#xff1a;https://ramostear.com/blog/2020/04/28/uk1860p8.html不得不承认&#xff0c;Lombok 是一个很不错的 Java 库&#xff0c;它可以让你在少写代码的同时耍耍酷&#xff0c;简单的几个注解&#xff0c;就可以干掉一大片模板代码。但是&a…

【LeetCode笔记】剑指 Offer 45. 把数组排成最小的数(Java、字符串、Lambda)

文章目录题目描述思路 && 代码二刷题目描述 很 nice 的一道题&#xff01;花的时间也不会很多&#xff0c;还能复习复习 Lambda&#xff5e; 思路 && 代码 核心思路&#xff1a;如果 x y > y x&#xff0c;说明 x 应该在 y 前面时间复杂度&#xff1a…

【LeetCode笔记】剑指 Offer 46. 把数字翻译成字符串(Java、字符串、动态规划、DFS)

文章目录题目描述思路 && 代码1. 动态规划做法2. DFS 做法二刷打卡第六天~继续加油&#xff01; 题目描述 万物皆可爬楼梯.…和爬楼梯原理很像&#xff0c;都是使用动态规划的做法来做~ 思路 && 代码 1. 动态规划做法 初始化&#xff1a;dp[0] 1, dp[1] …

【LeetCode笔记】剑指 Offer 47. 礼物的最大价值(Java、动态规划)

文章目录题目描述思路 && 代码1. 常规动规 O(n2n^2n2) 、O(n2n^2n2)2. 滚动数组法 O(n2n^2n2) 、O(nnn)原地操作O(n2n^2n2) 、O(111)题目描述 一眼动态规划啦~ 思路 && 代码 1. 常规动规 O(n2n^2n2) 、O(n2n^2n2) dp[i][j]: 对应位置 grid[i - 1][j - 1] …

【LeetCode笔记】剑指 Offer 93. 复原 IP 地址(Java、DFS、字符串)

文章目录题目描述二刷打卡第七天&#xff5e; 也是很常考的一道题了&#xff01;感觉和把数字翻译成字符串这道题很像&#xff0c;也都可以用 DFS 来做 题目描述 还是定义全局的 list&#xff0c;在 DFS 的过程不断维护 list递归结束的情况&#xff1a;已经凑够四个数字&am…