大家好,本博客致力于分享互联网领域的各种技术干货,欢迎关注我们一起交流一起学习哦!
一、背景
幂等性问题在电商系统里真的是常见的老大难问题,尤其是遇到高并发的场景时。我们需要保证某些操作不会因为重复执行而导致数据不一致,比如重复生成订单、多次发放优惠券等。今天我们就聊聊电商中的几个常见场景,以及如何通过一些设计技巧来保证幂等性。
二、电商业务中的幂等性场景
1、提交订单
场景描述:
用户在下单时,可能会因为网络延迟或重复点击“提交订单”按钮而导致重复请求。如果没有处理好幂等性,系统可能会生成多个订单,进而影响库存、结算等一系列问题。
解决方案:
签名机制:为了保证幂等性,我们可以给每一次下单请求生成一个唯一的签名值。具体方法是,前端将请求中的关键参数(比如用户 ID、商品 ID、时间戳等)通过一定规则进行签名(如用 MD5、SHA256 等哈希算法),生成唯一的签名值 sign。这个签名值随请求一起发送到服务端。
服务端逻辑:当服务端接收到请求时,首先会根据签名值 sign 查询 Redis,看这个签名值是否已经存在。如果存在,说明订单已经处理过了,直接返回处理结果;如果不存在,才会继续创建订单,并把这个签名值存到 Redis 中,设置一个适当的过期时间,以防止重复请求处理。
防重复提交的幂等设计思路:
- 请求发起时,前端对用户请求参数生成
签名值。 - 服务端检查 Redis 看签名值是否已处理,如果已处理则直接返回结果。
- 如果未处理,则生成订单并记录签名值,保证只处理一次。
具体代码示例:
复制代码
// 检查签名值是否已存在
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 中为该用户加锁,确保该用户的领券操作只能有一个请求进行。
具体实现步骤:
-
用户发起领券请求:
- 服务端使用用户 ID 作为锁的 key,使用 Redis 的
SETNX(SET if Not eXists)命令为该用户加锁,锁的生存时间设定为一个合理值(如 10 秒)。
- 服务端使用用户 ID 作为锁的 key,使用 Redis 的
-
检查领券记录:
- 如果锁获取成功,服务端会查询
coupon_records表,检查该用户是否已经领取了该优惠券。 - 如果已经领取,返回“已领取”结果。
- 如果锁获取成功,服务端会查询
-
处理领券逻辑:
- 如果用户未领取,则生成领券记录,并返回“领取成功”。
-
释放锁:
- 无论领券成功与否,在处理完请求后,服务端都会主动释放 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 "处理成功";
}
通过这种方式,可以确保每个订单的业务处理只会执行一次,避免了重复操作带来的数据问题。
三、总结
幂等性虽然看上去只是防止重复操作,但其实在实际开发中会涉及到很多细节和设计。通过签名机制、数据库唯一索引以及订单处理记录表等手段,我们可以有效保证系统的幂等性,避免因为重复请求或处理导致的数据混乱。
希望这些实际的场景和解决方案能帮你在开发中少踩坑,轻松应对幂等性问题!
关注我们一起学习技术吧,坚持相信有输入一定要有输出,希望我们的技术能力越来越强大。