「第12期」 距离大叔的80期小目标还有68期,今天大叔要跟大家分享的内容是 —— Reids中的事务。同样,这也是redis中重要指数为四颗星的必备基础知识点。下面一起来了解一下吧。

相信大家对Redis并不陌生了吧,对 Redis五种数据类型(String,Hash,List,Set, SortedSet) 的使用也应该是得心应手了。今天为什么要跟大家聊聊Redis的事务呢?

首先Redis事务在实际的场景应用上也占着比较重要的地位,例如在秒杀场景中,我们就可以利用Redis事务中的watch命令监听key,实现乐观锁,保证不会出现冲突,也防止商品超卖。

另外就是Redis事务也是面试过程中面试官着重照顾的基础知识对象,假设面试官问你实现Redis事务有哪些方式?事务发生错误时Redis是怎么处理的?Redis事务支持回滚吗等等这些问题,你是否能脱口而出回答上来呢?如果你对这方便的基础知识有所欠缺,那是不是就栽跟头了呢?

所以,这就是大叔想聊聊Redis事务的必要性所在。下面大叔将围绕以下几点与大家分享:

  • 什么是Redis事务
  • 实现Redis事务有哪些方式
  • Redis事务是否支持回滚
  • 事务中发生错误Redis如何表现
  • Redis事务的实战应用

什么是Redis事务

官方给出的定义是这样子的:

Redis事务可以一次执行多个命令, 并且带有以下两个重要的保证:

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

官方腔换成方言就是:

Redis事务提供了一种 “将多个命令打包, 然后一次性、按顺序地执行” 的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令。

或者你也可以把Redis事务理解为一个队列,开启事务后,往后的提交的Redis命令都会依次入队,遇到触发当前事务指令时,队列中的指令会依次被取出并执行。

「值得注意的是」

“事务中的命令要么全部被执行,要么全部都不执行” 这句话单纯想表达的是:“事务执行需要对应的触发条件(命令)”

下面看个例子先整体了解一下Redis事务:

127.0.0.1:6379> get name"zhangsan"127.0.0.1:6379> get sex"female"127.0.0.1:6379> MULTI  # 开启事务OK127.0.0.1:6379> set name dashuQUEUED                # 命令入队127.0.0.1:6379> set sex maleQUEUED                # 命令入队127.0.0.1:6379> EXEC  # 触发当前事务1) OK2) OK127.0.0.1:6379> get name"dashu"127.0.0.1:6379> get sex"male"127.0.0.1:6379>

实现Redis事务有哪些方式

了解完Redis事务是什么回事后,接下来我们继续看看实现Redis事务有哪些方式。

命令模式

命令模式是实现redis事务比较常见的方式,该方式的主要命令有:MULTI、EXEC、DISCARD、WATCH。

MULTI

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

MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行, 而是被放到一个队列中,等待事务被触发。

EXEC

EXEC 命令负责触发并执行事务中的所有命令

  • 如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。
  • 如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。

EXEC 命令返回的是一个数组, 数组中的每个元素都是执行事务中的命令所产生的回复。 回复元素的先后顺序和命令发送的先后顺序一致。

DISCARD

DISCARD 命令可以理解为是搞破坏的。当 DISCARD 命令被执行时, 事务会被丢弃, 事务队列会被清空, 并且客户端会从事务状态中退出。

我们看个例子:

127.0.0.1:6379> get name"dashu"127.0.0.1:6379> MULTIOK127.0.0.1:6379> set name saycodeQUEUED127.0.0.1:6379> DISCARDOK127.0.0.1:6379> get name"dashu"127.0.0.1:6379>

我们可以看到虽然开启事务后我们重新设置了name的值,但是当我们执行DISCARD命令后,该事务被成功丢弃了,所以当我们再次获取name的值的时候,我们可以看到它的值并没有发生改变。

WATCH

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

看例子:

  • 首先我们在一个Redis客户端一上使用 WATCH 命令监控两个key,分别为name和sex,然后开启事务,在事务中修改name的值,
  • 在客户端一执行 EXEC 命令之前,我们另外开一个客户端二,在客户端二中我们修改sex的值为man
  • 接着我们回到客户端一执行 EXEC 命令
# 客户端一127.0.0.1:6379> get name"dashu"127.0.0.1:6379> get sex"male"127.0.0.1:6379> WATCH name sexOK127.0.0.1:6379> MULTIOK127.0.0.1:6379> set name saycodeQUEUED127.0.0.1:6379> EXEC(nil)                  # 事务失败 127.0.0.1:6379> get sex"man"127.0.0.1:6379> get name"dashu"

