编程为什么那么难:从储值卡扣款说起

面向失败编程是编程中最难的事情 。
话说程序员小林的某一天:起床->吃饭->坐地铁->到公司->敲代码->回家->玩游戏->睡觉 。
这一天的另一个版本:起床->吃饭->坐地铁->到公司->突然要 24 小时健康码->进不了公司->坐地铁回去->地铁停运了->上厕所->踩到屎滑倒->摔成脑震荡 。
第二个版本充满意外 , 貌似有些极端 , 但你我天天在新闻上看到类似的事情 , 说明它其实每天都在发生 。
程序也是如此 。
程序员小林给公司开发的某个系统 , 用户量暴涨;三年后公司上市了 , 小林喜迎白富美 。
另一个版本:上线后第二天被 SQL 注入删库了 , 造成大量投诉;小林被老板痛骂一顿后 , 卷铺盖走人了 。
程序的世界充满意外 , 你我的每一行代码几乎都是 bug 。
写出可用的系统很容易 , 但写出健壮的系统很难 。
 
一个”简单“的例子我们通过储值卡消费这个例子来看看如此”简单“的案例到底存在多少让人眼花缭乱的失败场景 。
假设我们给某个加油站开发个储值卡系统 , 用户可以往里面充钱 , 可以用储值卡加油消费 , 类似你在理发店、洗脚店开的那种充值卡 。
我们看看车主加油消费的场景——而且只看这个场景中的”储值卡扣款“这一个结点 。
正常流程(简化版)大致是这样的:

编程为什么那么难:从储值卡扣款说起

文章插图
 
流程很简单 , 加油员加完油后 , 用户掏出手机扫码进入付款页面 , 输入油枪、金额 , 选储值卡支付 , 输完密码后点提交;后端创建订单后调卡服务的扣款接口执行扣款(传入卡号、订单号、金额);卡服务扣款成功后返回告知用户付款成功 。
”这个需求大概要几天开发?“产品经理问小林 。
”五天 。“小林觉得五天绰绰有余 。
”三天吧 , 这周我们就要上线 。“
”那就三天 。“小林觉得其实三天足够——不就一两个接口调用嘛 , 卡服务是现成的 。
于是小林撸起袖子开始敲代码 。进展比预想得要顺利 , 两天就敲完了(多少加了点班) , 一天测试完成 , 第四天就上线了!
某天夜里 , 小林正在撸猫时 , 运营同学打来电话:某车主的卡被莫名其妙扣款了!
事情是这样的:车主鲁某加了 3000 元的油 , 选择用储值卡支付 , 结果系统提示扣款失败 , 于是鲁某换微信付款成功 , 开车走人了 。
蹊跷的是:鲁某十分钟后收到消息说卡扣掉了 3000 元!
明明说支付失败 , 怎么扣了 3000?于是鲁某打电话找油站闹 。
小林赶紧排查日志 , 发现上图中地第 3 步(调卡服务的扣款接口)超时了 , 于是业务系统告知前端扣款失败 。
【编程为什么那么难:从储值卡扣款说起】调卡服务扣款接口超时 , 业务系统能直接返回失败给前端吗?
不能!
因为接口超时并不能说明卡服务那边实际上到底有没有扣成功(有可能卡服务处理成功了 , 但返回的时候网络出问题;也有可能卡系统负载高 , 业务系统等待超时从而断开连接) 。
我们看看上面的异常是怎么发生的:
编程为什么那么难:从储值卡扣款说起

文章插图
 
第四步超时后 , 业务后台直接告知车主支付失败 , 但实际上卡系统仍然在扣款!
那怎么办?告诉车主”请您稍后查看支付结果?“
怎么可能!
一个想法是超时后业务系统调卡服务的查询接口 , 看看这笔订单实际是否支付成功 。
问题是 , 如果查询接口调用也超时呢(卡系统负载高的情况下这个概率很大)?
另外 , 查询接口返回没有扣款成功就能直接告诉用户扣款失败吗?
不能!
因为查询接口查数据库的时候 , 数据库里面没有记录 , 但有可能前面发起的那个扣款逻辑仍然在执行 , 稍后仍然会发生扣款 。
既然怕查询的时候扣款逻辑仍然在执行 , 那我们能不能等一会(比如五分钟)再查结果呢(等那个可能的扣款执行流跑完)?


推荐阅读