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

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 条回复
clf
2022-03-04 16:30:31 +08:00
coupons 表先不会动,随机往数据库 U 表里塞 N 个用户,各个服务自由塞,超过 coupons 表个数无所谓。

然后由一个服务取最前面 N 个不同的用户和 coupons 里一一对应即可。
freelancher
2022-03-04 16:32:57 +08:00
考虑用伪随机吗?例如给用户手机尾号是 9 的发优惠券。发到完为止。就好啦。没这么多要操心的。
ZSeptember
2022-03-04 16:35:18 +08:00
@Habyss
@mxT52CRuqR6o5

我强调随机是是因为当前的保证不重复分配的方案是乐观锁,随机能避免冲突,提高性能。

如果分配方案不使用乐观锁,可以不需要
但是整体方案需要考虑到正确性以及性能
mekingname
2022-03-04 16:38:31 +08:00
如果你是随机取一个 coupon ,那为什么你不一开始就把所有 coupon 顺序打乱再入库,这样按顺序读取不就相当于原来顺序入库时的随机读取了吗?这样用户请求的时候,你就按顺序返回就好了。每次返回的时候就把这一条锁定。
mango88
2022-03-04 16:43:15 +08:00
不用乐观锁的话,
就把生成好的 coupons 读出来放 redis 里,每次 lpop 一个出来
ZSeptember
2022-03-04 16:43:57 +08:00
@mekingname 看我 23 楼。

随机不是目的。
而且随机入库以后,我多个服务实例读出来怎么错开
ZSeptember
2022-03-04 16:46:33 +08:00
@freelancher 你这是一种方案,对用户分区,冲突概率会低一些,但是哪个实例负责哪个分区也是一个问题,需要协商。。。
RickyC
2022-03-04 16:49:00 +08:00
遇到一个问题:ASDJAF!@#, ASDASD , @EDQMQOW , ASDAD , V(@#(JD
------
你后面的话,我一句也看不懂。
ZSeptember
2022-03-04 17:05:30 +08:00
@haython 这一点和我们业务不一样,我们的 coupon 是一个 coupon code 发给用户就需要能使用的。
现在的方案,不是在生成 coupon 的时候就给用户绑定,不过确实启发了一下,可以在生成的时候就绑定到人。就是较大调整。
CantSee
2022-03-04 17:20:11 +08:00
通过 Queue 进行读取是不是好点呢,每次读取一个,绑定用户,预加载到 Queue 中,没有了再查一批加载到 Queue 中;
oddcc
2022-03-04 17:21:09 +08:00
感觉就是个发号器啊

可以考虑这样做
有两张表,一个表放未使用的,一个表放已使用的
提前生成一大批可用的 coupon 放到未使用的表中

每次把一批 coupon 读到内存之后,就移动到已使用的表中,视为已使用的
根据并发的压力调整这一批 coupon 有多少个

这样你有多个分配服务,也不会重复,也不会冲突。也不涉及到其他的依赖,也不涉及到复杂的锁
就算中间服务崩溃了,恢复之后也不会有重复的
9c04C5dO01Sw5DNL
2022-03-04 17:22:17 +08:00
必须得预先创建吗?可不可以请求分配时再创建 coupon ,写入成功代表分配成功。
haython
2022-03-04 17:30:16 +08:00
@ZSeptember 只要是发到用户账户里,都可以一个码给多个人,使用的时候,只检查券的使用规则和用户的使用记录。只有让用户手动输入码的,才要保证唯一
ZSeptember
2022-03-04 17:38:31 +08:00
@oddcc 你这个和我 12 楼的一样的,是可以的。读 100 个,然后把这 100 个删掉,就不会被其他实例读取了。
edward1987
2022-03-04 17:48:28 +08:00
同 #1 #25 。 生成时随机,然后直接存 redis,用 lpop 读取就行。 你原本的 redis 方案太麻烦。。
corningsun
2022-03-04 17:48:56 +08:00
同步并发操作转异步单线程就好了,加个中间状态。

1 用户请求后是标记为 “待分发” 状态。
2 再单独起个线程跑任务,取所有 “待分发” 的用户,按顺序从 coupon 分配,这时候就不存在冲突的问题了。
3 用户查询的地方改一下,加个 “分配中” 页面
ZSeptember
2022-03-04 18:12:13 +08:00
@corningsun 服务多实例的,你这是转单实例了。
9c04C5dO01Sw5DNL
2022-03-04 18:12:50 +08:00
我的意思是,如果预创建只是为了限制数量的话,就不需要预创建了。可以用乐观锁计数,和请求时分配 coupon 放在同一个事务中。

比如:

```

begin;

update ct set count=count+1
where count=? and count<?;

update 成功则继续:

insert into coupon_user ......

insert 成功则表示分配成功

commit ;


```
ZSeptember
2022-03-04 18:13:12 +08:00
@edward1987 @mango88 可以,直接不落数据库
ZSeptember
2022-03-04 18:21:14 +08:00
@giiiiiithub 忘记说明一个大限制,因为一些不可控的原因,创建 coupon 是有 rate limit 限制的 2 QPS ,40 RPM 。但是提供批量创建机制 每次 100 个。

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

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

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

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

© 2021 V2EX