《Redis入门指南》一4.1 事务

4.1 事务

Redis入门指南
傍晚时候,忙完了一天的教学工作,宋老师坐在办公室的电脑前开始为明天的课程做准备。尽管有着近5年的教学经验,可是宋老师依然习惯在备课时写一份简单的教案。正在网上查找资料时,在浏览器的历史记录里他突然看到了小白的博客。心想:不知道他的博客怎么样了?

于是宋老师点进了小白的博客,页面刚载入完他就被博客最下面的一行大得夸张的文字吸引了:“Powered by Redis”。宋老师笑了笑,接着就看到了小白博客中最新的一篇文章:

标题:

使用Redis来存储微博中的用户关系

正文:

在微博中,用户之间是“关注”和“被关注”的关系。如果要使用Redis存储这样的关系可以使用集合类型。思路是对每个用户使用两个集合类型键,分别名为 user:用户 ID:followers和user:用户ID:following,用来存储关注该用户的用户集合和该用户关注的用户集合。

然后使用一个函数来实现关注操作,伪代码如下:

def follow($currentUser, $targetUser)
  SADD user:$currentUser:following, $targetUser
  SADD user:$targetUser:followers, $currentUser

如ID为1的用户A想关注ID为2的用户B,只需要执行follow(1, 2)即可。

然而在实现该功能的时候我发现了一个问题:完成关注操作需要依次执行两条Redis命令,如果在第一条命令执行完后因为某种原因导致第二条命令没有执行,就会出现一个奇怪的现象:A查看自己关注的用户列表时会发现其中有B,而B查看关注自己的用户列表时却没有A,换句话说就是,A虽然关注了B,却不是B的“粉丝”。真糟糕,A和B都会对这个网站失望的!但愿不会出现这种情况。

宋老师看到此处,笑得合不拢嘴,把备课的事抛到了脑后。心想:“看来有必要给小白传授一些进阶的知识。”他给小白写了封电子邮件:

其实可以使用Redis的事务来解决这一问题。

4.1.1 概述

Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。事务的应用非常普遍,如银行转账过程中A给B汇款,首先系统从A的账户中将钱划走,然后向B的账户增加相应的金额。这两个步骤必须属于同一个事务,要么全执行,要么全不执行。否则只执行第一步,钱就凭空消失了,这显然让人无法接受。

事务的原理是先将属于一个事务的命令发送给Redis,然后再让Redis依次执行这些命令。例如:

redis> MULTI
OK
redis> SADD "user:1:following" 2
QUEUED
redis> SADD "user:2:followers" 1
QUEUED
redis> EXEC
1) (integer) 1
2) (integer) 1

上面的代码演示了事务的使用方式。首先使用MULTI命令告诉Redis:“下面我发给你的命令属于同一个事务,你先不要执行,而是把它们暂时存起来。”Redis 回答:“OK。”

而后我们发送了两个SADD命令来实现关注和被关注操作,可以看到Redis遵守了承诺,没有执行这些命令,而是返回QUEUED表示这两条命令已经进入等待执行的事务队列中了。

当把所有要在同一个事务中执行的命令都发给 Redis 后,我们使用 EXEC命令告诉Redis将等待执行的事务队列中的所有命令(即刚才所有返回QUEUED的命令)按照发送顺序依次执行。EXEC命令的返回值就是这些命令的返回值组成的列表,返回值顺序和命令的顺序相同。

Redis保证一个事务中的所有命令要么都执行,要么都不执行。如果在发送EXEC命令前客户端断线了,则Redis会清空事务队列,事务中的所有命令都不会执行。而一旦客户端发送了EXEC命令,所有的命令就都会被执行,即使此后客户端断线也没关系,因为Redis中已经记录了所有要执行的命令。

除此之外,Redis 的事务还能保证一个事务内的命令依次执行而不被其他命令插入。试想客户端A需要执行几条命令,同时客户端B发送了一条命令,如果不使用事务,则客户端B的命令可能会插入到客户端A的几条命令中执行。如果不希望发生这种情况,也可以使用事务。

4.1.2 错误处理

有些读者会有疑问,如果一个事务中的某个命令执行出错,Redis 会怎样处理呢?要回答这个问题,首先需要知道什么原因会导致命令执行出错。

(1)语法错误。语法错误指命令不存在或者命令参数的个数不对。比如:

redis> MULTI
OK
redis> SET key value
QUEUED
redis> SET key
(error) ERR wrong number of arguments for 'set' command
redis> ERRORCOMMAND key
(error) ERR unknown command 'ERRORCOMMAND'
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

跟在MULTI命令后执行了3个命令:一个是正确的命令,成功地加入事务队列;其余两个命令都有语法错误。而只要有一个命令有语法错误,执行EXEC命令后Redis就会直接返回错误,连语法正确的命令也不会执行1。

(2)运行错误。运行错误指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键,这种错误在实际执行之前Redis是无法发现的,所以在事务里这样的命令是会被Redis接受并执行的。如果事务里的一条命令出现了运行错误,事务里其他的命令依然会继续执行(包括出错命令之后的命令),示例如下:

redis> MULTI
OK
redis> SET key 1
QUEUED
redis> SADD key 2
QUEUED
redis> SET key 3
QUEUED
redis> EXEC
1) OK
2) (error) ERR Operation against a key holding the wrong kind of value
3) OK
redis> GET key
"3"

可见虽然SADD key 2出现了错误,但是SET key 3依然执行了。

Redis的事务没有关系数据库事务提供的回滚(rollback)2功能。为此开发者必须在事务执行出错后自己收拾剩下的摊子(将数据库复原回事务执行前的状态等)。

不过由于Redis不支持回滚功能,也使得Redis在事务上可以保持简洁和快速。另外回顾刚才提到的会导致事务执行失败的两种错误,其中语法错误完全可以在开发时找出并解决,另外如果能够很好地规划数据库(保证键名规范等)的使用,是不会出现如命令与数据类型不匹配这样的运行错误的。

4.1.3 WATCH命令介绍

我们已经知道在一个事务中只有当所有命令都依次执行完后才能得到每个结果的返回值,可是有些情况下需要先获得一条命令的返回值,然后再根据这个值执行下一条命令。例如,介绍INCR命令时曾经说过使用GET和SET命令自己实现incr函数会出现竞态条件,伪代码如下:

def incr($key)
  $value = GET $key
  if not $value
      $value = 0
  $value = $value + 1
  SET $key, $value
  return $value

肯定会有很多读者想到可以用事务来实现incr函数以防止竞态条件,可是因为事务中的每个命令的执行结果都是最后一起返回的,所以无法将前一条命令的结果作为下一条命令的参数,即在执行SET命令时无法获得GET命令的返回值,也就无法做到增1的功能了。

为了解决这个问题,我们需要换一种思路。即在 GET 获得键值后保证该键值不被其他客户端修改,直到函数执行完成后才允许其他客户端修改该键键值,这样也可以防止竞态条件。要实现这一思路需要请出事务家族的另一位成员:WATCH。WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值),如:

redis> SET key 1
OK
redis> WATCH key
OK
redis> SET key 2
OK
redis> MULTI
OK
redis> SET key 3
QUEUED
redis> EXEC
(nil)
redis> GET key
"2"

上例中在执行WATCH命令后、事务执行前修改了key的值(即SET key 2),所以最后事务中的命令SET key 3没有执行,EXEC命令返回空结果。

学会了WATCH命令就可以通过事务自己实现incr函数了,伪代码如下:

def incr($key)
  WATCH $key
  $value = GET $key
   if not $value
      $value = 0
   $value = $value + 1
  MULTI
  SET $key, $value
   result = EXEC
  return result[0]

因为EXEC命令返回值是多行字符串类型,所以代码中使用result[0]来获得其中第一个结果。

提示

由于WATCH命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值,所以我们需要在EXEC执行失败后重新执行整个函数。

执行EXEC命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用UNWATCH命令来取消监控。比如,我们要实现hsetxx函数,作用与HSETNX命令类似,只不过是仅当字段存在时才赋值。为了避免竞态条件我们使用事务来完成这一功能:

def hsetxx($key, $field, $value)
  WATCH $key
  $isFieldExists = HEXISTS $key, $field
  if $isFieldExists is 1
     MULTI
     HSET $key, $field, $value
     EXEC
  else
     UNWATCH
  return $isFieldExists

在代码中会判断要赋值的字段是否存在,如果字段不存在的话就不执行事务中的命令,但需要使用UNWATCH命令来保证下一个事务的执行不会受到影响。

时间: 2024-08-03 16:08:38

《Redis入门指南》一4.1 事务的相关文章

《Redis入门指南》一5.3 Python与Redis

5.3 Python与Redis Redis入门指南 Redis官方推荐的Python客户端是redis-py1. 5.3.1 安装 推荐使用pip install redis安装最新版本的redis-py,也可以使用easy_install:easy_install redis. 5.3.2 使用方法 首先需要引入redis-py: import redis 下面的代码将创建一个默认连接到地址127.0.0.1,端口6379的Redis连接: r = redis.StrictRedis() 也

《Redis入门指南》一5.4 Node.js与Redis

5.4 Node.js与Redis Redis入门指南 Redis官方推荐的Node.js客户端是node_redis1. 5.4.1 安装 使用npm install redis命令安装最新版本的node_redis,目前版本是0.8.2. 5.4.2 使用方法 首先加载node_redis模块: var redis = require('redis'); 下面的代码将创建一个默认连接到地址127.0.0.1,端口6379的Redis连接: var client = redis.createC

《Redis入门指南》一第4章  进阶

第4章 进阶 Redis入门指南 没过几天,小白就完成了博客的开发并将其部署上线.之后的一段时间,小白又使用Redis开发了几个程序,用得还算顺手,便没有继续向宋老师请教Redis的更多知识.直到一个月后的一天,宋老师偶然访问了小白的博客-- 本章将会带领读者继续探索Redis,了解Redis的事务.排序与管道等功能,并且还会详细地介绍如何优化Redis的存储空间.

《Redis入门指南》一导读

前 言 Redis入门指南Redis如今已经成为Web开发社区中最火热的内存数据库之一,而它的诞生距现在不过才4年.随着Web 2.0的蓬勃发展,网站数据快速增长,对高性能读写的需求也越来越多,再加上半结构化的数据比重逐渐变大,人们对早已被铺天盖地地运用着的关系数据库能否适应现今的存储需求产生了疑问.而Redis的迅猛发展,为这个领域注入了全新的思维. Redis凭借其全面的功能得到越来越多的公司的青睐,从初创企业到新浪微博这样拥有着几百台Redis服务器的大公司,都能看到Redis的身影.Re

《Redis入门指南》一5.2 Ruby与Redis

5.2 Ruby与Redis Redis入门指南Redis官方推荐的Ruby客户端是redis-rb1,也是各种语言的Redis客户端中最为稳定的一个.其主要代码贡献者就是Redis的开发者之一Pieter Noordhuis. 5.2.1 安装 使用gem install redis安装最新版本的redis-rb,目前的最新版本是3.0.2. 5.2.2 使用方法 创建到Redis的连接很简单: require 'redis' redis = Redis.new 该行代码会默认Redis的地址

《Redis入门指南》一4.2 生存时间

4.2 生存时间 Redis入门指南转天早上宋老师就收到了小白的回信,内容基本上都是一些表示感谢的话.宋老师又看了一下小白发的那篇文章,发现他已经在文末补充了使用事务来解决竞态条件的方法. 宋老师单击了评论链接想发表评论,却看到博客出现了错误"请求超时"(Request timeout).宋老师疑惑了一下,准备稍后再访问看看,就接着忙别的事情了. 没过一会儿,宋老师就收到了一封小白发来的邮件: 宋老师您好!我的博客最近经常无法访问,我看了日志后发现是因为某个搜索引擎爬虫访问得太频繁,加

《Redis入门指南》一4.3 排序

4.3 排序 Redis入门指南 午后,宋老师正在批改学生们提交的程序,再过几天就会迎来第一次计算机全市联考.他在每个学生的程序代码末尾都用注释详细地做了批注--严谨的治学态度让他备受学生们的爱戴. 一个电话打来."小白的?"宋老师拿出手机,"博客最近怎么样了?"未及小白开口,他就抢先问道. 特别好!现在平均每天都有50多人访问我的博客.不过昨天我收到一个访客的邮件,他向我反映了一个问题:查看一个标签下的文章列表时文章不是按照时间顺序排列的,找起来很麻烦.我看了一下

《Redis入门指南》一5.1 PHP与Redis

5.1 PHP与Redis Redis入门指南 Redis官方推荐的PHP客户端是Predis1和phpredis2.前者是完全使用PHP代码实现的原生客户端,而后者则是使用C语言编写的PHP扩展.在功能上两者区别并不大,就性能而言后者会更胜一筹.考虑到很多主机并未提供安装PHP扩展的权限,本节会以Predis为示例介绍如何在PHP中使用Redis. 虽然Predis的性能逊于phpredis,但是除非执行大量Redis命令,否则很难区分二者的性能.而且实际应用中执行Redis命令的开销更多在网

《Redis入门指南》一4.4 消息通知

4.4 消息通知 Redis入门指南 凭着小白的用心经营,博客的访问量逐渐增多,甚至有了小白自己的粉丝.这不,小白刚收到一封来自粉丝的邮件,在邮件中那个粉丝强烈建议小白给博客加入邮件订阅功能,这样当小白发布新文章后订阅小白博客的用户就可以收到通知邮件了.在信的末尾,那个粉丝还着重强调了一下:"这个功能对不习惯使用RSS的用户很重要,希望能够加上!" 看过信后,小白心想:"是个好建议!不过话说回来,似乎他还没发现其实我的博客连RSS功能都没有." 邮件订阅功能太好实现