13 如何利用缓存实现万级并发扣减

news2024/11/19 13:27:04

在上一讲的实现方案里我们讨论了采用纯数据库的扣减实现方案,如果以常规的机器或者 Docker 来进行评估,此方案较难实现单机过万的 TPS。之所以介绍,是想告诉你,架构是面向业务功能、成本、实现难度、时间等因素的取舍,而不是绝对地追求高性能、高并发及高可用等非功能性指标。

另外,在上一讲里介绍的扣减业务的技术实现需求点、数据库表结构信息等内容,其实是和技术无关的,它们属于通用的基本信息和标准定义。因此,今天我们讲解的方案将直接复用以上信息,不再赘述,有忘记的或者直接跳读此讲的同学可以翻到上一讲进行复习。

这一讲,我将由浅入深地介绍如何基于缓存来实现单机万级这一并发扣减目标。

纯缓存方案浅析

纯数据库的方案虽然避免了超卖与少卖的情况,但因采用了事务的方式保证一致性和原子性,所以在 SKU 数量较多时性能下降较明显。

注:事务本质上有四个特点 ACID:原子性(Atomicity)、一致性(Consistency )、隔离性(Isolation)及 持久性(Durability)。

因为扣减有一个要求即当一个 SKU 购买的数量不够时,整个批量扣减就要回滚,因此,我们需要使用类似 for 循环的方式对每一个扣减 SQL 的返回值进行检查。另外一个原因是,当多个用户买一个 SKU 时,它的性能也并不乐观。因为当出现高并发扣减或者并发扣减同一个 SKU 时,事务的隔离性会导致加锁等待以及死锁情况出现。

现在看来,实现单机万级的并发扣减好像遥遥无期了。别急,还记得在上一讲里强调的,架构是在对问题清晰定义之后演化来的理念吗?

下面我们对问题再次梳理一遍,进而寻找可升级演化的方案。

首先,你要知道扣减只需要保证原子性即可,并不需要数据库提供的 ACID。在扣减库存时,重点是保证商品不超卖不少卖。而持久化这个功能,只有在数据库故障切换及恢复时才有需要,因为被中断的事务需要持久化的日志进行重演,也就是说持久化是主功能之外的后置功能、附加功能。

那么我们是否可以去掉持久化这一后置的、附加的功能?或者是否存在可替代方案?你先不着急回答这个问题,跟着我的思路咱们继续分析,自会寻得答案。

其次,在提升性能方面最简单、最快速的方案便是升级硬件。不管你使用的是哪一个厂商的数据库实现,提升或者替换部署数据库机器的硬件配置,都可以显著提升性能。虽然提升硬件可以解决问题,但与此同时也有另外一个问题——硬件的资金成本非常昂贵,动辄上百万、千万。

经过上面的分析,再请你思考第二个问题:为什么当年阿里浩浩荡荡的发起了去 IOE 运动,转而采用性能相对较弱的 MySQL 及相对应的硬件呢?

究其原因也是资金成本的考虑。此时,我们可以转换一个思路,既然提升或者替换机器配置可以提升性能,按此套路,是不是提升或者替换数据库存储也是一种方案?在不改变机器配置的情况下,把传统的 SQL 类数据库替换为性能更好的 NoSQL 类数据存储试试?

是不是有一个性能又好同时又能够满足扣减多个 SKU 具有原子性的 NoSQL 数据库呢?行文至此,答案显然是可以的。

Redis 作为最近几年非常流行的 NoSQL 数据库,它的原始版本或者改造版本基本上已经被国内所有互联网公司或者云厂商所采用。不管是微博爆点事件的流量应对,还是电商的大促流量处理,它的踪影无处不在,可见它在高性能上的能力是首屈一指。另外,因为 Redis 是开源软件且架构简单,部署在普通的 Docker 即可,成本非常低。

此外,Redis 采用了单线程的事件模型,保障了我们对于原子性的要求。对于单线程的事件模型,简单的比喻就是说当我们多个客户端给 Redis 同时发送命令后,Redis 会按接收到的顺序进行串行的执行,对于已经接收而未能执行的命令,只能排队等待。基于此特性,当我们的扣减请求在 Redis 执行时,也即是原子性的。此特性刚好符合我们对于扣减原子性的要求。

方案实现剖析

在确定了使用缓存来完成扣减和高性能后,为了帮助你理解,这里我们结合扣减服务的整体架构图来进一步分析:

图 1:纯缓存架构

上图中的扣减服务和上一讲里的扣减服务一样,都提供了三个在线接口。但此时扣减服务依赖的是 Redis 缓存而不是数据库了。我们顺着上一讲的思路,继续以库存为场景讲解扣减服务的实现。

缓存中存储的信息和上一讲中的数据库表结构基本类似,包含当前商品和剩余的库存数量和当次的扣减流水,这里要注意两点。

  • 首先,因为扣减全部依赖于缓存不依赖数据库,所有存储于 Redis 的数据均不设置过期并全量存储。

  • 其次,Redis 是以 k-v 结构为主,伴随 hash、set 等结构,与 MySQL 以表 + 行为主的结构有一定的差异。Redis 中的库存数量结构大致如下:

key为:sku_stock_{sku}。前缀sku_stock是固定不变,
所有以此为前缀的均表示是库存。{sku}是占位符,在实际存储时被具体的skuid替代。 
value:库存数量。当前此key表示的sku剩余可购买的数量。

在实际应用中,上述 key 的 sku_stock_ 前缀一般会简写成 ss_ 或者可以起到和其他 key 区分的较短形式。当我们存储的 SKU 有百万、千万级别时,此方式可极大地降低存储空间,从而降低成本,毕竟内存是比较昂贵的。

对于 Redis 中存储的流水表采用 hash 结构,即 key + hashField + hashValue 的形式。结构大致如下:

key:sx_{sku}。前缀sx_是按上述缩短的形式设计的,
   只起到了区分的作用。{sku}为占位符 hashField:此次扣减流水编号。 
hashValue: 此次扣减的数量

在一次扣减时,会按 SKU 在 Redis 中先扣减完库存数量再记录流水信息。

我们在上一讲里已经介绍过,扣减接口支持一次扣减多个 SKU + 数量。查询 Redis 的命令文档时你会发现:

  • 首先,Redis 对于 hash 结构不支持多个 key 的批量操作;

  • 其次,Redis 对于不同数据结构间不支持批量操作,比如 KV 与 Hash 间。

如果对于多个 SKU 不支持批量操作,我们就需要按单个 SKU 发起 Redis 调用。在上文中提到过,Redis 不对命令间保证单线程执行。如果采用上述 Redis 的数据结构,一次扣减必须要发起多次对 Redis 的命令才可完成。这样,上文提到的利用 Redis 单线程来保证扣减的原子性此时则满足不了了。

针对上述问题,我们可以采用 Redis 的 lua 脚本来实现批量扣减的单线程诉求。

lua 是一个类似 JavaScript、Shell 等的解释性语言,它可以完成 Redis 已有命令不支持的功能。用户在编写完 lua 脚本之后,将此脚本上传至 Redis 服务端,服务端会返回一个标识码代表此脚本。在实际执行具体请求时,将数据和此标识码发送至 Redis 即可。Redis 会和执行普通命令一样,采用单线程执行此 lua 脚本和对应数据。

当用户调用扣减接口时,将扣减的 SKU 及对应数量 + 脚本标示传递至 Redis 即可,所有的扣减判断逻辑均在 Redis 中的 lua 脚本中执行,lua 脚本执行完成之后返还是否成功给客户端。

lua 脚本执行流程

当请求发送到 Redis 后,lua 脚本执行流程如下图 2 所示:

图 2:lua 脚本执行流程

Redis 中的 lua 脚本执行时,首先会使用 get 命令查询 uuid 是否已存在,如已存在则直接返回,并提示用户请求重复。当防重通过后,会按 SKU 批量获取对应的剩余库存状态并进行判断,如果其中一个 SKU 此次扣减的数量大于剩余数量,则直接给扣减服务返回错误并提示数量不足。通过 Redis 的单线程模型,确保当所有 SKU 的扣减数量在判断均满足后,在实际扣减时,数量不够的情况是不会出现的。同时,单线程保证判断数量的步骤和后续扣减步骤之间,没有其他任何线程出现并发的执行。

判断数量满足之后,lua 脚本后续就可以按 SKU 进行循环的扣减数量并记录流水。

当 Redis 扣减成功后,扣减接口会异步的将此次扣减内容保存至数据库。异步保存数据库的目的是防止出现极端情况—— Redis 宕机后数据未持久化到磁盘,此时我们可以使用数据库恢复或者校准数据。

