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

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 ,方案比较复杂,不好维护;想跟大家请教下,看有没有什么更简洁的方案,不用引入数据库以外依赖

3115 次点击
所在节点    程序员
56 条回复
sagaxu
2022-03-04 15:14:02 +08:00
生成时随机,分配时按顺序
ZSeptember
2022-03-04 15:22:28 +08:00
@sagaxu 可以具体一点吗。生成的时候随机,其实生成以后已经不随机了。
我们用的 spanner ,不支持 random 函数。。不然每一批可以 random 选择 100 个,冲突概率也不高了。
agzou
2022-03-04 15:22:59 +08:00
coupons 分配服务有多个的时候,肯定要用到分布式锁,最简单就是用 redis ,有现成代码,也可以用 JDBC+数据库实现一个分布式锁。
agzou
2022-03-04 15:24:10 +08:00
@agzou #3 忘了看 OP 语言,我说的是 java 下的方案
ZSeptember
2022-03-04 15:28:38 +08:00
@agzou 分布式锁更重了,分布式锁的 timeout 很难解决。用数据库乐观锁简单点,但是大佬还是觉得复杂了,不希望引入 redis 。
murmur
2022-03-04 15:29:53 +08:00
我感觉这个可以从业务层面解决,不想用 redis ,不想上排它锁,那就把数设置灵活点,比如 100 张的名额,那我做活动就发 80 ,万一锁出了问题发了 90 张也在承受范围内

另外大额优惠券在实际电商的时候本来就是看人的,有些人生来就失去资格了,所以并发也是可以优化的
murmur
2022-03-04 15:33:14 +08:00
另外,实际上发卡系统对实时性也没有明确要求,京东不是经常告诉你你的优惠券 5-10 分钟才会到账么

很多东西就得用业务解决,死磕技术是没用的,就跟双十一抢购一样,0 点抢购天怒人怨,连续热卖两个月不好吗?
ZSeptember
2022-03-04 15:35:52 +08:00
@murmur 数量其实不是重点,重点是一个 coupon 只能给一个人发,不能重复发。
ZSeptember
2022-03-04 15:36:42 +08:00
重点其实看怎么能提高随机读取 coupon 的性能以及随机性
ZSeptember
2022-03-04 15:39:20 +08:00
@murmur 实时性其实要求也不是特别高,但是还是需要提高分配性能而已。

现在其实有一个最基本的方案:

1. 读 100 个 coupons
2. 随机选择一个

冲突概率还是挺低的,但是每一次都要读数据库,有点浪费
Habyss
2022-03-04 15:56:00 +08:00
1. 读取 100 个 coupons(条件是未标记预分配), 并数据库标记预分配
2. 随机的话, 是否能做到生成到数据库中时就是随机的呢
ZSeptember
2022-03-04 16:06:52 +08:00
@Habyss
1. 第一种考虑过,读取 100 个,然后全部删除掉,和预分配状态本质差不多;就是考虑到这时候挂掉了就浪费一批 coupons 。
2. 存到数据库可以给一个随机编号,但是不知道怎么能读出来是随机的?
mxT52CRuqR6o5
2022-03-04 16:11:53 +08:00
从数据库选取一条未被发出的 coupon 并标记为已发送(直接用原子操作实现,就不需要关心锁的问题)
把这条 coupon 发给用户,发失败的话就从头再来
代价是可能会有一些 coupon 没被发出去但被标记为已发送
这个方案如何
haython
2022-03-04 16:13:54 +08:00
我们当时也这样做过,后来发现,既然跟用户绑定了,券一样也无所谓
mxT52CRuqR6o5
2022-03-04 16:19:55 +08:00
你很强调随机,不是很明白你这个随机具体是要用来干嘛的
就像#1 说的 [生成时随机,分配时按顺序] ,分配时根本不需要随机
luckyrayyy
2022-03-04 16:20:28 +08:00
@ZSeptember 这种情况不用删除掉啊,打个已经被获取的标记,等到真的分配成功后再删除或者打标记分配成功。如果有一批 coupons 被获取了,但是服务器挂掉,那就用一个 Task 扫表长时间获取未分配的重新改成未分配。
Habyss
2022-03-04 16:26:34 +08:00
@ZSeptember
1. 挂掉是极端情况, 按照预分配状态的话, 即使挂掉了也是可以再将预分配还原的(预分配 /未分配 /已分配)
这样有一种情况就是, 数据库未分配 coupon 发完了, 但是还存在极少部分在内存中预分配, 这时候基本上已经尾声了, 那就直接查出预分配的来分... 反正也是有数据库的乐观锁的.
2. 我想的简单, 就是打乱之后再存数据库, 那顺序拿出来的也就相当于随机了...
InternetExplorer
2022-03-04 16:29:14 +08:00
生成的时候随机好,每个 coupon 给一个发放时间点,时间点可以均匀分布在活动时间上,哪个用户最先在发放时间后访问(或者请求)就分给哪个用户。
ZSeptember
2022-03-04 16:29:19 +08:00
@mxT52CRuqR6o5 看分配步骤,随机性是为了避免乐观锁冲突,影响分配性能,因为要保证不能重复分配同一个 coupon 。

你的那个方案和我 10 楼回复的一样,能 work ,但是性能应该应该比较差。其实我测试过,性能应该能满足我们系统现在的需求,但是天花板比较低。
ryd994
2022-03-04 16:29:53 +08:00
@ZSeptember #12
自增 id+随机 coupon 内容 /id
读出的结果不就是随机的了吗?

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

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

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

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

© 2021 V2EX