#--------- 这是一条分割线 ---------#

# 客户端二127.0.0.1:6379> get sex"male"127.0.0.1:6379> set sex manOK

从上面执行的结果可以看到,客户端一中的事务失败了,事务中所修改的name的值也不成功。主要原因是:调用 EXEC 命令执行事务时,被监控的sex 被客户端二修改了,所以客户端一的事务不再执行

WATCH命令的实现

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

比如说,以下字典就展示了一个 watched_keys 字典的例子:


其中, 键 key1 正在被 client2 、 client5 和 client1 三个客户端监视, 其他一些键也分别被其他别的客户端监视着。

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

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


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

WATCH的触发原理

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


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

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

了解完其工作原理后,我们发现该 WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。

上面讲到的是如何给我们需要的key加监控,那我们应该如何取消监控呢?

  • 实际上,当 EXEC 被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。
  • 另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。
  • 使用无参数的 UNWATCH 命令可以手动取消对所有键的监视

2、Lua脚本

除了上面介绍的命令模式可以实现Redis事务外,其实还有一种非常重要的方式:Lua脚本。

为什么要夸Lua脚本呢?我们来看看Lua脚本有什么优势:

  • 原子操作:Redis确保脚本执行期间,其它任何脚本或者命令都无法执行。也就是说,在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
  • 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延。因此使用脚本要更简单,速度更快
  • 复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。

香吗?真香!反正用过的都说好。可以看到相比命令模式还是优势还蛮大的。

那么Lua脚本要怎么用呢?下面跟大家介绍几个常见的常用的命令:

EVAL

EVAL 可以理解为是lua脚本的解释器,它的语法格式如下:

EVAL script numkeys key [key ...] arg [arg ...]
  • script:一段 Lua 脚本或 Lua 脚本文件所在路径及文件名。
  • numkeys:Lua 脚本对应参数数量
  • key [key ...]:Lua 中通过全局变量 KEYS 数组存储的传入参数
  • arg [arg ...]:Lua 中通过全局变量 ARGV 数组存储的传入附加参数

官方腔有点重对吧,没事,咱们来看个例子:

eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

eval的第一个参数是脚本的内容,第二个参数是脚本里面KEYS数组的长度(不包括ARGV参数的个数),这里是两个;紧接着就会有两个参数,用于传递个KEYS数组;后面剩下的参数全部传递给ARGV数组,相当于命令行参数。

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 username age jack 201) "username"2) "age"3) "jack"4) "20"

redis.call() / redis.call()

如果我们想在lua脚本中调用redis的命令该如何操作?其实我们可以在脚本中使用 redis.call() 或 redis.pcall() 直接调用。两者用法类似,只是在遇到错误时,返回错误的提示方式不同。

举个例子:

127.0.0.1:6379> get name"saycode"127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'dashu')" 1 nameOK127.0.0.1:6379> get name"dashu"127.0.0.1:6379> eval "return redis.call('get','name')" 0"dashu"127.0.0.1:6379>

SCRIPT LOAD 和 EVALSHA

  • SCRIPT LOAD:提前载入 Lua 脚本,返回对应脚本的 SHA1 摘要
  • EVALSHA:执行脚本,与EVAL相似,只不过它的参数为脚本的 SHA1 摘要

SCRIPT LOAD 和 EVALSHA 经常配合使用。我们看个例子:

127.0.0.1:6379> SCRIPT LOAD "return redis.call('set',KEYS[1],'30')""6445747e70ce11ad0b9717d78e8ff16fb0faed46"127.0.0.1:6379> evalsha 6445747e70ce11ad0b9717d78e8ff16fb0faed46 1 ageOK127.0.0.1:6379> get age"30"127.0.0.1:6379>

更多命令可以参看Redis Script 官方文档

有了上面的知识,我们就可以使用lua脚本来灵活的使用redis的事务,这里举几个简单的例子:

场景1:使用redis限制30分钟内一个IP只允许访问5次

思路:每次想把当前的时间插入到redis的list中,然后判断list长度是否达到5次,如果大于5次,那么取出队首的元素,和当前时间进行判断,如果在30分钟之内,则返回-1,其它情况返回1。我们来看一下具体实现:

eval "redis.call('rpush', KEYS[1],ARGV[1]);if (redis.call('llen',KEYS[1]) >tonumber(ARGV[2])) then if tonumber(ARGV[1])-redis.call('lpop', KEYS[1]) 1 'test_127.0.0.1' 1451460590 5 1800

Lua脚本 对于实现Redis事务确实是一种不错的选择,相信未来会有越来越多的开发者倾向于使用脚本来实现事务。不过我们在使用的时候也要注意以下两点:

  • 注意Redis版本。脚本功能是 Redis 2.6 才引入的。
  • 由于脚本执行的原子性,所以我们不要在脚本中执行过长开销的程序,否则会验证影响其它请求的执行。

好了,以上就是实现Redis事务方式的有关内容,如果你之前还没有了解到第二种脚本方式,赶紧给大叔点赞打call吧哈哈~

我们接着往下看。

Redis事务是否支持回滚

Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback)。

也就是说:当在事务过程中发生错误时,Redis事务失败时并不进行回滚(roll back),而是继续执行余下的命令。官方给出的理由是这样子的:

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

看个例子:

127.0.0.1:6379> get name"dashu"127.0.0.1:6379> MULTIOK127.0.0.1:6379> set name saycodeQUEUED127.0.0.1:6379> lpop nameQUEUED127.0.0.1:6379> EXEC1) OK2) (error) WRONGTYPE Operation against a key holding the wrong kind of value127.0.0.1:6379> get name"saycode"127.0.0.1:6379>

上面例子中,我们在事务中重新设置name的值,并且使用一个命令去操作一个错误的数据类型,可以看到最终事务还是成功执行了,同时也会返回事务中发生错误的指令的出错原因

事务中发生错误Redis如何表现

实际上,事务的错误我们可以总结两种情况:

  • 一种是:事务在执行 EXEC 之前,入队的命令可能会出错。比如命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。

对于发生在 EXEC 执行之前的错误,客户端的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。看例子:

127.0.0.1:6379> get name"saycode"127.0.0.1:6379> get sex"man"127.0.0.1:6379> MULTIOK127.0.0.1:6379> set name dashuQUEUED127.0.0.1:6379> sett sex woman(error) ERR unknown command `sett`, with args beginning with: `sex`, `woman`,127.0.0.1:6379> EXEC(error) EXECABORT Transaction discarded because of previous errors.127.0.0.1:6379> get name"saycode"127.0.0.1:6379> get sex"man"
  • 还有一种是:命令可能在 EXEC 调用之后失败。比如事务中的命令可能处理了错误类型的键,例如将列表命令用在了字符串键上面

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

127.0.0.1:6379> get name"dashu"127.0.0.1:6379> MULTIOK127.0.0.1:6379> set name saycodeQUEUED127.0.0.1:6379> lpop nameQUEUED127.0.0.1:6379> EXEC1) OK2) (error) WRONGTYPE Operation against a key holding the wrong kind of value127.0.0.1:6379> get name"saycode"127.0.0.1:6379>

我们可以看到:即使事务中有某条/某些命令执行失败了, 事务队列中的其他命令仍然会继续执行 —— Redis 不会停止执行事务中的命令。

Redis事务的实战应用

了解完Redis事务的基础,最后我们来写个Demo来实现乐观锁,业务场景是商品抢购,伪代码如下:

# 乐观锁public function actionBuy(){    $userId = mt_rand(1,99999999);    $goods = $this->goods;    $redis = Yii::$app->redis;    $lock = "Huawei p40";

    try {        $inventory['num'] = $redis->get('goodNums');        if($inventory['num']<=0){            throw new \Exception('活动结束');        }

        $redis->watch($lock);        $redis->multi();

        //todo:这里还需要重新判断下库存,否则会出现超发,高并发情况下$inventory['num']肯定会出现同时读取一个值;为了方便测试,没写db操作        //redis事务是将命令放入队列中,无法取goodNums来判断库存是否结束,此处使用数据库来判断库存合理

        //业务处理  减库存,创建订单        $redis->decr('goodNums');        $redis->sadd('order',$userId);

        $redis->exec();

        Common::addLog('shop.log',$userId.' 抢购成功');    }catch (\Exception $e){        $redis->discard();        Common::addLog('shop.log',$e->getMessage());        throw new \Exception('抢购失败');    }

    die('success');}

好了,今天的分享就到这里了,关注公众号「大叔说码」 获取更多干货,我们下期见~


参考:

1、 https://redis.io/topics/transactions

2、https://zhuanlan.zhihu.com/p/146865185

3、https://walkingsun.github.io/WindBlog/2019/03/14/redis/

4、https://blog.csdn.net/fangjian1204/article/details/5058508

5、https://redis.io/commands/eval

6、https://techlog.cn/article/list/10183180