最后,在纯缓存的架构图(图 2)中还有一个运营后台,它直接连接了数据库,是运营和商家修改库存的入口。当商品补齐了新的货物时,商家在运营后台将此 SKU 库存数量加回。同时,运营后台的实现需要将此数量同步的增加至 Redis,因为当前方案的所有实际扣减都在 Redis 中。

至此,采用纯缓存扣减的基本方案已经介绍结束了。因为实际的压测和很多因素相关,比如机器配置、压测的参数等,此处就不给出具体数字。但目前这个方案已经可以满足支撑单机万级的扣减了。下面我们再来看一看如何应对异常情况。

异常情况分析

因为 Redis 不支持 ACID 特性,导致在使用 Redis 进行扣减时相比纯数据库方案有较多异常场景需要处理,此处我挑选几个重要的给你讲解。

第一个场景是 Redis 突然宕机的场景。

如果 Redis 宕机时,请求在 Redis 中只进行了前置的防重和数量验证,此时则没有任何影响,直接返回给客户扣减失败即可。

但如果此时 Redis 中的 lua 脚本执行到了扣减逻辑并做了实际的扣减,则会出现数据丢失的情况。因为 Redis 没有事务的保证,宕机时已经扣减的数量不会回滚。宕机导致扣减服务给客户返回扣减失败,但实际上 Redis 已经扣减了部分数据并刷新了磁盘,当此 Redis 故障处理完成再次启动后或者 failover 之后,部分库存数量已经丢失了。

为了解决此问题,可以使用数据库中的数据进行校准。常见方式是开发对账程序,通过对比 Redis 与数据库中的数据是否一致,并结合扣减服务的日志。当发现数据不一致同时日志记录扣减失败时,可以将数据库比 Redis 多的库存数据在 Redis 中进行加回。

第二个场景是扣减 Redis 完成并成功返回给客户后,异步刷新数据库失败了的情况。

此时,Redis 中的数据库是准的,但数据库中的库存数据是多的。在结合扣减服务的日志确定是 Redis 扣减成功但异步记录数据失败后,可以将数据库比 Redis 多的库存数据在数据库中进行扣减。

升级纯缓存实现方案

上述的纯缓存方案在使用了 Redis 进行扣减实现后,基本上完成了扣减的高性能和高并发,满足了我们最初的需求。那整体方案上还有哪些可以优化的空间呢?

在“第 12 讲”里我们介绍过,扣减服务不仅包含扣减接口还包含数量查询接口。查询接口的量级相比写接口至少是十倍以上,即使是使用了缓存进行抗量,但读写都请求了同一个 Redis,将会导致扣减请求被读影响。

其次,运营在后台进行操作增加或者修改库存时,是在修改完数据库之后在代码中异步修改刷新 Redis。因为数据库和 Redis 不支持分布式事务,为了保证在修改时它们数据的一致性,在实际开发中,需要增加很多手段保证数据一致性,成本较高。

对于上述两个问题,我们可以做两方面的改造。

第一个是和“第 12 讲”里 MySQL 的优化方案思路一样,增加一个 Redis 从结点,在扣减服务里根据请求类型路由到不同的 Redis 节点。使用主从分离的好处是,不用太多的数据同步开发,直接使用 Redis 主从同步方案,成本低开发量小。

第二个是运营后台修改数据库数量后同步至 Redis 的逻辑使用 binlog 进行处理,关于如何接入和使用 binlog,你可以参见“第 12 讲”的内容。

当商家修改了数据库中的数量之后,MySQL 等数据库的 binlog 会自动发出,在数据转换模块接受 binlog 并转换格式插入 Redis 即可。因为 binlog 消费是采用 ack 机制,如果在转换和插入 Redis 时出错,ack 不确定即可。下一次数据转换代码运行时,会继续上一次未消费的 binlog 继续执行。最终,binlog 的机制不需要太多逻辑处理即可达到最终一致性。相比采用不借助 binlog 的方式,此方案成本和复杂度均较低。

优化后的整体方案如下图 3 所示:

图 3:纯缓存升级版架构

纯缓存方案适用性分析

相比于纯数据库扣减方案,纯缓存方案也存在一定的优缺点和适用性。

纯缓存方案的主要优点是性能提升明显。使用缓存的扣减方案在保证了扣减的原子性和一致性等功能性要求之外,相比纯数据库的扣减方案至少提升十倍以上。

