高品质的音视频能力是怎样的? | Qcon 全球软件开发大会·上海站邀请函 了解详情
写点什么

论类型和事务

  • 2019-10-24
  • 本文字数:3493 字

    阅读完需:约 11 分钟

论类型和事务

在任何数据库系统中,事务都是比较难以处理的。我们不仅需要理解存储的是什么数据,还要知道数据是何时存储的。理想情况下,多层次的抽象可以帮助我们隔离复杂性。但是如果我们深入了解一下 Redis 的事务就会发现,Redis 的做法和其他系统是不一样的,这也导致许多人认为 Redis 根本就没有事务。例如,Redis 有一点比较特别的是对回滚的处理方法。下面我们来详细分析一下。


从更高层面上来看, Redis 有几个需要特别注意的地方:


  1. Redis 是单线程的(也有不少例外),这就意味着,Redis 同时只能做一件事;当然,对于 Redis,“做什么”最好是以毫秒或者纳秒来衡量的;

  2. Redis 的持久化级别是可调的,一些选项可提供很好的持久性,另外一些则完全没有持久化可言;后面我们会注意到,持久化级别对事务也是有影响的;

  3. Redis 的事务没有回滚,但如果一个 key 的值在事务开始之前改变了,那么事务会失败。这种对事务控制的方法和通常的做法是相反的,这么做可以让我们把数据拉回到客户端,从逻辑上进行求值,确保在事务开始之前数据不发生变化。


对大多数人来说,最大的一个陷阱是,事务中的单条命令会发生错误。这就会导致在某些情况下,事务中的每个命令都执行了,但是某个或多个命令发生了错误。认识到这一点有助于我们理解和控制这些异常情况。


首先,让我们看下 Redis 的语法错误和语义错误的区别。语法错误只是命令的语法有问题,不会发生数据访存动作。例如,发送一条不存在的命令或者命令参数违反了 key/value 的顺序。语法错误将导致事务永远不会开始。


举个例子:


127.0.0.1:6379> MULTIOK127.0.0.1:6379> STE foo bar(error) ERR unknown command `STE`, withargs beginning with: `foo`, `bar`,127.0.0.1:6379> EXEC(error) EXECABORT Transaction discardedbecause of previous errors.
复制代码


Redis 知道 STE 不是一个合法指令,所以抛了个异常,没有对底层的数据进行求值,拒绝执行整个 MULTI/EXEC 指令块。其他可以捕获的错误包括参数个数和参数模式匹配的问题。即使抛出了异常,事务也是非常安全的。使用 MULTI,所有的指令序列都排着队等待运行,一旦 EXEC 调用了,就开始执行。任何触发了 EXECABORT 的事务都不能执行。


语义错误是另外一类出错代价更高的错误。它和语法错误在行为上有所不同,属于 Redis 不能静态捕获的错误,通常需要 Redis 对底层的数据进行求值。


一个经典的例子是这个:


127.0.0.1:6379> SET foo "helloworld"OK127.0.0.1:6379> MULTIOK127.0.0.1:6379> INCR fooQUEUED127.0.0.1:6379> SET bar bazQUEUED127.0.0.1:6379> EXEC1) (error) ERR value is not an integer orout of range2) OK
复制代码


现实世界中一个合格的开发者绝不会故意对字符串“hello world”执行递增操作。但设想一下,我们有一个端到端的接受用户输入的应用程序,期望输入的是一个数字,但验证后却发现不是一个数字的情况。另外一个问题是,第 2 个 SET 指令被执行了,但是 INCR 没有执行。这不是大多数开发者在使用事务时期望得到的结果。好消息是,如果我们使用 WATCH 指令,这种类错误就可以得到控制。WATCH 可以用来观察 key 的变化,如果 key 发生了变化,他马上就会向客户端发送一个错误消息。这是一个非常强大的命令,使得我们可以把数据发送到客户端进行求值验证。在这种情况下,如果 foo 可以递增(亦即是一个整数),那么我们就可以对其求值。


我们看下这个伪代码:


> SET foo 1234> WATCH fooIf watchError goto 2 else continue> GET fooIf the result of line 4 is a not a number,throw an error, otherwise continue> MULTI> INCR foo> SET bar baz> EXEC
复制代码


