如何避免下重复订单

电子交易的一个很基本的问题,就是避免用户下重复订单。用户明明想买一次,结果一看下了两个单。如果没有及时发现,就会带来额外的物流成本和扯皮。对商家的信誉也不好看。

从技术上看,这是一个分布式一致性问题;但实际上,技术无法100%解决这类问题,得结合多种手段综合处理。这里就来说道说道。

为啥会下重了呢?

  • 原因1:客户端bug

比如下单的按键在点按之后,在没有收到服务器请求之前,按键的状态没有设为已禁用状态,还可以被按。又或者,在触摸屏下,用户手指的点按可能被手机操作系统识别为多次点击。

嗯,谁能保证客户端不偶尔出个什么bug 呢。

  • 原因2: 超时

用户的设备与服务器之间可能是不稳定的网路。这样一个下单请求过去,返回不一定回得来。超时最大的问题是: 从用户的角度,他无法确定下单的请求是还没到服务器,还是已经到了服务器但是返回丢失了。——用户无法区分到底这个单下了还是没下

这样在等待一个超时后,UI可能会提示用户下单超时,请重复再试。

下单超时
  • 原因3: 用户的App闪退/人工强退,之后重新打开重新下单

也许可以使用一些技术手段避免用户下重单,但是心急的用户可能会重启流程/重启App/重启手机。在这种强制的手段下,任何技术手段都会失效——用户压根就不让你的技术执行,你怎么玩?

在这些条件下,如何避免用户多下了一笔订单呢?

用幂等防止重复订单

在技术方面,这是一个分布式一致性的问题,即客户端和服务器端对某个订单是否成功/失败达成一致。防止重单的关键是使用一个由客户端生成的,可用于避免重复的key,俗称dedup key(deduplicate key之意)。这个key可以用任意可以保证全局唯一性的方式生成,比如uuid。客户端和服务器需要使用这个dedup key作为串联条件,一起解决去重问题。

客户端的流程

客户端需要实现这样一个下单界面。用户点击【确认下单】时,应该产生一个独一无二的dedup key,连定订单数据发送给服务器端。在服务器返回之前,该界面应该一直等待,直到服务器响应成功/失败或者超时发生(比如15秒后,收不到服务器响应)。如果超时发生,应该向用户提示是否重试下单或者退出该界面。当用户点击【重试】时,应该用刚刚生成的dedup key来再次发送下单请求——如果用户一直不退出这个流程,每次用户点击重试,都应该用这个dedup key来重试下单,直到服务器正常返回,或者用户放弃返回。

下单的客户端流程

后端数据表设计

后端在订单数据表中,需要增加dedup_key这列,并设置唯一约束

create table order(
  # ...
  dedup_key varchar(60) not null comment 'key to pretend order duplication',
  # ...
  unique uniq_dedup_key(dedup_key)
);

下单的实现

在实现下单逻辑时,基于该dedup_key实现一个"create-or-get"语义的下单接口——简单说就是

如果带有指定dedup_key的订单已经存在,则直接返回;否则,用该dedup_key下单。

用伪代码表示大概是:

@Transactional
Order createOrder(Integer userId, String prodCode, Decimal amount, String dedupKey) {
  try {
    String orderId = createOrder(userId, prodCode, amount, deupKey); // insert a new order
    Order order = getOrderById(orderId); // read order from db
    order.setDuplicated(false);
    return order;
  } catch(UniqueKeyViolationException e) {
    // if duplicated order has existed
    Order order = getOrderByDedupKey(dedupKey);
    order.setDuplicated(true);
    return order;
  } catch (Exception e) {
    // hanlde other errors and rollback transaction ...
  }
}

这时,这段下单代码总是能返回一个订单(除非发生一些DB挂了之类的错误),要么是新创建的,要么就是一个已经存在的单。注意,最好在订单里增加一个属性(比如例子中用“duplicated”)来表示这个订单是这次新生成的,还是因为幂等而直接返回的。这样前端可以有针对性的对这两种情况提示不同的文案。

