由浅入深的介绍扣减业务中的一些高并发构建方案(中)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 这一讲我将由浅入深的介绍如何基于缓存来实现单机万级这些并发扣减目标。

前言

大家好,我是路由器没有路

在上一讲的实现方案里,我们讨论采用数据库的扣减实现方案,如果以常规的机器或者 Docker 来进行评估,此方案将来实现单机级的 TPS

之所以介绍,是要告诉你架构是面向业务功能、成本、实现难度、时间等因素的取舍,而不是绝对的追求高性能、高并发及高可用等非功能性指标

另外。在上一讲里介绍的扣减业务的技术实现方案有一定的需求基础介绍了。因此今天我们讲解的方案将直接复用以上信息,不再赘述。

有忘记的同学,可以查看这篇文章:《由浅入深的介绍扣减业务中的一些高并发构建方案(上)》。

另外,大家可以关注公众号:Go键盘侠,在上面会第一时间更新编程技术、开发经验的分享文章,回复“Redis”可免费领取《Redis 核心技术与实战》资料。

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

引入缓存方案的原因

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

在学习下面的内容之前,我们先来回顾一下事物本质上的四个特点,ACID,分别是原子性、一致性、隔离性及持久性

我们知道扣减有一个要求,就当一个 SKU 购买的数量不够时,整个批量扣减就要回滚,因此我们需要使用类似循环的方式对每一个扣减的返回值进行检查。

另外一个原因是,当多个用户一个 SKU 的性能也并不乐观,因为当出现高并发扣减或者并发扣减同一个 SKU 时,事物的隔离性会导致加锁、等待释放锁情况出现。

首先你要知道,扣减只需要保证原子性即可,并不需要数据库提供的 ACID。另外,在此基础上该如何提升性能?

在不改变机器配置的情况下,把传统的 SQL 类数据库替换为性能更好的 NoSQL 类数据存储试试。是不是有一个性能又好,同时又能够满足扣减多个 SQL 具有原子性的 NoSQL 数据库了?答案显然是可以的。

Redis 做缓存

Redis 作为最近几年非常流行的 NoSQL 数据库,它的原始版本或者改造版本基本上已经被国内所有互联网公司或者云厂商所采用。

不管是微博暴敛事件的流量应对,还是电商的大促流量处理,它的踪影无处不在。

它的高性能上的能力是首屈一指,另外因为是开源软件,且架构简单,布置在普通 Docker 即可,成本非常低。此外,Redis 采用了单线程的事件模型保障我们对于原子性的要求

对于单线程的事件模型,简单的比喻就是说,当我们多个客户端给同时发送命令后,会按接收到的顺序进行串行的执行。对已经接收而未能执行的命令,只能排队等待。

基于此特点,当我们的扣减请求的 Redis 执行时,也就是原子性的。此特性刚好符合我们对扣减原子性的要求。

加入缓存方案实现

在确定了使用缓存来完成扣减的高性能后,这里我们结合扣减服务的整体架构图来进行进一步的分析,如下图所示:

扣减服务加入缓存.png

上图中的扣减服务和上个案例的扣减服务一样,都提供了三个在线接口,但此时扣减服务依赖的是 Redis 缓存,而不是数据库了。

我们顺着上一讲的思路,继续以库存为场景,讲解扣减服务的实现。

缓存中存储的信息和上一展中的数据库表结构基本类似,包含当前商品和剩余的库存数量和当次的库减流水。

这里要注意两点:

  • 因为扣减全部依赖于缓存,而不依赖于数据库,所有存储与的数据均不设置过期并全量存储
  • 其次是以 KV 结构为主,伴随 hash、set 等结构,与 MySQL 表+行为主的结构有一定的差异

Redis 中的库存数量结构大致如下:

redis 库存剩余数量结构.png

当我们存储的 SKU 有上百万千万级别时,此方式可以其他的降低存储空间,从而降低成本,毕竟内存是比较昂贵的。

对于 Redis 中存储的流水表采用哈希结构,即 key+hashField+hashValue ,结构大概如下:

redis 流水表结构.png

我们在上一讲里介绍过,扣减接口支持一次扣减多个 SKU 数量。

查询 Redis 的命令文档时,你会发现,首先对于哈希结构不支持多个 key 的批量操作,其次对于不同数据结构间不支批量操作

如果对于多个 SKU 不支批量操作,我们就需要按个 SKU 的命令扣减必须要发起多次对命令才可完成。这样,上面提到的单线程来保证扣件的原则性,此时则满足不了了。