redis怎么修改_面试官问我Redis事务,还问我有哪些实现方式相关推荐

  1. java如何实现redis分片存储_面试官:你说一下Redis吧,怎么实现高可用,还有持久化怎么做的?...

    前言 作为Java程序员,在面试过程中,缓存相关的问题是躲不掉的,肯定会问,例如缓存一致性问题,缓存雪崩.击穿.穿透等.说到缓存,那肯定少不了Redis,我在面试的时候也是被问了很多关于Redis相关 ...

  2. 阅文java面试_面试官:说说Redis的Hash底层 我:......(来自阅文的面试题)

    redis源码分析系列文章 前言 hello,各位小可爱们,又见面了.今天这篇文章来自去年面试阅文的面试题,结果被虐了.这一part不说了,下次专门开一篇,写下我面试被虐的名场面,尴尬的不行,全程尬聊 ...

  3. 多项目加载顺序修改_面试官:Java 类在 Tomcat 中是如何加载的?

    说到本篇的Tomcat类加载机制,不得不说翻译学习Tomcat的初衷. 之前实习的时候学习JavaMelody的源码,但是它是一个Maven的项目,与我们自己的Web项目整合后无法直接断点调试. 后来 ...

  4. mysql释放练级_面试官:谈谈Mysql事务隔离级别?

    当我们的数据库的引擎是InnoDB的时候. 事务的隔离级别分为:未提交读(read uncommitted).已提交读(read committed).可重复读(repeatable read).串行 ...

  5. 3pc在mysql的实现_面试官:了解分布式事务?讲讲你理解的2PC和3PC原理

    分布式事物基本理论:基本遵循CPA理论,采用柔性事物特征,软状态或者最终一致性特点保证分布式事物一致性问题. 分布式事物常见解决方案:2PC两段提交协议 3PC三段提交协议(弥补两端提交协议缺点) T ...

  6. Redis综述篇:与面试官彻夜长谈Redis缓存、持久化、淘汰机制、哨兵、集群底层原理!...

    点击上方关注 "终端研发部" 设为"星标",和你一起掌握更多数据库知识 于哥你好,最近面试挺多的,尤其是在问到java面试题,Redis被问的特别多,比如Red ...

  7. 《吊打面试官》系列-Redis常见面试题

    前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难. 作为一个在互联网公司面一次拿一次Offer的面霸,打败了无数竞 ...

  8. 当面试官说“还有哪些问题需要问”该如何回答?

    很多面试官在面试结束前都会问"你有什么问题要问我?"这是求职者了解公司的重要机会,也可以利用这个时机表忠心,表决心,展现自己的优势.当面试官说"还有哪些问题需要问&quo ...

  9. 面试官:你还有什么问题要问的吗?

    面对HR或者其他Level比较低的面试官时 能不能谈谈你作为一个公司老员工对公司的感受? (这个问题比较容易回答,不会让面试官陷入无话可说的尴尬境地.另外,从面试官的回答中你可以加深对这个公司的了解, ...

最新文章

  1. C语言 小游戏之贪吃蛇
  2. (一)操作系统概论复习要点笔记
  3. Bootstrap系列 -- 37. 基础导航样式
  4. android tombstone发生过程,Android Tombstone解决步骤
  5. 为什么objc_msgSend必须用汇编实现
  6. “直播带货”还能火多久?
  7. 教你彻底学会Java序列化和反序列化
  8. npu算力如何计算_华为云郑叶来:多元算力驱动应用创新
  9. HTML5写的简单小游戏-绵羊快跑
  10. 学习POI处理word
  11. 凸优化第五章对偶 作业题
  12. C语言复习 -- 知识点总结(全)
  13. 三角网导线平差实例_导线平差实例(一):简易平差
  14. qlistview 自定义控件_QT中QListView中放置自定义控件并添加滚动条
  15. java poi 水印_poi excel如何设置水印透明度
  16. pandas 中delete、drop函数的用法
  17. 服装尺寸 html,服装尺寸表
  18. tr命令解析_学习笔记
  19. 阿里云ECS(centos)中安装Tomcat
  20. 大富翁11 V1.0.7 官方中文绿色免安装版

热门文章

  1. python打印字典树形_Python实现字典树
  2. pythonlist循环添加元素_list.append()在for循环中每次添加的都是最后的一个元素汗血宝马...
  3. php防伪溯源x系统_区块链溯源防伪追溯系统开发解决方案
  4. git安装 tor_Tortoisegit图文使用教程
  5. qq如何用其他进制登录
  6. linux系统用户管理
  7. leetcode刷题:求容器中能乘最大多少水
  8. mysql硬解析与软解析_SQL 软解析和硬解析详解
  9. 深圳先进院研究生计算机专业,2020年中科院深圳先进技术研究院全日制硕士研究生统考专业说明...
  10. pytorch torch.zeros