最近在做一个项目,接口使用了腾讯云函数,昨天发现前端会有重复提交的问题。尤其是在插入性质的接口,因为上一次请求还没完成,数据库并没有数据,重复的判断不会生效,会造成多次插入的 bug ;有没有什么好的解决方案,Redis 锁、数据库锁或者网关限制那种更好一点呢。
1
MrHyde 2022-11-10 03:12:31 +08:00
queue
|
2
CEBBCAT 2022-11-10 03:55:28 +08:00 via iPhone
queue 相当于把请求排了队,但又导致了队列只能有一个消费者的问题,未见其明也
我经验不是很多,这种情况下不是默认用数据库唯一约束、RequestID 来解决吗? |
3
opengps 2022-11-10 08:20:14 +08:00 1
数据库锁只是备用,redis 锁才是关键。不然你数据库没落盘的都判断失效了
打卡页面时候提前带上唯一参数,如果这个唯一参数重复,那就说明当前页面存在重复提交,用这个参数实现拦截,甚至可以用这个参数达到幂等效果。 |
4
xuanbg 2022-11-10 08:22:12 +08:00
RequestID 如果在请求时产生,那 P 用没有。如果在初始化页面时产生,真要重发请求的时候就 SB 了。最有效的办法就是按钮点击即 disable3-5 秒。这三五秒内,就无法重复请求。过了这三五秒还没成功或失败,OP 你自己看着延长时间吧。
|
5
ragnaroks 2022-11-10 08:35:58 +08:00
首先前端一定需要做 throttle ,倒不是说方法都要 throttle() 一次,像楼上写的点击按钮后使按钮处于不可响应状态就是简单有效的方式。
其次根据具体业务来选择队列、锁、唯一键,处理像站内信这样的发后不管就可以用队列,处理像提现这样的只需要知道是否执行成功而不管业务结果就可以用锁,处理像修改某实体数据这样的就用唯一键。 |
6
dengji85 2022-11-10 08:48:10 +08:00
前面说的按钮禁用是比较好用的方案
|
7
YepTen 2022-11-10 08:51:02 +08:00 1
对后端来说:
1. 单节点 可以试试 LRUCache ,key 为 hash ( RequestData ),value 为 systemtime 。 对 LRUCache 指定大小 对 systemtime 进行判断,比如重复提交的最小间隔为 3S 。 业务处理上: 先查数据库,没有就放在 LRUCache 中。 2. 多节点 redis 锁吧。 |
8
DreamStar 2022-11-10 09:28:35 +08:00
数据库层面
数据库上唯一限制, 并发更新上乐观锁字段. 代码层面 做个过滤器, 出个幂等接口返一个 token, 同 token 只有一次能成功, 多次就是重复请求 redis 锁 |
9
BugCry 2022-11-10 09:30:10 +08:00
加参数:timestamp + nonce ,每次请求生成一个随机数作为 nonce 。
请求进来先判断一下(timestamp, nonce)是否存在,存在说明是重复请求。 好处是灵活,依赖少,不用锁。单节点可以直接在内存判断,多节点存 redis |
10
wolfie 2022-11-10 09:31:09 +08:00
技术层面做不到通用的幂等。
拿 redis 作为锁 double check 。 |
11
qrobot 2022-11-10 09:31:35 +08:00
@ragnaroks 首先 throttle 是一件非常错误的行为, 因为 throttle 的实现就是 setTimeout , 这个在 JS 的线程模型中, 部分场景中会有非常不可思议的结果, 其次 throttle 多少毫秒合适? 600 毫秒? 1000 毫秒? 比如点击了,没有反馈, 就算间隔 5 秒,用户也会进行不断的点击, throttle 无疑就是一种非常错误的设计, 也是非常错误的解决方案.
相对于 throttle 来说, 按钮上的 loading 或者 按钮上的 禁用 都是一个非常好的方案, |
13
swulling 2022-11-10 09:33:53 +08:00 via iPhone
重复提交直接做幂等设计不就完了。
你最终是要落数据库的,那你就需要准备一个唯一字段,比如增加一个 uuid 字段。 这个字段由客户端生成,发过去后如果重复提交落库就会失败。 |
14
xiang0818 2022-11-10 09:44:05 +08:00
首先让前端做防抖,避免因为网络问题等问题造成的重复提交,二就是后端做幂等设计。方案网上有很多。
|
15
iPisces77 2022-11-10 09:53:32 +08:00
最后还是数据库唯一性兜底的
|
17
ragnaroks 2022-11-10 10:07:15 +08:00
@qrobot
翻了你的 github 发现你可能确实没有经验,那我就好心为你讲解下。 "throttle" 并不是说前端的 throttle() 或类似的方法,而是说对某一件事情进行节流控制,事实上在 SOF 更多的是数据库和 HTTP 服务器相关。4 楼和我写的,将按钮处于一个不可响应的状态,那对于用户多次点击按钮这个行为来说就是 throttle 。至于到底是加 disabled 属性,还是替换为 loading 组件,那是具体产品设计。 最后,throttle 从来不是"非常错误的行为",在没有异步完成端口之前,F5 就能刷死 HTTPd 。 |
18
facelezz 2022-11-10 10:15:34 +08:00
重复提交业界的方案:以 uber 为例子
1.返回一个 rid 给前端,前端带着这个参数请求 2.后端有一个 rid 的表( rid 唯一索引),执行业务时把插入 rid 表的语句和业务组合成一个事务 |
19
facelezz 2022-11-10 10:16:59 +08:00
不推荐 redis 的锁的原因,因为如果是支付相关的,最好不要使用这种效率锁(可以参考 Martin Kleppmann 和 redis 创始人的讨论)
|
20
pengtdyd 2022-11-10 11:11:25 +08:00
|
21
lmshl 2022-11-10 12:18:09 +08:00
前端生成资源 UUID ,重复提交始终更新同一个 UUID ,不就幂等了么?
|
22
qrobot 2022-11-11 11:30:31 +08:00
@ragnaroks 我跟你讲一下 throttle 也就是节流, 指的是在指定的间隔中指执行一次. 你说了,我可能在这方面没有经验.
请回答我两个问题 1. throttle 间隔时间多少合适? 2. throttle 会导致当前队列中所有的消息都处理完毕之后才能执行, 在特定的情况下, 我希望立即执行此函数应该怎么办? 3. 如果请求在 throttle 间隔时间中失效了, 我是否要等待 throttle 间隔结束? 当你回答了我的这三个问题, 请问 `throttle 从来不是"非常错误的行为"` 你还保持此观念吗? 你可以看我的 github 里面的所有方案, 我从来不会采用 throttle ,这种方式进行所谓防止多次触发. 在回答你之后的一个问题, 如果在 在没有异步完成端口之前,F5 就能刷死服务器, 很显然, 这个服务器的后端代码存在严重的问题, 前端的防止多次点击, 本质来说是为了给客户进行一个良好的视觉反馈, 而并不是为了保证请求到后端的接口一定就是一次. 因为恶意用户完全可以通过其他方式进行多次发送请求. |
23
qrobot 2022-11-11 11:36:28 +08:00
@ragnaroks 你可以在 Google, Facebook, Github 等产品中去学习一下, 看看在防止多次点击的情况下, 他们是否会使用 throttle 做优化处理, 或则你看在好的虚拟滚动的解决方案中, 是否经常采用 throttle 来作为解决方案, 确实 throttle 很方便, 但是我认为它并不是好的解决方案, 我甚至认为, 这在某种场景下, 是一个滥用的行为(非常错误的行为)
|
24
qrobot 2022-11-11 11:44:09 +08:00
一个请求大概会有 发送(send), 接受(receive), 重试(retry), 这三个阶段, 在 throttle 中并不是很优雅的对这三个阶段中进行很好的操作
|
25
ragnaroks 2022-11-11 11:54:06 +08:00
@qrobot
什么叫"在回答你之后的一个问题"?除了这我这当前的楼层,我在这个贴子还提出过问题? throttle 什么时候变成了"指的是在指定的间隔中指执行一次"? 你规定的? 有没有可能,throttle 指的是一种处理事务的方式而且这个时候连浏览器都没被发明? 你的 1 、2 、3 基于的假定就是错的,看来你完全无法理解 throttle ,但我还是按你的思路来解答你的疑问。 1:事务开始直到事务结束 2:throttle 只针对单个事务,throttle 不会导致当前队列中所有的消息都处理完毕之后才能执行,但可能导致事务无限期运行 3:请求失效(超时)、请求成功、请求错误都是事务结束的一种( return ),所以不存在需要等待 throttle 间隔的问题,但如果未设置超时时间则可能无限期执行此请求 综上所述,throttle 唯一的缺陷在于没有被正确结束时会导致事务无限期运行,形成一个阻塞( block )的效果。但这是人的问题,不是 throttle 的问题。我依然保持 throttle 从来不是"非常错误的行为"的正确观念。 我其实很不理解,throttle 是一种处理问题的方式,为什么一定要被你限定到前端上的 `const handler = throttle(callback)`? 用户点击按钮触发事件,代码将按钮替换为 loading 组件,fetch 请求返回然后代码将 loading 组件替换为按钮,这个流程就是 throttle ,你一边诋毁 throttle ("首先 throttle 是一件非常错误的行为") 又一边赞美 throttle ("按钮上的 loading 或者 按钮上的 禁用 都是一个非常好的方案") 确实挺有意思的。 最后提醒你一下,HTTPd 是 Apache ,一个古老的 HTTP 服务端应用程序,我不知道怎么就和后端扯上关系了。throttle 最开始还有一个在事务处理方面的同义词叫 backlog 。 我通常很少发布长篇大论在论坛上,因为驱逐劣币的活一般轮不到我。 |
26
ragnaroks 2022-11-11 11:56:38 +08:00
给楼主点了个感谢并加了收藏,很少有这样精彩的贴子用于在各种群里面当历史必修一了。
|
27
qrobot 2022-11-11 13:51:30 +08:00 1
@ragnaroks 因为长期在前端工作中, 我把 throttle 局限为 lodash.throttle 这样的方法, 这非常抱歉.
|
28
ragnaroks 2022-11-11 14:38:08 +08:00 1
|
29
iam OP 看来我沉了 24 天!!!
|