使用 Lua 脚本实现原子批量操作

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

Lua 是一个类似 JavaScript 的语言,它可以完成 Redis 已有命令不支持的功能。

Lua 脚本编写完之后将此脚本上传至 Redis 服务器,服务器会返回一个标识码代替此脚本。在实际执行具体请求时,将数据和此标识码发送出即可。

Redis 会和执行普通命令一样,采用单线程执行此脚本和对应数据。

当用户调用扣减接口时,将扣减 SKU 及对应数量脚本标识传递到 Redis 即可。

所有的扣减判断逻辑在 Redis 中的 Lua 脚本中执行,Lua 脚本执行完成之后返回是否成功给客户端。

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

lua 脚本实现扣减.png

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

最后,在存缓存的架构中还有一个运营后台,它直接连接到数据库,是运营和商家修改库存的入口。

当商品补齐了新的货物时,商家在运营后台将此 SKU 库存数量加回,同时运营后台的实现需要将此数量同步的增加至 Redis。

因为当前方案的所有实际扣减都在 Redis 中。

至此,新引入缓存扣减的基本方案已经介绍结束了。目前这个方案已经可以满足成单机万级的扣减了,下面我们再来看看如何应对异常情况。

异常场景分析

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

Redis 宕机的场景

异常场景描述

如果 Redis 宕机时,Redis 中的 Lua 脚本执行到了扣减逻辑,并做了实际的扣减,则出现数据丢失的情况。

因为 Redis 没有事务的保证,已经扣减的数量不会回滚,宕机导致扣减服务给客户返回扣减失败,但实际上已经扣减了部分数据,并刷新了磁盘。

解决方案

当此 Redis 故障处理完成,再次启动后。部分库存数量已经丢失了,为了解决此问题,可以使用数据库中的数据进行校准

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

Redis 扣减成功后,异步刷新数据库失败

异常场景描述

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

Redis 中的数据是准的,但数据库中的库存数据是多的。

解决方案

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

缓存实现方案升级

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

在前面我们介绍过,扣减服务不仅包含扣减接口,还包含数量查询接口。

需优化的点

  • 查询接口的量级小,比写接口至少是十倍以上,即使使用了缓存进行卡掉,但读写都请求了同一个 Redis,将会导致扣减请求被读影响。

  • 其次,运营在后台进行操作,增加或者修改库存时,是在修改完数据库之后,在代码中异步修改刷新。

优化改造方案

改造方案一

增加一个从节点,在扣件服务里,根据请求类型路由到不同的 Redis 节点。

使用储存分离的好处是:不用太多的数据同步开发,直接使用 Redis 同步方案,成本低,开发量小。

改造方案二

第二个是运营后台修改数据库数量后,同步至 Redis 的逻辑,使用 binlog 进行处理。

关于如何接入和使用 binlog,你可以参见《Canal 中间件同步 MySql 数据到 ElasticSearch》的内容,同步到 Redis 的原理是类似的。

方案升级后实现的结构

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

升级缓存实现方案.png

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

场景和适用性分析

优点

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

缺点

除了优点之外,存款存的方案同样存在一些缺点及其他一些缓存实现,为了高性能,并没有实现数据库的 ACID 特性。

导致在极端情况下可能会出现数据丢失,进而产生少卖。

另外,为了保证不出现少卖存款存的方案,需要做很多的对账异常处理的设计细则,复杂度会大幅增加。

适用场景

对于纯缓存的扣减的优缺点有了一定了解后,可以发现纯缓存在抗并发流量时效果非常显著。

因此它较适合应用于高并发、大流量的互联网场景。

但在极端情况下可能会出现一些数据的丢失,因此它优先适合对于数据精度不是特别苛刻的场景,比如用户购买限制等。

但如果上述的异常场景都降级方案应对,保证最终一致性,它也是可以应用在库存扣减、积分扣减的场景的。

在我所经历很了解的实战中,是有很多公司将此方案应用在非常精准的场景的。

总结

在上一讲《由浅入深的介绍扣减业务中的一些高并发构建方案(上)》中的数据库方案无法满足量级要求时。

本讲介绍了加入缓存的扩展方案,着重讲解了为什么缓存可以满足扩建的功能性要求。

对于分析的过程,希望你能够理解并应用,而不是关注最终提出的方案。

作为一名优秀的开发人员,你要知道,架构图是一个最终态,是静止的,它并不能 100%直接应用到你所面对的场景,而复习思路是可以复制和模仿的。