技术搞定幂等就足够了吗?

上面的流程没有考虑一种情况,就是用户中途强制退出客户端,或者直接点击【返回】回到产品页,重新走下单流程。这个时候客户端就无法判断用户到底是想重新下单,还是想第二次下单。此时,可以从产品设计上考虑一下。

比如,在客户端缓存一个表,记录所有没有确认结果的订单。

产品代码 产品数量 金额 dedup key
未确认订单1 AAA 1 1000 xxx-yyy-zzz
未确认订单2 BBB 2 500.00 Aaa-bbb-ccc
...

通过这个表,我们可以一下用户的意图。比如,如果用户重新提交了一笔订单,其产品代码、金额与表中记录的某条完全一致,就可以提示一下用户:

提示一下用户是不是下重了

如果用户想重试,可以继续用表中对应记录的dedup key重新发起下单。

这样不是绝对准确的,仅仅是尽量的减少用户误操作的可能性。当然,在产品设计上可以能出于用户交互简化,不一定真的会这样做。这就需要其他机制来配合,比如“通知”。

通知

一旦服务器下单成功,可以通过某种通知机制(如APNS、Websocket)主动将订单推送至客户端,强行让客户端重新拉取最新的订单信息,并配合“未确认订单”表,以通知Badge/弹框等方式提示用户刚刚一笔状态未知的订单成功/失败了。

另外一种手段就是,服务器端实时扫描用户的下单数据,一旦发现可能的重单,就立刻通知客服主动联系用户,及时处理问题。

如果还拦不住……

经过层层阻拦,可能还是会有用户误操作,直到收到两份商品才发现下重了。此时就得依靠运营/客服的支持了。提供用户申诉的手段,让用户提出哪些订单是重复的,并且由销售系统店家、商品提供者和买家三方共同根据用户操作的记录来协商如何处理。我们需要让技术帮助让这种人工处理的几率尽量小。因为每次处理都会耗费较大的人工成本,和一些运营费用(比如赔款、小礼品等等)。

这么麻烦,有必要吗?

这要分业务场景,对于很多电商来讲可能不是必要的。因为从用户下单到订单被审核处理进入到发货阶段需要一定的时间(可能是半小时~1小时),并且一定是支付成功后才会开始进行下一步流程。在这个时间段,用户大概率能从网络错误中恢复过来,自行区分是否下重了。配合客服主动提示,会极大的降低出问题的概率。

但是对于理财服务来说,这种去重就非常必要了。因为

  • “下单+支付”。用户购买理财往往是“下单+支付”一起执行,不可以单独下单/单独支付
  • 用户的入金可能很大。例如数万,数十万
  • 准确性丢失。如果一旦下重了,有可能影响用户的投资资金配置的准确性。
  • 撤销难。部分理财产品存在下单不可撤销的问题;或者即便撤销,资金也无法立刻回款。等到回款,可能这个购入机会就错过去了。例如对于基金交易,错过1个交易日,价格就会发生变动。

基于这些特性,在理财产品中,就要竭尽全力的去重。

结论

以上所讲是处理重复订单问题的一般方法。你可以注意到,无论多么好的技术,也不可能100%的拦截所有的可能性,必须依靠技术+产品设计+运营支持的综合手段才能解决这类问题。

另外,本文还没涉及到关于订单支付(支付也可能重复哦)带来的进一步的复杂性,也没有讨论在高并发情况下的性能优化,仅仅讨论下单本身的问题。所以可以想象一下现实中的交易业务比这里的说的要复杂得多。

本文介绍的原理也不仅仅适用于防止下重复订单,而是可以应用到任何需要“创建一个不应该重复资源”的场景,比如“向用户发一条通知”,“触发一次不能重复的批处理任务“……

希望今天你有get到:)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容