前言
api服务里,幂等场景在【写入】场景非常重要。场景包括了:
- 修改任务进度
- 领取奖品
- 充值
- ……
幂等的作用,是防止某一次操作,在并发调用时,写入操作意外地执行了不止一次。(理论上,不论执行多少次,执行结果,都应该和一次相同)
幂等的实现往往围绕某一个key, key具备一定的业务含义:
- 基于用户id 和用户请求限频有关
- 基于订单id 和充值相关
幂等和限频的关系是:
- 幂等比限频约束更精准
- 幂等一般固定为限频为单次
- 幂等的作用是防止并发造成强相关数据幻读。限频的作用是,降低单节点负载。
幂等是如何保障【写入】场景的安全的
以基于订单id的幂等为例。用户凭借订单id来获取道具,同一个时间,发了15次请求,那么:
- 基于数据库处理并发幂等,
update game_order set has_present=2 where order_id=? and has_present=1
- 第一个请求修改订单状态为已赠送,并下发道具
- 后14个请求,修改订单状态时,因为无法查询到未赠送的订单(第一个请求已经将它置为了已赠送),所以基于订单id已赠送,后续的14个请求将不会继续赠送道具。
二级幂等
很显然,除了第一个请求是有效请求,后续14个请求,都意外的打入了db,这在高流量的应用中是不可取的。所以,二级幂等非常重要。
二级幂等具备以下特性:
- 幂等key和一级幂等key强相关
- 可以具备时效
- 不会打入db层
- 分布式服务可靠
由上述的特性,可以使用Redis来实现这一层二级幂等。
func Once(conn redis.Conn, key string, seconds int) bool {
if seconds == -2 {
seconds = int(TomorrowZero().Sub(time.Now()).Seconds())
}
rs, e := redis.String(conn.Do("set", key, "done", "ex", seconds, "nx"))
if e == redis.ErrNil {
return false
}
if rs == "OK" {
return true
}
return false
}
基于前面的订单场景,那么使用时,表达为
if !Once(
conn,
fmt.Sprintf("app:shop_exchange_order:mideng:%s", orderId),
3
) {
fmt.Println("该订单已经被处理了")
return
}
含义: 某个订单的兑换场景,3秒内,只会放行一条记录。
- 2级幂等和1级幂等的key是强相关的。数据库为一级幂等,直接使用order_id。缓存使用2级幂等,key是order_id包装的字符串
- 缓存3秒后过期。
- redis是内存缓存,后14个请求,不会进入数据库
- 分布式服务共用一个redis集群/节点,所以分布式会共享这个key的状态,是分布式可用的