其次,本讲也分析了存款存方案存在一些异常场景,在实践中,正常流程是简单的还异常的,流程的思考与处理十分的复杂与繁琐,同时也最能体现技术性。

最后希望大家都有所收获。


思考题:

如果此时是一个集群,而不是个单独实例,又该如何演化和优化此方案?

可以把你的想法、思路或者总结写在评论区,我们一起交流、讨论。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
存储 缓存 安全
高并发内存池实战:用C++构建高性能服务器(下)
高并发内存池实战:用C++构建高性能服务器
高并发内存池实战:用C++构建高性能服务器(下)
|
7月前
|
负载均衡 前端开发 算法
聊聊高并发应用中电商秒杀场景的方案实现
聊聊高并发应用中电商秒杀场景的方案实现
305 0
|
20天前
|
消息中间件 架构师 数据库
本地消息表事务:10Wqps 高并发分布式事务的 终极方案,大厂架构师的 必备方案
45岁资深架构师尼恩分享了一篇关于分布式事务的文章,详细解析了如何在10Wqps高并发场景下实现分布式事务。文章从传统单体架构到微服务架构下分布式事务的需求背景出发,介绍了Seata这一开源分布式事务解决方案及其AT和TCC两种模式。随后,文章深入探讨了经典ebay本地消息表方案,以及如何使用RocketMQ消息队列替代数据库表来提高性能和可靠性。尼恩还分享了如何结合延迟消息进行事务数据的定时对账,确保最终一致性。最后,尼恩强调了高端面试中需要准备“高大上”的答案,并提供了多个技术领域的深度学习资料,帮助读者提升技术水平,顺利通过面试。
本地消息表事务:10Wqps 高并发分布式事务的 终极方案,大厂架构师的 必备方案
|
1月前
|
缓存 关系型数据库 MySQL
高并发架构系列:数据库主从同步的 3 种方案
本文详解高并发场景下数据库主从同步的三种解决方案:数据主从同步、数据库半同步复制、数据库中间件同步和缓存记录写key同步,旨在帮助解决数据一致性问题。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
高并发架构系列:数据库主从同步的 3 种方案
|
6月前
|
消息中间件 数据挖掘 程序员
【建议收藏】高并发下的分布式事务:如何选择最优方案?
本文介绍了分布式事务的三种常见解决方案。在分布式系统中,事务处理变得复杂,需确保ACID特性。TCC(Try-Confirm-Cancel)方案适用于严格资金要求的场景,如银行转账,通过预留、确认和取消步骤确保一致性。可靠消息最终一致性方案适合一致性要求较低的场景,如电商积分处理,通过消息中间件实现最终一致性。最大努力通知方案则用于允许不一致的场景,如数据分析,通过重复通知尽可能达成一致性。选择合适的方案取决于具体应用场景。
182 5
|
4月前
|
消息中间件 负载均衡 应用服务中间件
高并发环境下的Nginx整合方案
【8月更文挑战第20天】在高并发环境下,整合Nginx代理服务器、静态文件服务器、Tomcat集群、Mycat数据库读写分离和消息队列,可以构建一个强大、灵活且可扩展的Web服务架构。
55 1
|
6月前
|
canal 缓存 关系型数据库
高并发场景下,6种方案,保证缓存和数据库的最终一致性!
在解决缓存一致性的过程中,有多种途径可以保证缓存的最终一致性,应该根据场景来设计合适的方案,读多写少的场景下,可以选择采用“Cache-Aside结合消费数据库日志做补偿”的方案,写多的场景下,可以选择采用“Write-Through结合分布式锁”的方案,写多的极端场景下,可以选择采用“Write-Behind”的方案。
1387 0
|
7月前
|
缓存 监控 测试技术
ERP系统对接方案与API接口封装系列(高并发)
企业资源规划(ERP)系统是现代企业管理的核心,它集成了企业内部的各个部门和业务流程。为了实现ERP系统与其他外部系统或应用程序之间的数据交换和协作,需要对接方案。API(应用程序编程接口)是实现系统对接的常用方法之一。
|
7月前
|
前端开发 Java API
构建异步高并发服务器:Netty与Spring Boot的完美结合
构建异步高并发服务器:Netty与Spring Boot的完美结合
|
存储 缓存 Linux
高并发内存池实战:用C++构建高性能服务器(上)
高并发内存池实战:用C++构建高性能服务器