你一定要知道的电商业务中幂等场景与技术方案

1,596 阅读6分钟

大家好,本博客致力于分享互联网领域的各种技术干货,欢迎关注我们一起交流一起学习哦!

一、背景

幂等性问题在电商系统里真的是常见的老大难问题,尤其是遇到高并发的场景时。我们需要保证某些操作不会因为重复执行而导致数据不一致,比如重复生成订单、多次发放优惠券等。今天我们就聊聊电商中的几个常见场景,以及如何通过一些设计技巧来保证幂等性

二、电商业务中的幂等性场景

1、提交订单

场景描述: 用户在下单时,可能会因为网络延迟重复点击“提交订单”按钮而导致重复请求。如果没有处理好幂等性,系统可能会生成多个订单,进而影响库存、结算等一系列问题。

解决方案:

签名机制:为了保证幂等性,我们可以给每一次下单请求生成一个唯一的签名值。具体方法是,前端将请求中的关键参数(比如用户 ID、商品 ID、时间戳等)通过一定规则进行签名(如用 MD5、SHA256 等哈希算法),生成唯一的签名值 sign。这个签名值随请求一起发送到服务端。

服务端逻辑:当服务端接收到请求时,首先会根据签名值 sign 查询 Redis,看这个签名值是否已经存在。如果存在,说明订单已经处理过了,直接返回处理结果;如果不存在,才会继续创建订单,并把这个签名值存到 Redis 中,设置一个适当的过期时间,以防止重复请求处理。

防重复提交的幂等设计思路:

  1. 请求发起时,前端对用户请求参数生成签名值
  2. 服务端检查 Redis 看签名值是否已处理,如果已处理则直接返回结果。
  3. 如果未处理,则生成订单并记录签名值,保证只处理一次。

具体代码示例:

复制代码
// 检查签名值是否已存在
if (redis.exists(sign)) {
    return "订单已生成"; // 重复请求,直接返回
} else {
    // 处理订单生成逻辑
    createOrder();
    redis.set(sign, orderId, 10 * 60); // 设置 10 分钟过期时间
    return "订单创建成功";
}
这种方案通过前端生成签名值,后端只认一次请求的方式,可以有效防止重复下单问题。

2、领券与发放权益

场景描述: 用户在活动期间参加领券活动,比如每个用户每天只能领取一张券。然而,由于并发请求或恶意重复请求,可能导致用户领取多张券。我们需要确保每个用户在活动期间只能领取一次指定的优惠券。

解决方案:

记录表设计:创建一个“领券记录表”来记录用户领取券的历史。这个表包括 user_id(用户 ID)、activity_id(活动 ID)、coupon_id(券 ID)、以及 create_time(领取时间)等信息,并通过 user_id + activity_id + coupon_id 设定唯一索引,确保用户在每个活动中只能领取一张券。


CREATE TABLE coupon_records (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    activity_id BIGINT NOT NULL,
    coupon_id BIGINT NOT NULL,
    create_time DATETIME,
    UNIQUE KEY uniq_coupon (user_id, campaign_id, coupon_id)
);

分布式锁的引入:为了防止并发情况下用户多次领取相同的优惠券,可以使用Redis实现用户维度的分布式锁。每次用户发起领券请求时,首先在 Redis 中为该用户加锁,确保该用户的领券操作只能有一个请求进行。

具体实现步骤:

  1. 用户发起领券请求

    • 服务端使用用户 ID 作为锁的 key,使用 Redis 的 SETNXSET if Not eXists)命令为该用户加锁,锁的生存时间设定为一个合理值(如 10 秒)。
  2. 检查领券记录

    • 如果锁获取成功,服务端会查询 coupon_records 表,检查该用户是否已经领取了该优惠券。
    • 如果已经领取,返回“已领取”结果。
  3. 处理领券逻辑

    • 如果用户未领取,则生成领券记录,并返回“领取成功”。
  4. 释放锁

    • 无论领券成功与否,在处理完请求后,服务端都会主动释放 Redis 中的分布式锁。

领券逻辑:

public String claimCoupon(Long userId, Long activityId, Long couponId) {
    String lockKey = "lock:coupon:" + userId;
    boolean isLockAcquired = redis.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

    if (!isLockAcquired) {
        return "请求频繁,请稍后再试"; // 如果无法获取锁,说明用户在短时间内重复请求,直接返回
    }

    try {
        // 检查是否已经领取过该优惠券
        if (couponRecordsExist(userId, activityId, couponId)) {
            return "已领取";
        } else {
            // 创建领券记录
            createCouponRecord(userId, activityId, couponId);
            return "领取成功";
        }
    } finally {
        // 释放分布式锁
        redis.del(lockKey);
    }
}

通过这个分布式锁和唯一索引的设计,可以保证无论用户如何重复请求,都只能领取一张优惠券。

3、订单消费与业务处理

场景描述: 在电商促销活动中,用户消费达到一定次数可能会触发某种奖励。系统需要确保消费记录只会被处理一次,避免因为重复处理导致奖励多发。

解决方案:

订单处理记录表设计:可以设计一个 order_processed 表,记录所有已处理过的订单 ID。表的字段包括 order_id(订单 ID)、user_id(用户 ID)、process_time(处理时间)等,并将 order_id 设为唯一索引,确保每个订单只会被处理一次。

CREATE TABLE order_processed (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    order_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    process_time DATETIME,
    UNIQUE KEY uniq_order (order_id)
);

业务逻辑:每当接收到一笔订单时,系统首先检查 order_processed 表,看这笔订单是否已经处理过。如果已经处理过,则跳过处理逻辑;如果没处理过,则继续执行奖励或其他业务操作。 订单处理逻辑:

// 检查订单是否已经处理过
if (orderProcessed(orderId)) {
    return "订单已处理";
} else {
    // 先记录订单处理状态,防止重复处理
    recordOrderProcessed(orderId, userId);
    
    // 处理订单业务逻辑
    processOrder(orderId);
    
    return "处理成功";
}

通过这种方式,可以确保每个订单的业务处理只会执行一次,避免了重复操作带来的数据问题。

三、总结

幂等性虽然看上去只是防止重复操作,但其实在实际开发中会涉及到很多细节和设计。通过签名机制数据库唯一索引以及订单处理记录表等手段,我们可以有效保证系统的幂等性,避免因为重复请求或处理导致的数据混乱。

希望这些实际的场景和解决方案能帮你在开发中少踩坑,轻松应对幂等性问题!

关注我们一起学习技术吧,坚持相信有输入一定要有输出,希望我们的技术能力越来越强大。