一. 复杂点
- M和N均会变化
- 需支持变更实时生效
- 对超领零容忍
- 可能需要溯源, 如原来5天3次改成7天2次时, 原来5天内买了1次的用户只能再领1次
二. 隐含条件
- 业务上N的最大值Nmax
如一般送体验会员, 频控最大周期为一个月, Nmax可以取2个月 - 业务上M都比较小(这意味着缓存数据的空间几乎可以忽略不计)
三. 方案选择
方案1
- kv结构
- key为uid+resourceId,value为领取次数
- expire为当前周期第1次领取资格时间向后偏移当时的N天
方案2
- zset结构
- key为uid+resourceId,member为orderNo,score为领券时间
- expireAt为领取资格时间向后偏移潜在的最大Nmax, 每次领取(expire+惰性检查删除)
- 领取次数通过size来获取
方案比较
- 方案1的过期时间包含N, 但N变化时不好处理
- 每次领取资格的时间不知道, 当规则变化时没法最大程度兼容上一版本的规则, 如原来5天3次改成7天2次时, 原来领过1次的用户只能再领券1次就不好实现, 因为没有每次的领券时间戳, 不符合业务预期
- 方案适用的场景: N不会变化
- 方案2将过期时间固定取潜在最大Nmax
- 有领取资格时间, N变化时也可根据score精确溯源
- 配置变化时会最大程度兼容上一版本, 丝滑过渡
方案二的时序图:
四. 拓展
- 上述的N为非自然月/周, 如果需要设置成每资源日/周/月重置次数, 则计算expire时还要查找最近的重置临界点
- 某些场景M=1, 如3天弹窗1次, 5天内不重复等诉求, 此时方案2的缓存结构可以直接简化为
zset结构, key为uid, member为resourceId,score为领券时间