使用幂等性解决重复下单问题

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

从技术上看,这是一个分布式一致性问题;但实际上,技术无法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重新发起下单。

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

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

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

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

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

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

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

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

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

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

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

希望今天你有get到:)。

<think>嗯,用户问的是如何使用Redis防止重复下单。这个问题看起来挺常见的,尤其是在电商或者高并发系统中。我得先理清楚用户的需求。重复下单可能是指同一个用户在短时间内多次提交相同的订单,这会导致库存问题或者重复支付等情况。所以,用户需要一种机制来防止这种情况发生。 首先,我需要回忆一下Redis的特性。Redis是内存数据库,读写速度快,支持原子操作,适合处理高并发场景。常见的防重方法可能有几种,比如使用唯一标识、分布式锁、Token机制或者幂等性处理。但用户这里可能更关注的是如何具体用Redis实现。 接下来,我得想一下具体步骤。假设用户在下单时,可能因为网络延迟或者用户多次点击导致同一请求被多次发送到服务器。这时候,服务器需要判断这个请求是否已经处理过。 可能的解决方案之一是生成一个唯一的请求标识,比如用户ID加商品ID加时间戳之类的组合,然后将这个标识存入Redis,并设置一定的过期时间。每次请求到来时,先检查Redis中是否存在这个标识,如果存在就拒绝处理,否则继续处理。但这种方法需要注意并发情况下的原子性问题,比如多个请求同时检查是否存在这个标识,可能会同时通过检查,导致重复下单。 这时候可能需要用到Redis的setnx命令(SET if Not eXists),或者使用带有过期时间的SET命令,并确保原子性。比如使用SET key value NX EX 过期时间,这样可以在设置键的同时设置过期时间,避免重复下单。这里的key可以是订单的唯一标识,比如用户ID+商品ID或者其他业务相关的唯一组合。 另外,还可以考虑使用分布式锁。比如,当用户开始下单时,先获取一个锁,锁住用户ID或者订单相关资源,处理完后再释放锁。这样可以防止同一个用户的并发请求。Redis的RedLock算法或者简单的setnx都可以用来实现分布式锁,但需要注意锁的超时时间和正确释放,避免死锁或者锁被其他进程误删。 还有一种方法是利用Redis的原子递增操作。例如,为每个用户和商品生成一个key,每次下单前先检查这个key的值是否超过阈值,或者通过INCR命令来计数,如果计数超过1则拒绝。但这种方法可能不如设置唯一标识直接有效。 另外,幂等性处理也是一个思路。服务端在接收到请求时,先生成一个唯一的token返回给客户端,客户端下单时必须携带这个token。服务器在处理请求时,先检查token是否存在,存在则处理并删除token,否则拒绝。这种方法需要客户端配合,可能不适合所有场景。 不过,用户的问题更偏向于如何用Redis防止重复下单,所以可能最适合的是使用唯一key加过期时间,结合setnx命令。例如,当用户提交订单时,生成一个唯一的key,如order:user123:product456,然后尝试在Redis中设置这个key,如果成功(即之前不存在),则处理订单,并设置一个较短的过期时间(比如30秒)。如果设置失败,说明已经有相同的订单在处理中,直接返回重复提交的错误。 需要注意的是,这个key的生成规则必须确保唯一性,比如结合用户ID、商品ID、时间戳(比如分钟级,避免同一用户同一商品在一分钟内重复下单),或者使用更精确的业务字段组合。同时,过期时间的设置要根据业务需求来定,太短可能无法有效防止重复,太长可能影响用户体验。 可能还要考虑不同客户端的重复提交,比如用户在手机和电脑同时提交,这时候可能需要更复杂的机制,但基本的防重复下单还是可以用上述方法。 总结来说,步骤大概是:生成唯一请求标识,用Redis的setnx或SET NX EX命令存入,处理订单前检查是否存在,存在则拒绝,否则继续处理,并设置合适的过期时间。同时要考虑并发情况下的原子性操作,确保即使多个请求同时到达,只有一个能成功设置key,从而防止重复下单。</think>使用 Redis 防止重复下单的核心思路是通过**唯一标识+原子性操作**拦截重复请求,以下是具体实现步骤和注意事项: --- ### **1. 核心方案:基于唯一请求标识** #### **步骤说明** 1. **生成唯一标识** 将用户ID、商品ID、时间戳(如秒级/分钟级)等业务参数拼接,或通过哈希(如MD5)生成唯一标识,例如: `order_lock:user123:product456` 或 `order_lock:MD5(用户ID+商品ID+下单时间戳)`。 2. **Redis 写入校验** 使用 `SET key value NX EX seconds` 命令(原子性操作): - **NX**:仅当 Key 不存在时才能写入,避免并发覆盖。 - **EX**:设置自动过期时间(根据业务设定,如 10~30 秒)。 ```bash SET order_lock:user123:product456 1 NX EX 30 ``` 3. **校验结果处理** - 如果写入成功 → 允许下单,继续后续业务流程。 - 如果写入失败 → 直接返回 "请勿重复提交" 提示。 4. **过期时间结束后** Redis 自动删除 Key,允许用户再次正常下单。 --- ### **2. 进阶优化:防止极端并发场景** #### **场景问题** 若用户疯狂点击导致同一毫秒内大量并发请求,可能绕过时间戳拼接的唯一性。可补充以下方案: - **方案一:分布式锁(更严格)** 下单前对用户ID加锁(如 `lock:user123`),通过 Redis 的 `SETNX` 或 `Redisson` 库实现锁竞争,确保同一用户串行处理请求。 - **方案二:Token 机制(需前端配合)** 1. 前端下单前先请求服务端获取一次性 Token(存 Redis 并设置过期时间)。 2. 提交订单时携带 Token,服务端验证 Token 存在后删除,防止重复使用。 --- ### **3. 关键注意事项** 1. **原子性操作** 必须使用 `SET NX EX` 或 Lua 脚本确保“判断+写入”的原子性,避免高并发下多个请求同时通过校验。 2. **合理设置过期时间** 根据业务容忍度设置,例如: - 短时效(10~30秒):拦截短时间内的重复点击。 - 长时效(分钟级):防止恶意用户周期性重试。 3. **异常补偿机制** 若下单失败(如支付超时),需手动删除 Redis Key 或设置更短的过期时间,避免阻塞用户正常重试。 --- ### **4. 代码示例(Python)** ```python import redis import hashlib def generate_order_key(user_id, product_id): # 示例:生成唯一Key(可结合时间戳) return f"order_lock:{user_id}:{product_id}" def prevent_duplicate_order(user_id, product_id): r = redis.Redis(host='localhost', port=6379) key = generate_order_key(user_id, product_id) # 原子性写入,NX=True(不存在才写入),EX=30秒 success = r.set(key, "1", ex=30, nx=True) if not success: return {"code": 400, "msg": "检测到重复下单,请稍后再试"} else: # 正常处理订单逻辑... return {"code": 200, "msg": "下单成功"} ``` --- ### **总结** 通过 Redis 的原子性操作和唯一Key机制,能高效拦截 99% 的重复下单场景。结合分布式锁或 Token 机制可进一步优化极端情况,最终根据业务需求平衡安全性与用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值