V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
lipaa
V2EX  ›  程序员

讨论下 卡密下发 保证不重发 除了不用 redis 还有啥方案

  •  
  •   lipaa · 2022-01-05 14:48:13 +08:00 · 5462 次点击
    这是一个创建于 1051 天前的主题,其中的信息可能已经有所发展或是发生改变。

    业务背景:公司采购了一批爱奇艺会员卡密,导入到了 mysql 现在的做法是有用户购买就获取最后一张未使用的, 更新时判断状态(可能已被使用 会失败),如果失败就自旋重试 redis 的方案已经启弃用了 感觉难以保证一致性... 各大 v 友还有啥方案 麻了

    第 1 条附言  ·  2022-01-06 09:29:40 +08:00
    以解决 列队异步消费确实有也不错
    54 条回复    2022-01-07 00:01:58 +08:00
    lipaa
        1
    lipaa  
    OP
       2022-01-05 14:49:01 +08:00
    失败就自旋重试 感觉也不好 所以..
    Rwing
        2
    Rwing  
       2022-01-05 15:00:12 +08:00
    加锁。。。。
    murmur
        3
    murmur  
       2022-01-05 15:00:54 +08:00
    并发有多大,你们那点销量需要很严格的锁机制么
    murmur
        4
    murmur  
       2022-01-05 15:01:45 +08:00
    最后一张未使用的是什么意思,我感觉这需求就有问题,连 ATM 都知道取钱和存钱是两个箱子

    用户买了这卡,那就死活不退,这口子开不得
    lipaa
        5
    lipaa  
    OP
       2022-01-05 15:05:11 +08:00
    @murmur 按主键排序取第一条未被使用的啊 需求没问题 很正常的需求
    yushiro
        6
    yushiro  
       2022-01-05 15:07:05 +08:00 via iPhone
    这种需求能有多大的并发量啊?取卡密的时候上个锁不行?
    murmur
        7
    murmur  
       2022-01-05 15:07:47 +08:00
    @lipaa 哦,那我理解错了,我还是想问并发,因为你再严格的锁也会遇到用户撕逼,我就跟你死磕你卖我的卡不能用,你不处理么

    所以是不是可以考虑宽松锁剩下的客服处理
    abcbuzhiming
        8
    abcbuzhiming  
       2022-01-05 15:09:49 +08:00
    技术上保证不重发,那就只能加锁变成序列化。除此之外,看看有没有人能在业务模型上提出个新方案来
    Canon1014
        9
    Canon1014  
       2022-01-05 15:11:12 +08:00
    并发不是特别大加个乐观锁就够了吧
    WildCat
        10
    WildCat  
       2022-01-05 15:12:05 +08:00
    很经典的系统设计问题?

    分段加锁?

    https://zhuanlan.zhihu.com/p/104515829
    h82258652
        11
    h82258652  
       2022-01-05 15:13:44 +08:00
    加锁
    数据库获取一张未使用的
    将这张标记为已使用
    释放锁

    并发不高就这么搞吧

    并发高那也只能排队了,这业务就是个库存问题
    murmur
        12
    murmur  
       2022-01-05 15:14:12 +08:00
    @abcbuzhiming 模型就更简单了,直接排队领卡号,锁这部分更好设计,就跟淘宝一样,你买了卡,不是立刻收货,过段时间别人会把卡号通过消息给你发过来,或者直接代充

    那后面的模型更简单了,不需要确认收货,撕逼都省了
    wei745359223
        13
    wei745359223  
       2022-01-05 15:16:09 +08:00
    两张表
    卡密表
    ID,CODE

    发卡表
    ID,USER_ID

    两个表 ID 都是自增,往发卡表里插入数据,返回的 ID 在卡密表里找到就占用。
    jadec0der
        14
    jadec0der  
       2022-01-05 15:17:04 +08:00
    更新时判断状态会失败,是指…乐观锁?
    EileenJ
        15
    EileenJ  
       2022-01-05 15:43:06 +08:00
    pg 里用 skip locked 应该可以实现,mysql 不知道有没有类似的功能
    lipaa
        16
    lipaa  
    OP
       2022-01-05 15:46:04 +08:00
    @Rwing 是的
    lipaa
        17
    lipaa  
    OP
       2022-01-05 15:46:15 +08:00
    @jadec0der 是的
    Chinsung
        18
    Chinsung  
       2022-01-05 16:13:37 +08:00
    上个分布式锁就行了吧,觉得 redis 不可靠就上 zk 。
    你自旋的方案也没什么问题,但是锁肯定更好一些。
    自旋的方案如果想吞吐高点,可以给卡密表分下区,然后再建张表记录表记录每个区里剩几张,在一个事务里更新。
    比如卡密表 10 条记录分区,那记录表就记录 10 (范围)-9 (剩余数量)。然后请求进来就先找一个剩余数量大于 0 的分区去尝试更新。
    如果你卡密数量无限,不怕超发,其实楼上兄弟那个双表自增主键的方案也挺好的
    abigeater
        19
    abigeater  
       2022-01-05 16:14:36 +08:00
    用户点击领取向一个表写入一条记录 并返回 ID 给用户
    然后提供一个接口给前端 通过 Id 获取卡密 让前端轮询调用直到有卡密返回
    再起一个定时任务之类的 队列方式从卡密表拿一条就给领取表填一条

    是不是就解决了。。
    philchang1995
        20
    philchang1995  
       2022-01-05 16:26:13 +08:00
    @abigeater 通过 id 获取卡密这个操作是如何实现的?类似 13 楼说的那个双表自增主键的方式么?
    philchang1995
        21
    philchang1995  
       2022-01-05 16:29:56 +08:00
    @abigeater 还有你的那个给领取表填数据的定时任务是不是不太合理,领取表的数据来源只有用户触发领取操作这一个途径才对吧
    Rache1
        22
    Rache1  
       2022-01-05 16:32:26 +08:00
    如果觉得单独引入 redis 做锁成本太高的话,又想用类似可铐的方案,可以用 flock
    boozer
        23
    boozer  
       2022-01-05 16:37:09 +08:00
    说个题外话, 有时候业务问题不该让技术来买单
    hgc81538
        24
    hgc81538  
       2022-01-05 16:55:08 +08:00
    我想到這個方法:

    UPDATE key_table set user_id="123" where id = (Select min(id) from key_table where user_id=null);

    如果 affected rows == 0 就重試一, 兩次, 都不成功就報錯, 叫用戶稍後再試
    如果 affected rows == 1, 則成功.
    這方法不用鎖, 請問可行嗎?
    abigeater
        25
    abigeater  
       2022-01-05 17:04:25 +08:00
    @philchang1995 主要目标是将领取这个过程变成异步填充
    和 13 的有区别吧 毕竟我更希望是有 ID 关联 有日志可查
    假设一共有 2 张表卡密表 和 领取表
    卡密表 id,code
    领取表 id,user_id,code
    通过 ID 获取是指,领取表的 ID 找到该条记录 (如果怕被穷举 可以用 hash 或者鉴权 user_id ) 轮询到 code 不为空为止。

    正如你所说领取表数据来源是用户触发,用户写入了 user_id 到领取表后,一个队列(假设是定时任务或者 mq 消费队列)只需要取出 code 等于空的数据 从卡密表获取数据后 update 即可。

    主要目的把这个领取过程交给后台异步队列填充,避免并发。
    ZXCDFGTYU
        26
    ZXCDFGTYU  
       2022-01-05 17:28:39 +08:00
    之前遇到过类似的问题,用 for update 结合 order by 使用次数 asc 解决的
    ganbuliao
        27
    ganbuliao  
       2022-01-05 17:33:49 +08:00
    redis list 不是挺好的吗
    gam2046
        28
    gam2046  
       2022-01-05 17:45:45 +08:00
    这样可以嘛?

    cursor = select ... for update from ... where status = unused limit 1

    update set cursor.status = used

    return cursor
    Pythoner666666
        29
    Pythoner666666  
       2022-01-05 17:46:52 +08:00
    @EileenJ 没有·
    dzdh
        30
    dzdh  
       2022-01-05 17:49:52 +08:00
    难道不是给 code +个 order_id 的字段么
    Pythoner666666
        31
    Pythoner666666  
       2022-01-05 17:54:00 +08:00
    归根结底就是个并发问题。下发的时候加锁,可以解决 99%的下发。剩下 1%的锁冲突就丢到队列,然后消费队列即可。
    lipaa
        32
    lipaa  
    OP
       2022-01-05 17:55:07 +08:00
    @ZXCDFGTYU for update 只是锁住了 b 如果 a 事务释放 b 之后还是可以拿到锁的
    lipaa
        33
    lipaa  
    OP
       2022-01-05 17:55:23 +08:00
    @ganbuliao 领导觉得一致性难以保证
    lipaa
        34
    lipaa  
    OP
       2022-01-05 17:56:17 +08:00
    感谢 大家 还是自旋把 一次性取一大批 再去乐观锁更新 也基本可以保证不出问题
    philchang1995
        35
    philchang1995  
       2022-01-05 17:56:40 +08:00
    @abigeater 那是我一开始理解错你的队列的意思了,按这样的逻辑来的话确实可以避免发卡的并发操作 只是领卡并发高的时候用户轮询等待卡密这一块可能会等的久一点 不过我个人感觉这个方案可行👍
    whcoding
        36
    whcoding  
       2022-01-05 17:58:27 +08:00
    我公司给你这差不多的一个业务
    whcoding
        37
    whcoding  
       2022-01-05 18:00:24 +08:00
    我公司给你这差不多的一个业务 是这么处理的 导入的时候 同时导入 mysql 和 redis list, 用户来取码直接取 list 中的 然后去走个队列去修改数据库. 完事 ~
    Building
        38
    Building  
       2022-01-05 18:00:35 +08:00
    分成几张表放,再加锁,并发不就好了吗。
    Chad0000
        39
    Chad0000  
       2022-01-05 18:05:06 +08:00 via iPhone
    这种并发应该不高吧,可能使用队列,然后只有一个消费者,外加乐观锁,外加先写日志,就差不多了吧。
    philchang1995
        40
    philchang1995  
       2022-01-05 18:08:47 +08:00
    @whcoding 这种方案 redis 的集群和数据保持做的好的话还不错 如果不好的话数据一致性和数据丢失确实是个问题
    lu5je0
        41
    lu5je0  
       2022-01-05 18:13:08 +08:00
    update tb set order=xxx limit 1
    lipaa
        42
    lipaa  
    OP
       2022-01-05 18:22:18 +08:00
    @philchang1995 我们一开始就这么做的
    whcoding
        43
    whcoding  
       2022-01-05 18:24:59 +08:00
    @philchang1995 怕 redis 里的数据丢失?
    ETiV
        44
    ETiV  
       2022-01-05 18:26:22 +08:00 via iPhone   ❤️ 8
    做成队列,改发放机制成申请制,申请后提示用户“稍后将卡片以短信、邮件发送”

    然后慢慢消费队列就好了

    话说你们不怕灰产、羊毛党吗?
    philchang1995
        45
    philchang1995  
       2022-01-05 18:39:56 +08:00
    @whcoding 对啊😂
    philchang1995
        46
    philchang1995  
       2022-01-05 18:41:59 +08:00
    @lipaa 25 楼那种方案么?
    whcoding
        47
    whcoding  
       2022-01-05 18:43:59 +08:00
    @philchang1995 reids 挂掉? 丢数据 可能性不大吧 再说了还有持久化呢 不怕.....
    git00ll
        48
    git00ll  
       2022-01-05 18:55:35 +08:00
    for update 。简单 暴力。性能也不差
    zhoujinjing09
        49
    zhoujinjing09  
       2022-01-06 00:19:46 +08:00
    就正常分布式自增 ID 的解法,每个 ID 对应一个卡密不就行了吗……可能会有跳过但是无所谓吧,你们每天定时任务重新归档一下?
    ryd994
        50
    ryd994  
       2022-01-06 03:41:23 +08:00 via Android   ❤️ 1
    比起纠结这个,你更需要销售的日志。对库存表的每一个操作都应当有审计记录。这样就算碰到发错货或者无赖买家也有据可查。
    v2orz
        51
    v2orz  
       2022-01-06 08:33:27 +08:00
    单线程 消费队列下发
    Hug125
        52
    Hug125  
       2022-01-06 09:05:16 +08:00 via iPhone
    @ETiV #44 我司核心业务也是用消息队列异步处理保证数据准确性,就是需要注意消费者多实例部署时加分布式锁,避免两个实例同时消费同一条消息。OP 的业务场景做到准实时也够用了。
    NeezerGu
        53
    NeezerGu  
       2022-01-06 12:25:22 +08:00
    非技术人员好奇问问。
    能不能简单粗暴的吧所有卡密分成 10 分,放在 10 个 redis 分库里,对应 10 个取卡密进程(取卡时加锁排队)。
    取卡的时候做个负载均衡,保证高并发场景能扛得住就行
    印象里 redis 消耗资源不大,一台机开 n 个也行?当然如果内存不够 redis 可以换成 pika ?
    rekulas
        54
    rekulas  
       2022-01-07 00:01:58 +08:00
    @NeezerGu 单机的话 redis 主要瓶颈在内存,多开不能提高什么性能(我怀疑还会降低)
    如果是多机,那不如直接 redis 集群了,还不用考虑分配系统逻辑更简单
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1011 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 22:10 · PVG 06:10 · LAX 14:10 · JFK 17:10
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.