防抖、幂等和防超卖

ops/2024/12/26 2:29:38/

防抖和幂等

接口防抖(Debounce)和幂等是两个不同的概念,但它们确实在某些场景下可以达到类似的效果,都旨在避免多次重复操作造成的问题。

防抖的主要目的是控制高频操作的触发,确保在一定时间间隔内只执行一次请求。它通常在用户操作层面使用,比如前端用户频繁点击按钮、表单提交等。

防抖的典型场景

  • 用户快速点击“提交订单”按钮多次。
  • 搜索框中连续输入时,避免每次按键都触发请求。
  • 前端分页加载时避免滚动过快触发多次数据请求。

实现方式
前端通过定时器或后台逻辑限制在短时间内重复调用接口。

后端防抖如何实现
如果后端需要实现“防抖”,可以采用以下方式:

1. 幂等性控制
为每次请求生成一个唯一标识(幂等键),服务端通过幂等性机制确保同一标识的请求只处理一次。
示例: 用户点击“提交订单”:

  • 第一次请求正常创建订单。
  • 后续相同的请求直接返回订单信息,不再重复处理。

2. 请求频率限制
限制某个用户或某个接口的调用频率,可以通过以下方式实现:

  • 使用 Redis 的 计数器。
  • 使用分布式限流工具(如 Sentinel、RateLimiter)。

示例: 同一用户每秒只能调用接口一次:

String userKey = "user:" + userId + ":rate_limit";
Long count = redisTemplate.opsForValue().increment(userKey, 1);
if (count == 1) {redisTemplate.expire(userKey, 1, TimeUnit.SECONDS); // 设置 1 秒过期
} else if (count > 1) {throw new TooManyRequestsException("请求过于频繁,请稍后重试");
}
// 执行业务逻辑

3. 状态校验
对于需要防抖的操作,可以通过记录操作状态来避免重复处理。
示例: 支付接口可以通过订单状态来防止重复支付:
如果订单状态为“已支付”,直接返回结果。
如果订单状态为“待支付”,则进行支付操作。

幂等的核心是确保无论一个接口被调用多少次,其结果都是一致的,主要在后端实现。即便请求重复到达,系统也能正确处理并返回相同结果。

4. 分布式锁
在高并发环境下,通过分布式锁可以保证同一时间内只处理一次请求。
示例: 订单支付场景:

String lockKey = "order:lock:" + orderId;
boolean isLocked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.SECONDS);
if (!isLocked) {throw new ConcurrentOperationException("请求处理中,请稍后重试");
}
// 执行业务逻辑
redisTemplate.delete(lockKey);

幂等的典型场景

  • 重复点击“支付”按钮时,保证订单只支付一次。
  • 消息队列的重复消费。
  • 分布式系统中接口重试机制。

实现方式

  1. 使用唯一标识(幂等键)
    客户端请求时生成一个全局唯一标识(例如 UUID 或业务上的唯一键,如订单号),服务器通过幂等键确保同一请求只被处理一次。

实现步骤

  1. 客户端生成唯一标识:每个请求附带一个唯一的幂等标识(如 requestId)。
  2. 服务端检查幂等标识:
    • 请求到达时,检查数据库、Redis 或缓存中是否已经存在此标识。
    • 如果存在,直接返回已有结果;否则,继续处理请求并保存标识和结果。

示例代码
使用 Redis 来保存幂等键:

String requestId = "unique-id-from-client";
if (redisTemplate.hasKey(requestId)) {return "Request already processed";
} else {// 执行业务逻辑redisTemplate.opsForValue().set(requestId, "processed", 10, TimeUnit.MINUTES); // 设置过期时间return "Request processed successfully";
}
  1. 防重操作
    利用数据库的唯一性约束或去重机制,防止重复操作。例如:
  • 订单创建接口:订单号应作为数据库的唯一索引,防止重复插入。
  • 更新操作接口:通过乐观锁或版本号机制,防止并发更新造成数据异常。

示例
订单表设计:

CREATE TABLE orders (id BIGINT AUTO_INCREMENT PRIMARY KEY,order_no VARCHAR(64) UNIQUE NOT NULL, -- 唯一订单号status INT NOT NULL
);

插入时,如果订单号重复会直接失败,从而避免重复下单:

try {orderRepository.save(order);
} catch (DuplicateKeyException e) {// 处理重复订单逻辑
}
  1. 状态校验
    对于接口操作,可以通过校验当前状态来保证幂等性。例如:
  • 支付接口:检查订单状态是否已经支付完成。
  • 库存扣减接口:检查库存是否已经扣减。

实现方式
在操作前查询当前状态,如果状态已经是目标状态,则直接返回结果;否则执行操作。

  1. 分布式锁
    对于需要高并发操作的场景,可以通过分布式锁(例如基于 Redis 或 Zookeeper 实现)来确保同一时间只有一个请求在处理。
    Redis 实现分布式锁
boolean isLocked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (!isLocked) {return "Request already in progress";
}
// 执行业务逻辑
redisTemplate.delete(lockKey);
return "Request processed";
  1. 使用幂等设计的 HTTP 方法
    根据 HTTP 标准,某些方法天然支持幂等性:
  • GET:查询接口,通常是幂等的。
  • PUT:更新接口,应幂等(传入的资源状态相同时,多次调用结果一致)。
  • DELETE:删除接口,通常是幂等的。

示例
更新用户信息:

PUT /users/123
{"name": "Alice","email": "alice@example.com"
}

多次调用,服务器都会将用户信息更新为相同内容。

  1. 数据版本号机制
    对于更新操作,可以通过版本号(version 字段)来控制并发,防止重复更新。
    实现步骤
  • 查询记录的当前版本号。
  • 更新时携带版本号。
  • SQL 更新语句中校验版本号。
  • 更新成功后,版本号递增。

SQL 示例

UPDATE orders
SET status = 'PAID', version = version + 1
WHERE order_no = '123456' AND version = 1;

如果版本号不匹配,则更新失败。

  1. 异步消息队列的去重
    在使用消息队列时,消费端需要保证幂等性。例如:
    将消息的唯一 ID 存入 Redis,消费时检查 ID 是否已存在。
    代码示例
String messageId = "unique-message-id";
if (redisTemplate.hasKey(messageId)) {return; // 消息已处理
} else {redisTemplate.opsForValue().set(messageId, "processed", 1, TimeUnit.DAYS);// 处理消息逻辑
}
  1. 数据合并
    对于批量操作,可以通过合并数据的方式来避免重复处理。例如,将多次的相同更新操作合并成一次。

关系和联系

  • 防抖是前端常用的一种手段,主要在用户操作层面减少高频触发,避免重复调用接口,减少对后端的压力。
  • 后端要做到防抖,并不一定等同于实现幂等,但幂等是后端实现防抖的一种必要保障。后端的“防抖”逻辑更准确来说,是为了防止重复处理同一操作,而幂等正是实现这种效果的核心机制之一。
  • 防抖可以间接实现幂等效果:比如限制用户频繁点击提交按钮,可以减少接口被重复调用的概率,从而降低幂等处理的复杂度。
  • 幂等是更底层的保障:即便用户操作频繁、网络抖动或前端防抖失败,后端仍然需要保证接口逻辑是幂等的。因为接口调用可能来自多个来源(如重试机制、分布式调用),防抖无法全面覆盖这些场景。

总结

  • 防抖和幂等不完全相同:防抖更多是为了避免高频请求,减轻服务器压力;幂等则是为了保证接口的正确性和一致性。
  • 防抖与幂等可以结合使用:前端防抖可以减少请求量,后端幂等可以确保重复请求的安全性。两者结合可以优化系统性能,同时保证逻辑正确性。

防止超卖

在现代电商平台中,防止超卖是一个关键问题,尤其在高并发环境下(如秒杀活动或促销场景)。以下是防止超卖的常见解决方案和关键点:

