redis-事务(MULTI、EXEC、DISCARD、WATCH与lua脚本、包含lua脚本的简单介绍、乐观锁抢购案例的实现)

news2025/4/19 10:35:00

https://juejin.cn/post/6891158857708797959

首先Redis事务在实际的场景应用上也占着比较重要的地位,例如在秒杀场景中,我们就可以利用Redis事务中的watch命令监听key,实现乐观锁,保证不会出现冲突,也防止商品超卖。
另外就是Redis事务也是面试过程中面试官着重照顾的基础知识对象,假设面试官问你实现Redis事务有哪些方式?事务发生错误时Redis是怎么处理的?Redis事务支持回滚吗等等这些问题,你是否能脱口而出回答上来呢?如果你对这方便的基础知识有所欠缺,那是不是就栽跟头了呢?

两种实现方式

redis命令 MULTI、EXEC、DISCARD、WATCH

multi开启事务
exec是执行
disccard是放弃事务返回

重点讲下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 sex
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set name saycode
QUEUED
127.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 man
OK

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

watch命令的原理 看 https://juejin.cn/post/6891158857708797959

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 20
1) "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 name
OK
127.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 age
OK
127.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])<tonumber(ARGV[3]) then return -1 else return 1 end else return 1 end" 1 'test_127.0.0.1' 1451460590 5 1800

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

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

Redis事务是否支持回滚

Redis的事务和传统的关系型数据库事务的最大区别在于,Redis不支持事务回滚机制(rollback)。
也就是说:当在事务过程中发生错误时,Redis事务失败时并不进行回滚(roll back),而是继续执行余下的命令。官方给出的理由是这样子的:

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

事务中发生错误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> MULTI
OK
127.0.0.1:6379> set name dashu
QUEUED
127.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> MULTI
OK
127.0.0.1:6379> set name saycode
QUEUED
127.0.0.1:6379> lpop name
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.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');
}

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

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

相关文章

CCLinkIE转ModbusTCP借网关之力打破组态王与三菱PLC通讯隔阂​

在某自动化生产线项目中&#xff0c;客户采用了三菱PLC作为现场控制核心&#xff0c;该PLC支持CCLinkIE现场总线协议。同时&#xff0c;客户希望使用组态王上位机软件进行生产过程的监控与管理&#xff0c;然而组态王上位机更擅长与ModbusTCP协议设备进行通讯。为了解决这一协议…

Linux网络编程第一课:深入浅出TCP/IP协议簇与网络寻址系统

知识点1【网络发展简史】 **网络节点&#xff1a;**路由器和交换机组成 交换机的作用&#xff1a;拓展网络接口 路由&#xff1a;网络通信路径 1、分组交换 分组的目的&#xff1a; 数据量大&#xff0c;不能一次型传输&#xff0c;只能分批次传输&#xff0c;这里的每一批…

GESP2023年12月认证C++七级( 第三部分编程题(2)纸牌游戏)

参考程序&#xff1a; #include <iostream> #include <cstring> // for memset #include <vector> using namespace std;const int max_n 1005; int n; int a[max_n], b[max_n], c[max_n]; // a[]: 得分系数&#xff1b;b[]: 换牌惩罚&#xff1b;c[]: …

HarmonyOS学习 实验九:@State和@Prop装饰器的使用方法

HarmonyOS应用开发&#xff1a;父子组件状态管理实验报告 引言 在HarmonyOS应用开发领域&#xff0c;组件之间的状态管理是一个至关重要的概念。通过有效的状态管理&#xff0c;我们可以确保应用的数据流动清晰、可预测&#xff0c;从而提升应用的稳定性和可维护性。本次实验…

【Ai】MCP实战:手写 client 和 server [Python版本]

什么是mcp MCP 是一个开放协议&#xff0c;它为应用程序向 LLM 提供上下文的方式进行了标准化。你可以将 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 为设备连接各种外设和配件提供了标准化的方式一样&#xff0c;MCP 为 AI 模型连接各种数据源和工具提供了标准化的接口…

Java与C在典型场景下的性能对比深度剖析

&#x1f381;个人主页&#xff1a;User_芊芊君子 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 &#x1f50d;系列专栏&#xff1a;AI 【前言】 在计算机编程领域&#xff0c;Java和C语言都是举足轻重的编程语言。Java以其跨平台性、自动内存管理和丰富…

多智能体 AI 游戏框架(开源程序):竞争、发展、适应

一、软件介绍 文末提供程序和源码下载 SamoAI 在人类和 AI 之间创建了一个无缝的多代理叙事层&#xff0c;实现了跨多个平台的自然协作。通过一致的身份保留和情境记忆&#xff0c;它允许通过一系列行动随着时间的推移而演变的交互&#xff0c;就像人际关系一样。 二、核心概念…

java实现二叉树的前序、中序、后序遍历(递归和非递归方式)以及层级遍历

