Redis入门--头歌实验Redis事务与流水线

任务描述

本关任务:编写一个商品交易平台的后端处理逻辑。

相关知识

手机、互联网普遍的当下,系统会同时处理多客户端的请求,而在多客户端同时处理相同的数据时,数据一致性就变得十分重要,稍不谨慎的操作就会导致数据出错。本关卡以用户购买商品这一实际应用场景为背景,实现使用 Redis 事务保证数据一致性。

用户购买商品依托一个商品交易平台进行,该平台中定义了一些数据结构:

  • 用户信息存储在哈希键 users:* 中(其中*是用户ID),记录了两个属性:
    • 用户姓名(name
    • 用户余额(funds
  • 用户仓库用集合键 inventory:* 保存(其中*是用户ID),其中元素为:
    • 商品的唯一标识。
  • 如下所示:

  • 同时我们使用一个有序集合 market 存储商品买卖信息:

  • 成员为:
    • 由商品 ID 和卖家 ID 通过英文字符 . 拼接而成。
    • 例如:ItemO.27
  • 分值为:商品售价。

为了完成本关任务,你需要掌握:1.Redis事务的特性,2.将商品加入平台的实现方式,3.购买商品的实现方式,4.非事务性流水线。

Redis事务的特性
Redis事务概述

Redis 中的事务是一组命令的集合,事务和命令一样,是 Redis 的最小执行单位。事务保证这组命令要么都执行,要么都不执行(All or Nothing)。

事务的原理是将一组命令发送给 Redis,然后再让 Redis 依次执行这些命令:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SADD test:set:1 1
QUEUED
127.0.0.1:6379> SADD test:set:2 2
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 1
2) (integer) 1

Redis 在接收到 EXEC 命令后才会将事务队列中的所有命令依次执行,并获取其执行的结果(返回值)。在事务执行完毕前,用户无法根据事务中命令的结果来做不同处理(提交或回滚)。

由于 Redis 事务是最小执行单位,所以它保证一个事务内的命令不被其他命令插入,在事务执行完毕后,Redis 才会响应其他请求。

Redis事务错误处理

Redis 事务执行遇到错误时,会根据错误原因做不同处理:

  • 语法错误(命令不存在/命令参数错误)
    • 事务的所有命令均不执行
    • 返回错误。
  • 运行中错误(使用不合适的命令操作键等)
    • 除了出现错误的命令不执行,其他均执行
    • 返回每个命令的执行结果。

Redis 事务的错误处理机制可以看出,Redis 不提供关系型数据库的回滚(ROLLBACK)功能,在运行出错时,需要自己将数据库复原到事务执行前的状态。保证事务要么都执行,要么都不执行的特性。

Redis数据一致性保证

要保证数据一致性,就是要防止多个进程同时操作同一个数据时产生资源争抢。加锁是确保数据一致性的方法之一,一般分为乐观锁和悲观锁。

乐观锁与悲观锁

悲观锁(pessimistic locking,关系型数据库会对被访问的数据行进行加悲观锁,直到事务被提交(Commit)或回滚(ROLLBACK)时解除,此时如果其他客户端试图对被加锁数据进行访问,则会被阻塞到第一个事务执行完毕。

悲观锁会导致其他客户端的长时间等待,所以 Redis 采用了乐观锁(optimisitic locking的方式,只在数据被其他客户端修改的情况下,通知所有加锁的客户端,这样客户端不需要等待取得锁的客户端执行完毕,只需要在得到通知时进行重试即可。

所以在 Redis 中,我们通过乐观锁来确保数据一致性。

将商品加入平台的实现方式

将商品放到平台上销售遵循以下规则:

  • 卖家拥有该商品。
    • 扣除卖家商品成功
      • 将商品加入平台。
    • 扣除卖家商品失败
      • 不允许将商品加入平台。
  • 卖家不拥有该商品。
    • 不允许将商品加入平台。

        如果按照传统的 Redis 事务,简单地将扣除卖家商品与将商品加入商品买卖信息有序集合放到一个事务中。当用户通过多终端同时执行操作,或者用户在短时间内多次执行操作时,就会引发数据一致性问题。多个进程同时验证到卖家拥有该商品,并开始扣除卖家商品,由于操作的原子性,第二次及之后的扣除操作会失败,但事务中的其他命令会继续执行,导致卖家将一个商品多次加入平台,从而引发数据出错。

        这时我们需要使用 WATCH 命令(乐观锁)来解决数据一致性问题。WATCH 命令可以监控一或多个键,一旦其中有一个键被修改/删除时,之后的事务都不会被执行。WATCH 命令的语法如下:

WATCH key [key ...]

当监视的 key 被其他命令改动时,事务将被打断并返回一个错误 WatchError,用户可以根据自身需求选择重试或者取消事务。在这里我们就需要对商品所在的用户仓库进行监视,并在捕获到错误时进行重试,重试时间为 5 秒,如果重试失败,则应该取消事务。

此时,将商品加入平台的步骤变为如下:

  • 对用户仓库加乐观锁,监视其变化。
    • 无变化。
      • 用户拥有该商品。
        • 从用户仓库中扣除该商品。
        • 将商品加入到商品买卖信息有序集合中。
      • 用户不拥有该商品。
        • 取消对用户仓库的乐观锁,取消监控。
    • 有变化
      • 进行重试。
      • 限定时间为 5 秒。
import time
import redis

# 函数定义:将商品添加到市场
def add_item_to_market(itemid, sellerid, price):
    # 拼接卖家的库存键名
    repertory = "inventory:" + sellerid
    # 拼接商品键名
    item = itemid + "." + sellerid
    # 设置超时时间为5秒
    end = time.time() + 5
    # 创建 Redis 连接的 pipeline
    pipe = conn.pipeline()
    
    # 在规定时间内执行以下操作
    while time.time() < end:
        try:
            # 监视卖家的库存是否发生变化
            pipe.watch(repertory)
            # 检查卖家是否仍持有该商品
            if not pipe.sismember(repertory, itemid):
                # 如果卖家不再持有该商品,则解除对卖家库存的监视
                pipe.unwatch()
                # 返回空值,表示加入失败(与重试失败区分)
                return None
            # 开始事务操作
            pipe.multi()
            # 将商品添加到市场
            pipe.zadd("market", item, price)
            # 从卖家的库存中移除该商品
            pipe.srem(repertory, itemid)
            # 执行事务
            pipe.execute()
            # 返回 True 表示成功将商品加入市场
            return True
        except redis.exceptions.WatchError:
            # 如果捕获到 WatchError,说明卖家的库存发生了变化,需要重试
            pass
    # 如果超时或重试失败,返回 False
    return False

我们在发现卖家不再拥有该商品时,就意味着不能再对用户仓库进行扣除商品操作,商品不能被加入到平台中,所以就没有必要继续对用户仓库进行监控。此时,我们使用 UNWATCH 命令取消了对用户仓库的监视,并取消将商品加入平台的操作。UNWATCH 命令语法如下:

UNWATCH

需要注意的是UNWATCH 命令会解除对**所有 key **的监视。

当我们确定卖家拥有该商品,且同一时刻没有其他进程对用户仓库进行扣除操作时,可以将商品加入平台,将扣除卖家商品与将商品加入商品买卖信息有序集合放到一个事务中执行。

购买商品的实现方式

成功购买商品需要满足三个条件:

  • 买家用户余额足够购买该商品。
  • 买家用户没有同时在购买其他商品(也就是说:买家余额不变化)
  • 该商品没有被其他用户买走。

通过分析上述条件,可以发现购买商品过程中存在两个独占资源

  • 商品买卖信息中的某商品条目(对应的键:商品买卖信息有序集合)
  • 买家余额(对应的键:买家用户信息)

所以我们需要对这两个独占资源所属的键加乐观锁(进行监视):

# 生成买家键名
buyer = "users:" + buyerid
# 创建 Redis 连接的 pipeline
pipe = conn.pipeline()
# 监视市场和买家键,以便在事务中对它们进行操作
pipe.watch("market", buyer)

乐观锁帮助我们确保后两个条件成立,接下来需要判断该用户余额是否足够购买该商品:

# 获取该商品价格
price = pipe.zscore("market", itemid)
# 获取买家用户余额
funds = int(pipe.hget(buyer, "funds"))
if funds < price:
    pipe.unwatch()

当用户余额不足以购买该商品时,意味着交易不能继续进行,所以需要解除对商品买卖信息和买家个人信息的监视,同时终止交易。

在满足了上述三个条件后,购买商品的过程就可以顺利的进行了:

  • 买家用户余额减去商品价格的数值。
  • 卖家用户余额增加商品价格的数值。
  • 从商品买卖信息有序集合中移除该商品。
  • 为买家用户仓库增加该商品。
# itemid 的格式为 `itemX.userX`,其中:
#   前半部分为商品ID
#   后半部分为卖家ID
# 所以可以使用 split 方法将其分为两段
item, sellerid = itemid.split(".")
seller = "users:" + sellerid
repertory = "inventory:" + buyerid

# 开始 Redis 事务操作
pipe.multi()
# 增加卖家的资金
pipe.hincrby(seller, "funds", int(price))
# 减少买家的资金
pipe.hincrby(buyer, "funds", int(-price))
# 将商品添加到买家的库存
pipe.sadd(repertory, item)
# 从市场中移除该商品
pipe.zrem("market", itemid)
# 执行事务
pipe.execute()

通过对商品买卖信息和买家用户信息加锁,确保了商品被其他买家买走,或者买家账户正在支付其他商品时,程序会阻止用户进行交易,保证了被竞争资源的独占性,避免数据出错,维护了系统数据一致性。

非事务性流水线

使用事务的好处除了在执行时不会被其他命令中断外,还可以通过使用流水线加快事务执行的速度。实际上,在不使用事务的情况下,我们也可以通过使用流水线提高命令的执行效率。

流水线通过一次发送所有命令来减少通信次数,降低通信延迟带来的时间开销,创建流水线的方式在之前就已经使用过:

pipe = conn.pipeline()

按上述方式调用 pipeline 方法时会默认传入 True 参数指定使用事务的方式提交命令,客户端将会使用 MULTIEXEC 命令将所有命令包裹起来,延迟命令的执行。更为重要的是,MULTIEXEC 命令会消耗一定的资源。

所以当我们只需要使用流水线的情况下,我们可以传入 False 参数:

pipe_without_transaction = conn.pipeline(False)

通过使用流水线一次性发送多条命令,可以提高 Redis 的整体性能。

通过下图,我们可以对比不使用流水线(左侧)使用流水线(右侧)的通信过程:

可以明显地看出,通过使用流水线,通信往返次数降低到了原来的三分之一,大大降低了通信时间开销,如果 Redis 和应用服务器通过局域网相连,这样的修改则可以减少24毫秒的时间开销。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import time
import redis

conn = redis.Redis()

# 将商品放到市场上
def add_item_to_market(itemid, sellerid, price):
    # 构建相关键
    repertory = "inventory:" + sellerid  # 卖家的仓库键
    item = itemid + "." + sellerid  # 商品键
    end = time.time() + 5  # 设置超时时间为5秒
    pipe = conn.pipeline()

    # 在超时时间内尝试执行操作
    while time.time() < end:
        try:
            pipe.watch(repertory)  # 监视卖家的仓库
            if not pipe.sismember(repertory, itemid):  # 如果商品不在卖家的仓库中
                pipe.unwatch()
                return None
            pipe.multi()  # 开启事务
            pipe.zadd("market", item, price)  # 将商品添加到市场
            pipe.srem(repertory, itemid)  # 从卖家的仓库中移除商品
            pipe.execute()  # 执行事务
            return True
        except redis.exceptions.WatchError:  # 处理 WatchError 异常
            pass
    return False

# 购买商品
def purchase(buyerid, itemid):
    item, sellerid = itemid.split(".")  # 解析商品和卖家id
    buyer = "users:" + buyerid  # 买家键
    seller = "users:" + sellerid  # 卖家键
    repertory = "inventory:" + buyerid  # 买家的仓库键
    end = time.time() + 10  # 设置超时时间为10秒
    pipe = conn.pipeline()

    # 在超时时间内尝试执行操作
    while time.time() < end:
        try:
            pipe.watch("market", buyer)  # 监视市场和买家键
            price = pipe.zscore("market", itemid)  # 获取商品价格
            funds = int(pipe.hget(buyer, "funds"))  # 获取买家资金
            if funds < price:  # 如果买家资金不足
                pipe.unwatch()
                return None

            pipe.multi()  # 开启事务
            pipe.hincrby(seller, "funds", int(price))  # 增加卖家资金
            pipe.hincrby(buyer, "funds", int(-price))  # 减少买家资金
            pipe.sadd(repertory, item)  # 将商品添加到买家的仓库
            pipe.zrem("market", itemid)  # 从市场中移除商品
            pipe.execute()  # 执行事务
            return True
        except redis.exceptions.WatchError:  # 处理 WatchError 异常
            pass
    return False

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux操作系统逻辑、线性、物理地址,带你轻松理解Linux运维-Hook机制

虚拟内存&#xff08;Virtual Memory&#xff09;是指计算机呈现出要比实际拥有的内存大得多的内存量。因此他允许程式员编制并运行比实际系统拥有的内存大得多的程式。这使得许多大型项目也能够在具有有限内存资源的系统上实现。一个非常恰当的比喻是&#xff1a;你不必非常长…

Replication Controller、ReplicaSet和Deployment(Kubernetes调度系列,结合操作命令讲解)

目录 一、概述 二、Replication Controller 2.1 Replication Controller 说明 2.2 Replication Controller 举例 三、ReplicaSet 3.1 ReplicaSet说明 3.2 ReplicaSet 举例 四、无状态应用管理Deployment 4.1 概述 4.2 创建Deployment 4.2.1 Deployment 标签内容解析 …

视频分块上传Vue3+SpringBoot3+Minio

文章目录 一、简化演示分块上传、合并分块断点续传秒传 二、更详细的逻辑和细节问题可能存在的隐患 三、代码示例前端代码后端代码 一、简化演示 分块上传、合并分块 前端将完整的视频文件分割成多份文件块&#xff0c;依次上传到后端&#xff0c;后端将其保存到文件系统。前…

C++教学——从入门到精通 9.比大小

如果叫你比较a,b,c的大小并排序都会吧&#xff0c;先用我们学过的方法做 #include"iostream" using namespace std; int main(){int a,b,c;cin>>a>>b>>c;if(a>b&&a>c){if(b>c)cout<<c<<" "<<b;else…

Vue2电商前台项目(二):完成Home首页模块业务

一、项目开发的步骤 1、书写静态页面&#xff08;HTML&#xff0c;CSS&#xff09; 2、拆分组件 3、获取服务器的数据动态展示 4、完成相应的动态业务逻辑 经过分析之后&#xff0c;Home首页可以拆分为7个组件&#xff1a;TypeNav三级联动导航&#xff0c;ListContainer&…

先进电机技术 —— 无线电机

一、背景 无线电能传输电机是一种创新的电机设计&#xff0c;它结合了无线电能传输技术与传统的电机工作原理。这种电机的主要特点是通过无线方式传输电能&#xff0c;从而消除了传统电机中需要有线连接的限制&#xff0c;提高了系统的灵活性和可靠性。 无线电能传输技术主要…

C51实现每秒向电脑发送数据(UART的含义)

其实核心的问题是&#xff1a;串口的通信方式 异步串行是指UART&#xff08;Universal Asynchronous Receiver/Transmitter&#xff09;&#xff0c;UART包含TTL电平的串口和RS232电平的串口 UART要实现异步通信的&#xff1a; UART是异步串行接口&#xff0c;通信双方使用时…

LeetCode每日一题之专题一:双指针 ——快乐数

快乐数OJ链接&#xff1a;202. 快乐数 - 力扣&#xff08;LeetCode&#xff09; 题目&#xff1a; 题目分析: 为了房便叙述&#xff0c;将「对于⼀个正整数&#xff0c;每⼀次将该数替换为它每个位置上的数字的平方和」这⼀个 操作记为 x 操作&#xff1b; 题目告诉我们&#…

Shell脚本之基础-2

目录 一、字符处理 cut命令 awk命令 sed命令 字符串排序 二、条件判断 文件类型判断 文件权限判断 两个文件的判断 整数比较 字符串判断 多重判断 三、流程控制 if分支 if else 双分支结构 case分支 for循环 while循环 一、字符处理 cut命令 命令格式&#x…

Python 金融数据分析工具库之zvt使用详解

​​​​​​​ 概要 Python在金融数据分析领域有着广泛的应用,而zvt库作为一款强大的金融数据分析工具,为开发者提供了丰富的功能和灵活的应用接口。本文将深入介绍zvt库的安装、特性、基本功能、高级功能、实际应用场景,并总结其在金融数据分析中的价值和优势。 安装 …

mysql故障排查

MySQL是目前企业最常见的数据库之一日常维护管理的过程中&#xff0c;会遇到很多故障汇总了常见的故障&#xff0c;MySQL默认配置无法满足高性能要求 一 MySQL逻辑架构图 客户端和连接服务核心服务功能存储擎层数据存储层 二 MySQL单实例常见故障 故障1 ERROR 2002 (HY000)…

(echarts)title和legend不重叠/legend图例滚动显示不换行

(echarts)title和legend不重叠/legend图例滚动显示不换行 title和legend都被放置在了不同的位置&#xff0c;从而避免了重叠。你可以根据实际的图表布局和需求调整left&#xff08;水平位置&#xff09;和top&#xff08;垂直位置&#xff09;等属性&#xff0c;确保它们不会相…

【SCI绘图】【箱型图系列1 python】多类对比及各类下属子类对比

SCI&#xff0c;CCF&#xff0c;EI以及核心期刊绘图宝典&#xff0c;爆款更新&#xff0c;助力科研&#xff01; 本期分享&#xff1a; 【SCI绘图】【箱型图系列1】多类对比各类下属子类对比 文末附带完整代码&#xff1a; 1.环境准备 python 3 from matplotlib import pyp…

QT-QPainter

QT-QPainter 1.QPainter画图  1.1 概述  1.1 QPainter设置  1.2 QPainter画线  1.3 QPainter画矩形  1.4 QPainter画圆  1.5 QPainter画圆弧  1.6 QPainter画扇形 2.QGradient  2.1 QLinearGradient线性渐变  2.2 QRadialGradient径向渐变  2.3 QConicalGr…

【Unity每日一记】如何从0到1将特效图集制作成一个特效

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;Uni…

Prometheus+grafana环境搭建Nginx(docker+二进制两种方式安装)(六)

由于所有组件写一篇幅过长&#xff0c;所以每个组件分一篇方便查看&#xff0c;前五篇链接如下 Prometheusgrafana环境搭建方法及流程两种方式(docker和源码包)(一)-CSDN博客 Prometheusgrafana环境搭建rabbitmq(docker二进制两种方式安装)(二)-CSDN博客 Prometheusgrafana环…

数据分析python代码——数据填充

在Python中&#xff0c;我们通常使用pandas库来处理和分析数据。数据填充是数据预处理的一个重要步骤&#xff0c;用于处理数据中的缺失值。以下是使用pandas库进行数据填充的示例代码&#xff1a; 在数据分析中&#xff0c;处理缺失值&#xff08;空值&#xff09;是一个重要…

基于微信小程序的实验室预约系统的设计与开发

个人介绍 hello hello~ &#xff0c;这里是 code袁~&#x1f496;&#x1f496; &#xff0c;欢迎大家点赞&#x1f973;&#x1f973;关注&#x1f4a5;&#x1f4a5;收藏&#x1f339;&#x1f339;&#x1f339; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的…

c语言文件操作(超详细)

前言 这次的博客&#xff0c;可以让大家快速掌握文件操作&#xff0c;方便大家快速找到不懂的内容 文件操作的作用以及基础 1. 为什么使用文件&#xff1f; 如果没有文件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&…

(arxiv2401) CrossMAE

作者团队来自加州大学伯克利分校&#xff08;UC Berkeley&#xff09;和加州大学旧金山分校&#xff08;UCSF&#xff09;。论文主要探讨了在MAE的解码中&#xff0c;图像patch之间的依赖性&#xff0c;并提出了一种新的预训练框架 CrossMAE。 论文的主要贡献包括&#xff1a; …