超卖问题的本质
超卖发生在以下场景:

  1. 库存并发问题:多个用户同时抢购商品时,库存扣减逻辑未正确处理并发,导致实际库存被扣成负数。
  2. 数据库一致性问题:库存扣减和订单确认不是一个事务,可能存在中间状态。
  3. 延迟问题:在缓存和数据库同步过程中,延迟导致库存状态不准确。

解决超卖的常见方法
以下是几种主要的方法,结合具体业务场景,可以单独或组合使用:

  1. 乐观锁控制库存
    通过数据库的乐观锁机制实现库存扣减时的并发控制:
  • 在数据库中维护库存字段,如 stock。
  • 更新库存时通过 version 或 WHERE 子句校验库存是否被其他线程修改。

实现步骤:

-- 假设商品库存初始值为 10
UPDATE product_stock
SET stock = stock - 1
WHERE product_id = ? AND stock > 0;

如果同时两个请求执行,只有第一个成功减库存,第二个会失败(因为 stock <= 0)。

  1. 悲观锁控制库存
    悲观锁通过锁定库存数据避免多个线程同时操作库存:
  • 使用数据库事务的 SELECT … FOR UPDATE。
  • 适用于高并发但不需要极致性能的场景。

实现步骤:

START TRANSACTION;
-- 锁住库存行
SELECT stock FROM product_stock WHERE product_id = ? FOR UPDATE;
-- 判断库存是否足够
IF stock > 0 THENUPDATE product_stock SET stock = stock - 1 WHERE product_id = ?;
END IF;
COMMIT;

缺点:性能较低,在高并发场景下可能导致线程阻塞。

  1. 基于缓存的库存扣减
    将库存数据提前加载到 Redis 等缓存中,利用缓存的高性能处理扣减逻辑:
  • 初始化库存:将库存数据存储到 Redis。
  • 扣减库存:使用 Redis 的原子操作(如 DECR)扣减库存。
  • 最终一致性:定期同步缓存与数据库的库存。

实现步骤:

// 初始化库存
redis.set("product_stock:123", 100);// 扣减库存
Long remainingStock = redis.decr("product_stock:123");
if (remainingStock < 0) {// 回滚扣减redis.incr("product_stock:123");throw new RuntimeException("库存不足");
}

优点:性能高,适用于高并发场景。
缺点:需要保证缓存和数据库的最终一致性。

  1. 分布式锁控制库存
    通过分布式锁(如 Redis 的分布式锁机制)在高并发场景下确保同一时间只有一个线程可以操作库存:
  • 锁的粒度是商品级别(商品 ID)。
  • 适用于极高并发但单品库存更新较少的场景。

实现步骤:

String lockKey = "lock:product:123";
if (redis.tryLock(lockKey, 10, TimeUnit.SECONDS)) {try {// 查询库存并扣减int stock = queryStockFromDatabase(productId);if (stock > 0) {updateStockInDatabase(productId);} else {throw new RuntimeException("库存不足");}} finally {redis.unlock(lockKey);}
}
  1. 预扣库存机制
    在用户下单时立即锁定库存,确保库存只能被一个订单使用:
  • 下单时先锁定库存,状态设置为“预扣”。
  • 用户支付成功后,确认扣减库存;如果支付超时,则释放库存。

关键流程:

  • lock_stock 表记录每次下单预扣的库存数量。
  • 定期清理超时的预扣库存。
  1. 消息队列异步削峰
    在高并发场景下,将用户的下单请求通过消息队列进行削峰处理,确保每次只处理有限的库存扣减请求:
  • 接受订单请求后,立即返回排队中。
  • 将请求加入消息队列。
  • 后台消费者顺序处理扣减库存的逻辑。

流程图:

用户请求 -> 接口返回排队中 -> 消息队列 -> 消费者扣减库存

方案对比

方法优点缺点适用场景
乐观锁实现简单,性能较高并发量特别高时可能出现重试失败并发量中等,更新频繁
悲观锁数据安全可靠性能较低,可能造成阻塞并发量较低,数据一致性要求高
缓存控制库存性能极高,适合高并发需处理缓存与数据库的最终一致性问题秒杀、促销场景
分布式锁数据安全可靠并发量高时可能导致锁争用特殊场景(如库存量低时)
预扣库存机制减少支付失败导致的库存问题实现较复杂,需要定期清理预扣库存用户支付行为较慢的场景
消息队列异步削峰平稳处理高并发,避免瞬时压力实时性稍差,队列延迟可能影响用户体验超高并发秒杀

综合建议

  1. 中小型电商平台:
    • 使用乐观锁或缓存控制库存方案即可,简单高效。
  2. 大型促销/秒杀场景:
    • 使用Redis 缓存控制库存结合消息队列削峰,应对高并发。
    • 配合预扣库存机制减少支付延迟对库存的影响。
  3. 高一致性要求场景:
    • 使用悲观锁或分布式锁确保库存操作的安全性。

选择方案时应权衡性能与一致性需求,结合实际业务场景优化实现。


http://www.ppmy.cn/ops/144999.html

相关文章

React+TypeScript+Tailwind 实现圣诞祝福网页

圣诞节快要到啦&#xff0c;提前祝大家圣诞节快乐&#xff01;&#xff01;&#xff01; 项目完整源码在最后哦✨ 视频 &#xff08;一&#xff09;&#xff1a;项目环境搭建 在这个教程中&#xff0c;我们将一步步创建一个精美的圣诞祝福网页。本文是系列的第一部分&#xf…

【LuaFramework】服务器模块相关知识

目录 一、客户端代码 二、本地服务器代码 三、解决服务器无法多次接收客户端消息问题 一、客户端代码 连接本地服务器127.0.0.1:2012端口&#xff08;如何创本地服务器&#xff0c;放最后说&#xff09;&#xff0c;连接成功后会回调 协议号Connect是101&#xff0c;其他如下…

音视频学习(二十七):SRT协议

SRT&#xff08;Secure Reliable Transport&#xff09;是一种开源的网络传输协议&#xff0c;专为实时音视频数据传输设计&#xff0c;具有低延迟、高可靠性和安全性等特点。 核心功能 SRT协议旨在解决实时音视频传输中的网络抖动、丢包、延迟和安全问题&#xff0c;提供以下…

gitlab window如何设置ssh

在GitLab中设置SSH需要以下步骤&#xff1a; 在GitLab账户中&#xff0c;导航到“用户设置”下的“SSH密钥”部分。 生成SSH密钥对&#xff08;如果你还没有的话&#xff09;。在Windows上&#xff0c;你可以使用ssh-keygen命令来生成密钥。 在命令提示符或PowerShell中运行以…

自动控制系统综合与LabVIEW实现

自动控制系统综合是为了优化系统性能&#xff0c;确保其可靠性、稳定性和灵活性。常用方法包括动态性能优化、稳态误差分析、鲁棒性设计等。结合LabVIEW&#xff0c;可以通过图形化编程、高效数据采集与处理来实现系统综合。本文将阐述具体方法&#xff0c;并结合硬件选型提供实…

Pycharm配置PyQt 5

Pycharm配置PyQt 5 一、配置 打开Pycharm,点击File(文件)->Settings(设置): 弹出Settings(设置)配置框,在框中选择 Tools(工具)->External Tools(外部工具) 点击”+“号 在Name(名称)中输入一个名字 Program(程序)中选择安装的designer.exe 我的路径是D:\…

8.Java内置排序算法

10. Java内置排序算法 排序算法的分类 如何写一个通用的排序算法排序元素比较分治算法思想 排序算法选择的建议 O(n)排序算法的选择 1.插入排序性能最好、其次是选择排序&#xff0c;冒泡排序性能最差 2.选择排序不是稳定的排序算法 3.插入排序是最好的选择 4.对于大规模…

流量主微信小程序工具类去水印

工具类微信小程序流量主带后台管理&#xff0c;可开通广告&#xff0c;带自有后台管理&#xff0c;不借助第三方接口 介绍 支持抖音&#xff0c;小红书&#xff0c;哔哩哔哩视频水印去除&#xff0c;功能实现不借助第三方平台。可实现微信小程序流量主广告变现功能&#xff0c…