除了优点之外,纯缓存的方案同样存在一些缺点。Redis 及其他一些缓存实现,为了高性能,并没有实现数据库的 ACID 特性。导致在极端情况下可能会出现丢数据,进而产生少卖。另外,为了保证不出现少卖,纯缓存的方案需要做很多的对账、异常处理等的设计,系统复杂度会大幅增加。

对于纯缓存的扣减的优缺点有了一定了解后,可以发现纯缓存在抗并发流量时,效果非常显著。因此,它较适合应用于高并发、大流量的互联网场景。但在极端情况下,可能会出现一些数据的丢失。因此,它优先适合对于数据精度不是特别苛刻的场景,比如用户购买限制等。

但如果上述的异常场景都有降级方案应对,保证最终一致性。它也是可以应用在库存扣减、积分扣减等等场景。在我所经历的和了解的实践中,是有很多公司将此方案应用在非常精度的场景里的。

本节总结

在上一讲中的纯数据库方案无法完全满足量级要求时,本讲介绍了纯缓存的扣减方案。着重讲解了为什么纯缓存可以满足扣减的功能需求,对于分析的过程希望你能够理解并应用,而不是关注最终提出的方案。作为一名优秀的开发人员,你要知道架构图是一个最终态,是静止的,它并不能 100% 直接应用到你所面对的场景,而分析思路却是可以复制和模仿的。

其次,本讲也分析了纯缓存方案存在的一些异常场景。在实战中,正常流程是简单的,而异常流程的思考与处理十分的复杂与烦琐,同时也最能体现技术性,请你务必注意与加强。

最后,我再留给你一道思考题,如果此处的 Redis 是一个集群,而不是一个单独实例,方案应该如何演化?你可以把你的答案、思路或者课后总结写在留言区,我们一起交流。

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

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

相关文章

广工电工与电子技术实验报告-8路彩灯循环控制电路

实验代码 module LED_water (clk,led); input clk; output [7:0] led; reg [7:0] led; integer p; reg clk_1Hz; reg [7:0] current_state, next_state; always (posedge clk) begin if(p25000000-1)begin …

对2023年图灵奖揭晓看法

2023年图灵奖揭晓,你怎么看? 2023年图灵奖,最近刚刚颁给普林斯顿数学教授 Avi Wigderson!作为理论计算机科学领域的领军人物,他对于理解计算中的随机性和伪随机性的作用,作出了开创性贡献。这些贡献不仅推…

C++修炼之路之多态---多态的原理(虚函数表)

目录 一:多态的原理 1.虚函数表 2.原理分析 3.对于虚表存在哪里的探讨 4.对于是不是所有的虚函数都要存进虚函数表的探讨 二:多继承中的虚函数表 三:常见的问答题 接下来的日子会顺顺利利,万事胜意,生活明朗--…

PPSSPPSDL for Mac v1.17.1 PSP游戏模拟器(附500款游戏) 激活版

PPSSPPSDL for Mac是一款模拟器软件,它允许用户在Mac上运行PSP(PlayStation Portable)游戏。通过这款模拟器,用户可以体验到高清甚至更高的分辨率的游戏画面,同时还能够升级纹理以提升清晰度,并启用后处理着…

安卓手机连接电脑实用技巧:实现文件传输与共享

在手机使用过程中,我们常常需要将手机中的文件传输到电脑,或者将手机与电脑进行共享。为了实现这一需求,掌握一些实用的安卓手机连接电脑技巧就显得尤为重要。本文将为您详细介绍2种简单、高效且安全的方法,让您轻松实现安卓手机与…

【网络安全】安全事件管理处置 — 事件分级分类

专栏文章索引:网络安全 有问题可私聊:QQ:3375119339 目录 一、安全事件分级 二、应急事件分级 三、安全事件分类 四、常见安全事件原因分析 1.web入侵 2.漏洞攻击 3.网络攻击 一、安全事件分级 在对安全事件的应急响应过程中&#xf…

如何最大程度使用AWS?

随着云计算技术的不断发展,AWS已经成为众多企业的首选,为其提供了强大的基础设施和服务。那么如何最大程度地、灵活地利用AWS,成为许多企业专注的焦点。九河云作为AWS的合作伙伴,为读者们提供一些技巧和策略,帮助读者充…

物联网鸿蒙实训解决方案

一、建设背景 在数字化浪潮汹涌的时代,华为鸿蒙系统以其前瞻的技术视野和创新的开发理念,成为了引领行业发展的风向标。 据华为开发者大会2023(HDC. Together)公布的数据,鸿蒙生态系统展现出了强劲的发展动力&#x…

Qt : 禁用控件默认的鼠标滚轮事件

最近在写一个模拟器,在item中添加了很多的控件,这些控件默认是支持鼠标滚动事件的。在数据量特别大的时候,及容易不小心就把数据给修改了而不自知。所有,我们这里需要禁用掉这些控件的鼠标滚轮事件。 实现的思想很简单&#xff0c…

[Swift]组件化开发

一、组件化开发基础 1.组件定义 在软件开发中,一个组件是指一个独立的、可替换的软件单元,它封装了一组相关的功能。组件通过定义的接口与外界交互,并且这些接口隔离了组件内部的实现细节。在Swift语言中,组件可以是一个模块、一…

新品发布!无人机装调检修实训系统

近年,我国密集出台相关产业政策,推动低空经济从探索走向发展,根据新华网数据,2030年低空经济规模有望达2万亿。无人机专业属于跨学科的综合性专业,其中装调检测技术是无人机教培的重要组成部分。 天途推出无人机装调检…

easyExcel - 带图片导出

目录 前言一、情景介绍二、问题分析三、代码实现1. 单图片导出2. 多图片导出3. 多图片导出(优化) 前言 Java-easyExcel入门教程:https://blog.csdn.net/xhmico/article/details/134714025 之前有介绍过如何使用 easyExcel,以及写…

分布式文件系统--MinIO

1 MinIO安装(Docker) ●在root目录下新建docker_minio文件夹 ●在docker_minio文件夹下新建config文件夹,data文件夹 ●在root目录下新建docker_compose文件夹,在docker_compose文件夹中添加docker-compose.yaml services:minio:image: quay.io/minio/miniocontainer_name: mi…

Maya vs Blender:制作3D动画首选哪一个?

就 3D 动画而言,有两款3D软件引发了最多的争论:Blender 与 Maya。这两个强大的平台都提供强大的工具集,使动画故事和角色栩栩如生。但作为一名3D动画师,您应该投入时间学习和创作哪一个呢?下面我将从以下六点给您一个清…

从0开始用C写贪吃蛇(基于链表)

目录 1. 游戏背景 2. 游戏效果演示​编辑​编辑​编辑 3. 实现目标 4. 技术要点 5. 控制台程序 5.1 设置控制台窗口的长宽和名字 5.2 控制台屏幕上的坐标COORD 6.Win32 API 6.1 GetStdHandle 6.2 GetConsoleCursorInfo 6.3 CONSOLE_CURSOR_INFO 6.4 SetConsole…

python监听html click教程

👽发现宝藏 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。【点击进入巨牛的人工智能学习网站】。 Python实现监听HTML点击事件 在Web开发中,经常需要在用户与页面交互时执行一些…

电磁兼容(EMC):静电放电(ESD)抗扰度试验深度解读(七)

目录 1. 第一步 确定电磁环境 2. 第二步 确认设备工作状态 3. 第三步 制定试验计划 4. 间接施加的放电 4.1 水平耦合板 4.2 垂直耦合板 静电抗扰度的试验测试细节对测试结果影响比较大,本文详细介绍静电抗扰度试验的测试程序和注意事项。 1. 第一步 确定电磁…

工作任务管理平台作品集源文件 figma格式UX/UI设计师作品集

交付文件:作品集源文件项目源文件包装样机源文件字体文件 交付格式:figma、psd 作品集文件页数:28页 项目文件页数:12页(Web)12页(App) 以下重点哦,请认真阅读&#…

怡宝母公司冲刺上市:产能未满仍要募资扩产,突击分红25亿元

又一家瓶装水企业冲刺上市。 近日,怡宝母公司华润饮料(控股)有限公司(下称“华润饮料”)递交招股书,准备在港交所主板上市,BofA securities(美银证券)、中银国际、中信证…

nosql数据库 redis

一、介绍 1、redis与mysql的区别: Redis是一种基于键值对的内存数据库,数据存储在内存中,因此读写速度非常快。它支持多种数据结构,如字符串、哈希、列表等。 MySQL是一种关系型数据库,数据以表格的形式组织存储在磁…