现在,如果在第 3 行到第 9 行(但不在第 9 行执行过程中,这一行允许事务本身改变被监视的数据)命令执行的过程中,foo 被任何连上的客户端改变了,这个过程将会跳回到第 2 行。在第 4 行之后,将查看 foo 的内容,并决定是否可以进行递增操作。因为如果发生了变化,他就会开始重试,这就保证了不会因为 foo 的内容是错误的,从而导致整个事务发生错误。


这一点同 Redis 如何处理整数和浮点数也有关系。可以参考:


https://redislabs.com/blog/set-command-strange-beast/


基本上,如果数据是一个浮点数,就要使用 INCRBYFLOAT,而不是 INCR 或者 INCRBY。如果这个值正好等于一个整数,我们可以用 INCR 或者 INCRBY。很重要的一点是,使用 INCRBYFLOAT 命令,我们能用非浮点数对非浮点数进行递增操作,而不会产生什么影响。


事实上,INCR 命令族,绝对产生不了一个“1.0”的值。在执行 MULTI/EXEC 命令时,使用 INCRBYFLOAT 而不是 INCR 或者 INCRBY 是比较安全的,因为这不会产生错误。此时,使用 INCR 或者 INCRBY 的唯一理由是,如果我们确保这个数是一个浮点数,那么可以保证抛出一个错误。但不管怎样,我们还是必须使用 WATCH 命令。


以这种方式对数据求值,需要注意的是,这是有代价的。如果我们把数据拉到客户端,求值代价是极小的。例如,我们有一个值未知的 key,需要对其求值,这个值不是像组成一个数字的所需要的那么长的 5~7 个字节,而是 500M 字节的二进制块。把他传到客户端再求值那就要多等一会儿了。当然这是个比较极端的例子。


WATCH/MULTI/EXEC 模式,在用来防止命令/类型不匹配(WRONGTYPE)问题时很有用,这个错误会出现在事务的结果中。在 INCR 问题中,我们不得不 WATCH 一个字符串,使用在客户端对数据求值的方法,来决定是否可以在上面执行 INCR 或者 INCRBYFLOAT 命令。也可以直接使用 TYPE 命令对数据的类型进行求值,而不是对数据本身进行求值(这可以很好的避免对 500M 字节的数据直接进行求值的开销)。


让我们看下这个模式是怎么使用的:


> HSET foo bar 1234> WATCH fooIf watchError goto 2 else continue> TYPE fooIf the result of line 4 is a not “hash”, throw an error, otherwise continue> MULTI> HSET foo baz helloworld> HLEN foo> EXEC
复制代码


在这个例子里,我们知道 HSET 和 HLEN 命令是可以执行的,因为事先我们已经验证了他们的类型,而且在执行 EXEC 的时候,类型也没有发生变化。对于 HINCRBY/HINCRBYFLOAT 命令来说,需要结合之前的技巧,使用 HGET 而不是 GET 来检查是否可以在上面执行 INCR 命令。


有意思的是,BITFIELD 命令对数值越界的处理是有控制的,基于我们在命令中定义的类型,既可以封装也可以填充这些值。或者可以使用 FAIL 选项,奇怪的是,这并不会产生一个错误,而是忽略 INCR 命令,值保持和以前一样。然而,BITFIELD 还有另外一个陷阱,虽然这个命令很复杂,但 Redis 只做一些基本的语法检查,所以如果有语法错误,当这个命令加入到事务中时,命令是不会求值的。但当命令在事务中执行时,将产生一个语法错误,事务并不会取消,而是随着返回值返回一个错误。防范这类错误的唯一方法,就是确保这条命令的语法是正确的,确保命令在放入事务之前,在客户端层面来看语法是正确的。


总的来说,Redis 对数据是没有初始化这个步骤的。一些人会感到有点吃惊,但对大多数人来说,这是可接受的。如果一个 key 是空的,我们向其中加入数据,新创建的数据结构的类型是通过向其中加入数据的命令来定义的。但是这个情况随着模块 (module) 的出现慢慢开始变化了,使得这个惯例被打破了。比如,RedisGraph 要求,在查询 graph 之前,需要添加一些节点和关系。


我们看下这个例子:


