文章目录
- 简介
- 幂等性如何实现
- 前端应当处理
- 后端基于 token + redis 处理
简介
-
接口的幂等性是指:
用户对同一个操作发起多次请求,系统的设计需要保证其多次请求后结果是一致的。常见的如支付场景,连续快速点击两次支付 10 元,不应该扣费 20,第一次应该扣费成功,第二次则应该失败 -
什么场景要考虑幂等性设计
- 在现在公司里头,都在做分布式划分,一个大型系统会拆成比较多的模块,不同的模块用集群来部署,上游服务调用下游服务的形式,比如 rpc 调用,但是由于网络抖动等原因,可能会造成服务迟迟没有响应,用户端可能多次点击
- 再一个就是支付场景也必须得考虑一下幂等性的问题
- select 是天然幂等的操作,需要注意的是编辑相关的操作
幂等性如何实现
前端应当处理
前端一般对比如支付按钮需要加上频控,避免过快的连续点击
后端基于 token + redis 处理
想法:
两次针对同一订单的付款请求,如果每次这样的请求有一个唯一凭证,每次这个凭证访问到后端,看看 redis 有没有,有的话请求执行,redis 中删除凭证,没有的话请求失败。这样同样的 2 个请求就可以保证第一次成功第二次失败
具体步骤:
- 步骤一:客户端要付款前,即在点击付款按钮前,客户端先发送一个请求获取 token,服务端生成一个全局唯一 id 作为 token,保存到 redis 中,同时把这个 id 返回给客户端
// 订单生成后,然后立马一个获取 token 的请求过来
// 从请求中获取用户 userId
userId := ...
// 雪花算法,或者大厂自己封装出来的依赖包的使用
token := ...
// redis 存储此全局唯一 id 并且设置过期时间 30 min
redisClient.Set(ctx, "order:token"+userId, token, time.Minute*30)
// 业务操作,返回 token 全局唯一数据
- 步骤二:客户端点击付款,会带上这个 token id
- 步骤三:服务端接收到 token id,去查 redis 能够查询到,于是放行后续业务操作,同时删除 redis 的 id
// 先通过请求参数拿到全局唯一 id
token := ...
// redis 通过 lua 执行原子操作判定此次请求是否执行还是失败返回,lua 原子操作的原因是防止并发时都执行了
scriptStr := "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
script := redis.NewScript(scriptStr)
cmd := script.Run(ctx, redisClient, []string{"order:token"+userId}, token)
// 处理 err,失败返回客户端说明失败
if cmd.Err() != nil {
// 返回扣款失败
}
// redis 原子操作查找删除是否成功
if v, _ := cmd.(int); v == 0 {
// 不成功返回客户端说明扣款失败
}
// 执行后续业务逻辑操作
- 步骤四:客户端紧接着快速点了第二次付款,但是这时候请求打到服务端发现 redis 已经没了此 token,因此第二次付款失败
注意:
- 全局唯一 id 可以通过雪花算法,很多大公司也有封装的依赖包拿到全局 id,需要全局唯一 id 的原因也因为是分布式的环境,需要保证在集群中保证 id 唯一
- 第一次和第二次付款请求需要保证 redis 多个操作的原子性,比如先查询 redis,能查到再删除 redis 这两步操作合在一起不具备原子性,因此需要用天然支持原子性的 lua 脚本去执行,这样能保证并发快速点击多次付款后不会出现这样的并发场景:一个执行到 if(是否能查到),另一个夜之星 if(是否能查到),这样就会导致都判定走后续的付款逻辑