java实现二叉树的前序、中序、后序遍历以及层级遍历 一、二叉树节点定义二、递归方式1.前序遍历2.中序遍历3.后序遍历 三、非递归方式1.前序遍历2.中序遍历3.后序遍历4.层级遍历5.分层打印 四、测试用例 一、二叉树节点定义 class TreeNode {int val;TreeNode left;TreeNode r…

Solr admin 更新文档

<add><doc><field name"id">1904451090351546368</field><field name"companyName" update"set">测试科技有限公司</field></doc> </add>

【Netty篇】EventLoopGroup 与 EventLoop 详解

目录 开场白&#xff1a;话说 Netty 江湖第一段&#xff1a;EventLoopGroup——“包工头”的角色第二段&#xff1a;EventLoop——“身怀绝技的工人”第三段&#xff1a;EventLoop 如何处理 I/O 事件、普通任务和定时任务第四段&#xff1a;Handler 执行中如何换人&#xff1f;…

操作系统之shell实现(上)

&#x1f31f; 各位看官好&#xff0c;我是maomi_9526&#xff01; &#x1f30d; 种一棵树最好是十年前&#xff0c;其次是现在&#xff01; &#x1f680; 今天来学习C语言的相关知识。 &#x1f44d; 如果觉得这篇文章有帮助&#xff0c;欢迎您一键三连&#xff0c;分享给更…

数据结构与算法——链表OJ题详解(2)

文章目录 一、前言二、OJ续享2.1相交链表2.2环形链表12.2环形链表2 三、总结 一、前言 哦了兄弟们&#xff0c;咱们上次在详解链表OJ题的时候&#xff0c;有一部分OJ题呢up并没有整理完&#xff0c;这一个星期呢&#xff0c;up也是在不断的学习并且沉淀着&#xff0c;也是终于…

Linux 基础知识详解

Linux 基础知识详解 一、快照与克隆 1. &#x1f4f8;快照&#xff08;Snapshot&#xff09; 快照是虚拟机当前运行状态的一次“瞬间拷贝”&#xff0c;包括内存、磁盘、配置等信息。这使得管理员能够快速恢复到某个特定的时间点。 用途&#xff1a; 安全实验前保存状态&am…

centOs7配置有限网络

最简单快速的是使用nmtui命令&#xff0c;采用图形页面修改。 点击编辑连接并回车&#xff1a; 选中编辑然后回车&#xff1a; 千万记住DNS服务器就是子网掩码&#xff0c;不是常说的DNS域名。把地址&#xff0c;网关&#xff0c;子网掩码配置好。只要ip不冲突&#xff0c;网…

C语言 —— 指尖跃迁 刻印永恒 - 文件操作

目录 1. 什么是文件 1.1 程序文件 1.2 数据文件 1.3 文件名 2. 二进制文件和文本文件 3. 文件的打开与关闭 3.1 流和标准流 3.2 文件指针 3.3 文件的打开与关闭 fopen fclose 4. 文件的顺序读写 4.1 fgetc和fputc fgetc fputc 4.2 fgets和fputs fgets fputs…

网络安全与信息安全的区别​及共通

在数字化时代&#xff0c;网络安全与信息安全已成为保障个人、企业乃至国家正常运转的重要防线。尽管二者紧密相关且常被混为一谈&#xff0c;但实则存在显著差异。当然&#xff0c;它们也有一些相同点&#xff0c;比如都以保障数字环境下的安全为核心目标&#xff0c;均需要通…

【愚公系列】《Python网络爬虫从入门到精通》052-Scrapy 编写 Item Pipeline

&#x1f31f;【技术大咖愚公搬代码&#xff1a;全栈专家的成长之路&#xff0c;你关注的宝藏博主在这里&#xff01;】&#x1f31f; &#x1f4e3;开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主&#xff01; &#x1f…

【AI News | 20250416】每日AI进展

AI Repos 1、Tutorial-Codebase-Knowledge 自动分析 GitHub 仓库并生成适合初学者的通俗易懂教程&#xff0c;清晰解释代码如何运行&#xff0c;还能生成可视化内容来展示核心功能。爬取 GitHub 仓库并从代码中构建知识库&#xff1b;分析整个代码库以识别核心抽象概念及其交互…

GIS开发笔记(6)结合osg及osgEarth实现半球形区域绘制

一、实现效果 输入中心点坐标及半径&#xff0c;绘制半球形区域&#xff0c;地下部分不显示。 二、实现原理 根据中心点及半径绘制半球形区域&#xff0c;将其挂接到地球节点。 三、参考代码 void GlobeWidget::drawSphericalRegion(osg::Vec3d point,double radius) {// 使…

element-ui自定义主题

此处的element-ui为基于vue2.x的 由于https://element.eleme.cn/#/zh-CN/theme/preview&#xff08;element的主题&#xff09;报错503&#xff0c; 所以使用https://element.eleme.cn/#/zh-CN/component/custom-theme 自定义主题文档中&#xff0c;在项目中改变scss变量的方…