> MULTIOK> GRAPH.QUERY mygraph "MATCH(r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Yamaha' RETURN r,t"QUEUED> LPUSH teamqueries YamahaQUEUED> EXEC1) (error) key doesn't contains a graphobject.2) (integer) 1
复制代码


事实上,在 Redis Streams 中也能看到这个行为,例如:


> MULTIOK> XGROUP CREATE my-streammy-consumer-group $QUEUED> LPUSH my-stream-groups my-consumer-groupQUEUED> EXEC1) (error) ERR The XGROUP subcommandrequires the key to exist. Note that for CREATE you may want to use theMKSTREAM option to create an empty stream automatically.2) (integer) 1
复制代码


值得庆幸的是,使用前面提到的 WATCH,TYPE, MULTI/EXEC 的模式,这些问题解决起来还是比较简单的。类似在上面这些例子,你只需检查类型是否匹配,例如是否是 graphdata 或 stream 类型。


使用 MULTI 和 EXEC 的 Redis 事务其实也没那么复杂。但我们确实需要注意一些陷阱来确保事务的行为是符合预期的。最后给大家在使用 Redis 事务上的一点建议,不要对类型、内容或者引用的 key 的存在性做任何假设,这样我们的 Redis 事务就是“刀枪不入”的了。


本文转载自公众号中间件小哥(ID:huawei_kevin)。


原文链接:


https://mp.weixin.qq.com/s/QShkLdQYcR3LqBSAPs1w3g


2019-10-24 11:18484

评论

发布
暂无评论
发现更多内容

Docker搭建项目环境实战

书旅

Docker Dockerfile Docker-compose

Postman生成接口文档

书旅

Postman 接口文档

七夕节来啦!AI一键生成情诗,去发给你的女朋友吧!

华为云开发者联盟

AI 智能高效 华为云 modelarts 七夕

LeetCode题解:26. 删除排序数组中的重复项,双指针,JavaScript,详细注释

Lee Chen

大前端 LeetCode

SQL查询语句执行顺序详解

书旅

MySQL SQL语法 sql查询

让理性思维走进我们的生活,帮助我们做出更好的决策。

叶小鍵

心理学 基思. 斯坦诺维奇 超越智商 认知科学

请不要随便修改基类

架构师修行之路

你可能需要一个脱机状态

非著名程序员

学习 程序员 个人成长 工作方式

完了,这个硬件成精了,它竟然绕过了 CPU

简爱W

图解javascript——基础篇(以思维导图总结js中关键技术点,为面试及工作助力)

执鸢者

Java 大前端

战斗还是逃避,或许可以考虑一下合作?

escray

学习 面试

java安全编码指南之:基础篇

程序那些事

Java 安全编码 安全编码指南

前端分页组件实现逻辑

书旅

php 大前端 分页

week 11学习总结

Geek_2e7dd7

【解Bug之路】——Nginx 502 Bad Gateway

简爱W

大数据技术思想入门(四):分布式文件的元数据是怎么存储的

cristal

Java 大数据 hadoop 分布式

不想做经理的程序员

escray

学习 面试

追逐影子的人,最终只会是影子

小隐乐乐

数据库是咋工作的?

简爱W

融云 X- Meetup 技术沙龙广州站:全球通信云技术实践分享

InfoQ_967a83c6d0d7

十一周作业

olderwei

极客大学架构师训练营

week 11

Geek_2e7dd7

Netty之旅二:口口相传的高性能Netty到底是什么?

一枝花算不算浪漫

你可能不知道的计算机基础

书旅

c 常量 计算机 基础

Docker 最常用的镜像命令和容器命令

哈喽沃德先生

Docker 容器 微服务

一个快捷方便的油煎鸡胸肉,懒人标配香喷喷好吃看得见

小霸王其乐无穷

美食 鸡胸肉 懒人

POI 之 策略游戏

zhoo299

随笔杂谈

「零代码」搭建简易招聘管理系统

Tony Wu

效率工具 SaaS 招聘管理 HRIS

Redis 之父关于 CRC64 的神秘往事!

yes

redis CRC

Flink水位线和时间戳理解-7

小知识点

scala 大数据 flink 模块化流程

动态修改logback的日志级别

thuni

Java springboot logback

论类型和事务_文化 & 方法_中间件小哥_InfoQ精选文章