当前位置: 首页 > 科技观察

SQL设计模式-关系型数据库的幂等处理

时间:2023-03-13 07:34:43 科技观察

IT中的很多术语,正向解释很难,逆向描述比较容易理解。幂等处理就是这一类。举出数据处理中非幂等性的两种常见场景:1、创建订单时,偶尔会因为网络抖动、失智、掉线等因素导致客户端与服务端通信不畅。例如,客户端发起请求后,在约定时间内(一般为30秒)没有得到服务端的反馈,导致重复请求创建订单。事实上,之前看似失败的订单已经创建成功,最终导致创建了两个甚至多个相同的订单2.重复扣款,扣库存。这是最不能容忍的。如前所述,客户会不断发起扣款和存货的请求,从而导致账目混乱。由此可见,做好程序的幂等处理是非常重要的!很多教科书一般会说幂等处理就是最后返回一致结果的程序处理。也就是说,并不完美。幂等处理不仅对结果有约束,对处理的负面影响也有约束。下面看一下关系型数据库中DML的幂等处理。在库存管理软件中,对同一批次的商品进行增删改查可能会产生负面影响。比如在某苹果专卖店的仓库管理软件中,该店某一天客流量非常大,盘点操作比平时频繁很多。因此,它给库存管理带来了风险。例如,某结算终端经常因访客过多而掉线、超时。小王好不容易卖出了两件,结果结账不成功。连续操作四五次无果后,小王打电话给店长重启电脑。重启后结算成功,但库存为0。店长跑到仓库一看,10部iPhone13躺在那里。为什么库存为0?这是由非幂等处理引起的。客户端发起交易后,网络拥塞,结账请求一直没有发送成功。电脑重启后,之前的订单连续重复发送了10次,但是库存都被扣掉了。看看库存表的设计:createtableProductInventory(ProductLotIdINT,ProductNameVARCHAR(200),ProductInventoryVolumeINT)iPhone13库存是这样的:ProductLotIdProductNameProductInventoryVolumeA0001iPhone1310更新程序也很简单:UPDATEProductInventorySETProductWIdLventoryInventoryVolume=VoProduct='A0001'可以看出是一个连续的事务请求,将库存清零。于是,第一个幂等处理方法来了——UUIDUniversalUniqueIdentifier:CREATETABLEProductSalesTransactionAudit(AuditIdBIGINT,RequestUUIDUniqueIdentifier,RequestCompletedBIT)在每个请求中,添加一个RequestUUID(UniversallyUniqueIdentifier,通用唯一标识符字符,Java/C#/Python等编程语言都有实现UUID的库)在数据库端维护一个表ProductSalesTransactionAudit,如果收到一个请求数据库,先去表中查看是否它存在。如果存在且RequestCompleted为1,则说明请求已经被数据库正确处理,可以跳过这个处理,将RequestCompleted返回给客户端;如果没有,则在该表中插入一行,并将数据库的处理结果更新为RequestCompleted。这样一个可行的幂等处理就完成了。但并不完美,因为表的数据量会大幅度增加,导致性能变慢。因此,需要寻找下一个幂等的处理方案。我们再来看这个例子,还是以苹果商店为例。有一天,仓库里剩下10部iPhone。13、小王和小黄同时卖出了2部iPhone,理论上还剩6部。按照正常操作,小王和小黄在操作盘点时同时看到10只股票,每人减去2只股票,剩下8只股票。我认为库存没有错。createtableProductInventory(ProductLotIdINT,ProductNameVARCHAR(200),ProductInventoryVolumeINT)当小王和小黄同时查询iPhone的库存时,是这样的:ProductLotIdProductNameProductInventoryVolumeA0001iPhone1310抢到之后,他们通过各自的本地计算(在网络或手持设备上)变成这样:ProductLotIdProductNameProductInventoryVolumeA0001iPhone138当他们上传本地数据时,无论谁先,数据库中iPhone13的最终库存都变成8.但事实上,错的离谱,店长想骂她!那么我们平时在设计系统的时候,应该如何处理这种意外的错误,这就涉及到事务管理的技巧了。一种乐观的方法是在库存表中添加一列来标识该行的版本。该行数据更新时,先比较version列,相同则更新,不同则报“您修改的数据已被他人先更新,请确认并再次保存”提示,最后显示的识别栏会自动更新。接下来实现上面的版本控制方法:createtableProductInventory(ProductLotIdINT,ProductNameVARCHAR(200),ProductInventoryVolumeINT,ProductLotTStimestamp)原始库存如下:变成这样:ProductLotIdProductNameProductInventoryVolumeProductLotTSA0001iPhone1382022050114364700001小王上传数据时,程序会同时以A0001+2022050114364700001作为更新条件,先自动更新ProductInventoryVolume为8。待更新的对象变成了2022050114364700002,等待小黄再次更新,程序也同时使用A0001+2022050114364700001作为更新条件。发现ProductLotTS变了,说明读取数据后,是别人先更新了。这时候小黄就会更新库存失败。他必须在操作前重新读取数据。只要一次更新成功,ProductLotTS就会改变。即使再次发送同样的请求,也会因为ProductLotTS不匹配而失败!这是第二个幂等的handler,它不仅做了防重处理,还节省了Go对一张表的维护成本。