遇到一个问题,需要从预插入数据的表,随机找一条,分配给一个用户,求一个简洁的方案

2022-03-04 15:05:53 +08:00
 ZSeptember

背景:

需求:

预先创建一批 coupons ,存到 coupons 表里面,然后将这些 coupons 发给用户,要求是一个 coupon 只能发给一个用户,不能重复发放。

分配性能越高越好。

现状:

  1. coupons 分配服务有多实例
  2. 其他服务请求 coupons 分配服务获取 coupon

当前方案:

两个步骤:

  1. 随机读一个 coupon
  2. 分配 coupon 使用数据库事务+乐观锁,保证不重复分配

所以优化重点是:尽量少访问数据库;随机读到的 coupon 尽量不要冲突。

优化方案:

预读取 100 个 coupons 到内存,分配的时候优先从缓存中读取,缓存没有从数据库再读一批。

为了解决多实例读取冲突问题,在 redis 记录一个 coupon id 作为 cursor ,每读一批,将 redis 中的 cursor 更新为最新的,下一批读取的是,id > cursor 的 coupons 。

redis 可以做到 cas 更新 cursor ,可以保证读取的每一批都不会重复。

cursor 也是 30min 失效,下一次继续从 0 开始,就算读取了一批,但是没有分配,然后挂了,被跳过的 coupons 还是会有机会读到的。

求教

因为公司大佬觉得引入 redis ,方案比较复杂,不好维护;想跟大家请教下,看有没有什么更简洁的方案,不用引入数据库以外依赖

3083 次点击
所在节点    程序员
56 条回复
corningsun
2022-03-04 18:29:04 +08:00
@ZSeptember

用户请求还是多实例啊,只是把分发 coupon 变成单线程了。

单线程操作的速度是很快的,比如设置批量操作上限 1 万条。

每次查询 “待分发” 的用户最多 1 万个,然后去 coupon 查询和用户数相同的出来直接分配,再批量更新。
ZSeptember
2022-03-04 18:43:22 +08:00
@corningsun 可以,转串行,没问题,就是改动比较大。
siweipancc
2022-03-04 18:53:20 +08:00
随机入库的数据有一个唯一索引列 1 开始递增,用户每次请求 redis 原子加一取对应数据行。
缺点是每次初始化数据批量入库都要全局锁,而且业务回滚之后对应的行要删除下次入库不能复用。
rekulas
2022-03-04 21:42:32 +08:00
如果不想用 redis ,消息队列也可以实现该功能,预先把可用 coupon 都推到消息队列,消费即确认该 coupon ,还可以进一步集合消息队列延迟更新数据库,几乎不会造成压力
lldld
2022-03-04 22:01:12 +08:00
好比手机号嘛, 按号段先分配给不同的运营商, 运营商再卖给个人.

另起一个服务, 目前的服务每次向新的服务申请若干连续的优惠券(申请记录在一张新表里面, 请求比较少, 可以不用 redis), 用完了再申请.
darkengine
2022-03-04 22:04:58 +08:00
我觉得完全没有必要随机啊,只要 coupon code 不会被猜出来就行了。对于开发者来说你觉得是顺序的,但是对于单个用户来说,随机领跟按顺序领没有区别。
ZeawinL
2022-03-04 22:37:54 +08:00
用用户 id 和可分配的 coupon 数取模
ZSeptember
2022-03-04 22:46:19 +08:00
@ZeawinL 每次都要 count 一次,然后 offset 一次,性能不太行。
chihiro2014
2022-03-04 22:51:15 +08:00
不一定要 redis ,用 Guava Cache ,本地缓存
Huelse
2022-03-04 23:05:18 +08:00
插入的时候就做好随机了,取的时候按顺序取这么做应该没问题
xy90321
2022-03-04 23:35:30 +08:00
1 ) coupon 表乱序插入,并且每行加一个自增 ID 。2 )为自增 ID 做一个 Sequence 。3 )所有服务通过 Sequence.nextval 拿到 ID 来取 coupon 。
ZSeptember
2022-03-05 00:03:50 +08:00
@xy90321 自增步长改成 100 ,就是和我最开始的方案差不多,只是我是用 redis 保存 sequence 。使用自增连续序列确实可以,不过我们使用 spanner ,自增插入会有热点问题,一般不考虑。
不过我的方案,相比,去掉了 redis 依赖,热点影响应该还可以接受,感觉确实还行。
xy90321
2022-03-05 14:38:07 +08:00
@ZSeptember
如果 redis 做原子操作的性能能接受的话,那无非是再加个允许接受 coupon 意外灭失的前提。
这样 redis 每次从 coupon 表里面读出一套 coupon (比如 5000 条)的同时,把 coupon 表里对应的 coupon 状态直接切成已消费完毕。
这样不论 redis 怎么搞,最多就是意外灭失 5000 个 coupon 而已,不影响你主体业务逻辑的完整性。
ZSeptember
2022-03-05 20:27:06 +08:00
@xy90321 看我 12 楼,你这个方案提过了
snowsui
2022-03-06 12:37:12 +08:00
给你一个方案,这一批 coupon 我理解应该是一个规则吧,假设这个规则叫做活动,缓存里存下活动 id&用户的关系就行了,这样是否领取通过缓存判断,有这个用户就领取过了。没有的话再去通过发号器也好,还是之前生成的也好,落库 couponId 绑定用户
sampeng
2022-03-06 14:14:33 +08:00
redis 要维护麻烦,就没有比 redis 更麻烦的了。。。再说了。。没有 redis ,很多性能问题需要花很大的精力才能解决。。

另外,如果实例数是固定的,一致性 hash 就能把预生成好的直接匹配到每个实例上去。我觉得每次 load 几百条进来挺好的。。简单可依赖。。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/837977

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX