V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
felo
V2EX  ›  程序员

代金券锁券优化方式

  •  
  •   felo · 31 天前 · 1816 次点击

    有一个用券逻辑,券有金额/剩余金额属性。并发场景有很多请求使用同一张券,我用到了 select for update 的方式进行锁券,当一个请求消耗完券之后,update 记录(券剩余金额),然后会释放锁,下一个请求再继续用券。

    但现在同一张券,券金额比较大且并发场景的情况下,某些请求耗时会比较长,因为等待锁释放

    请教下,这里有什么优化点么,合并请求之类的改动太大,需要上游配合

    第 1 条附言  ·  31 天前
    我说的场景是类似那种后付费的场景,会定时扣费,所以会有同一个用户同一张券并发使用的情况
    32 条回复    2024-11-27 12:36:04 +08:00
    yankebupt
        1
    yankebupt  
       31 天前
    我完全不懂,但是按金额切分券,分别锁,闲时再合并?
    felo
        2
    felo  
    OP
       31 天前
    @yankebupt 可能没说清
    1. 券可以用多次
    2. 用券请求是并发的
    所以会出现一张券要被并发使用多次的情况。因为要修改剩余金额,所以需要给券加锁,但加了锁之后,等待锁释放的请求又会慢一些
    yiqiao
        3
    yiqiao  
       31 天前
    券不是发放到个人账户上然后消费吗? OP 问的是发的券超库存了吗?
    InDom
        4
    InDom  
       31 天前   ❤️ 1
    券余额也是余额, 所以你要问的是并发余额怎么减.

    问题是单用户会有并发么?
    如果是全局有一个“该券能用多少次、最多全局抵多少钱”.

    那就是减库存问题了.

    最好的就是落到缓存,然后用队列按顺序处理.
    yankebupt
        5
    yankebupt  
       31 天前
    当用户从页面点选那张优惠券还没有提交时,立刻把券切成两张,并锁住相应金额的那张,如果用户提交,就更新,如果不更新页面关闭了,把两张券再合起来......
    yankebupt
        6
    yankebupt  
       31 天前
    或者你也可以提供一个功能让用户自己切然后一张一张用(很丑陋,但代码最少
    byehair
        7
    byehair  
       31 天前   ❤️ 1
    1 、内存队列(或者 MQ ),排队对单券扣减,减少无畏的 CPU 消耗、加解锁消耗
    2 、使用 Redis 的 decrby 进行扣减,通过内存扣减提升性能
    3 、如果是多次的金额相同,还可以使用券预置拆分策略,减少锁竞争
    lcy630409
        8
    lcy630409  
       31 天前
    就是类似京东的 e 卡 说是卷 就是一个单独的存储余额
    clysto
        9
    clysto  
       31 天前
    用 redis 缓存剩余金额,直接在 redis 中 decrby 可行吗?
    yankebupt
        10
    yankebupt  
       31 天前
    楼上说的 MQ 比较靠谱。如果还没屎山堆到动不了数据结构,可以考虑
    esee
        11
    esee  
       31 天前
    你 update 剩余金额一定要放在处理完业务逻辑的事务的最后么,可不可以加锁后就 update 金额 释放锁,然后去处理其他业务逻辑,如果处理出错,订单失败,则回滚剩余金额。。这样慢不了多少。。
    felo
        12
    felo  
    OP
       31 天前
    @byehair 感谢,我思考下
    yinmin
        13
    yinmin  
       31 天前   ❤️ 1
    创建事务,直接跑 sql:
    update tblStoredValueCard set fldBalance=fldBalance-@value where fldCardID=@id and fldBalance>=@value
    然后获取变更记录数,如果变更记录为 0 条就回滚,如果>0 就写其它表,然后 commit trans
    lyxxxh2
        14
    lyxxxh2  
       31 天前
    赞同 11 楼。

    读写锁 -> 读取数据库为变量 -> 修改 -> 解锁。 # 也就这部分会影响到其他用户
    time.sleep(.5)# 业务逻辑
    except Exception as e:
    加读写锁,回滚。 (怕其他请求读取为变量,所以读也锁)

    ps: 似乎有点不对,"a 请求"回滚,"b 请求"的内存变量又不会改。 (不过很难发生)
    yinmin
        15
    yinmin  
       31 天前
    @felo 接#13 ,没有显性锁,使用 update 的数据一致性的特性,对并发影响最小吧。
    yinmin
        16
    yinmin  
       31 天前
    “显性锁+事务处理”会扼制脏记录特性,可能会严重影响数据库性能
    felo
        17
    felo  
    OP
       31 天前
    @esee 业务逻辑现在也不是很复杂,就是锁了之后写了用券记录,更新金额。写用券记录后置也是准备优化的点
    felo
        18
    felo  
    OP
       31 天前
    @yinmin 感谢我也看下
    esee
        19
    esee  
       31 天前
    @felo 个人感觉首先在处理逻辑上优化会比较好,像我刚刚说的加锁后立刻 update 后释放延迟不了多少,贸然引入第三方组件比如消息队列会增加业务复杂度,久而久之就变屎山了,尤其是系统本来就没用到消息队列的情况下。。。
    yinmin
        20
    yinmin  
       31 天前   ❤️ 1
    @felo 一个事务会有多条 sql 交互,应用服务器与数据库服务器交互 1 次至少 1-2ms ,如果一个事务有 5 次 sql 交互,总耗时 6-10ms ,加锁的情况下 100 次事务处理就累积耗时 1 秒+,感知明显。优化方式是用数据库的存储过程,减少应用服务器与数据库服务器的网络延时。
    felo
        21
    felo  
    OP
       31 天前
    @esee 对,我个人倾向也是不想用别的组件,链路越复杂问题越多。看了大家回复我感觉可以用乐观锁代替 select for update ,但乐观锁失败之后的重试处理以及如何保证请求顺序还得再看看
    xuelu520
        22
    xuelu520  
       31 天前
    你对券的基础理解是不是错了?
    看你说的是所有人共用一张券,实际你得每个人都发一张券或领一张,然后对券进行核销
    felo
        23
    felo  
    OP
       31 天前
    @xuelu520 看我的附言哈,不同的业务场景。券怎么用要看业务逻辑,我没说过所有人共用一张。
    esee
        24
    esee  
       31 天前
    @felo 可是你的场景就是高并发的情况,感觉乐观锁并不适合,如果冲突会产生大量重试,数据库负担会加重,select for update 感觉就够了
    felo
        25
    felo  
    OP
       31 天前
    @esee 嗯理解,锁券到更新金额中间只有一个写用券记录,这个我后置试试
    admin7785
        26
    admin7785  
       31 天前 via iPhone
    看起来有点像预算池的场景:比如:发了 100 张券,总共 100 元的预算,每个人都能用,先到先得,预算用完即止。
    ucando
        27
    ucando  
       30 天前
    有点不是太理解需求,如果是后付费,是不是可以理解成用户有多个需要扣费的业务,然后在某一个时间点需要自动扣费?以我的理解这部分并不需要并行处理,因为不是用户通过多个客户端同时操作进行的,直接照#13 楼的处理方式就行了。如果需要返回余额,还可以使用 returning fldBalance 来获取最新的余额,select 都省了
    INCerry
        28
    INCerry  
       30 天前
    用 actor 模式,每个券都单线程处理
    asAnotherJack
        29
    asAnotherJack  
       30 天前
    现在慢是因为有大事务吗,如果是的话可以考虑把大事务拆成多个小事务,再弄个状态机进行处理,比如分成多个步骤,扣减优惠券、扣减余额、其他业务逻辑 abc…,完成一个操作后就更新为另一个状态,通过状态流转,一步一步操作,某一步出错时区分错误类型决定是重试还是回滚之前的操作进入失败状态。这样每一个操作都耗时不长,不会因为操作优惠券的一个锁拖慢其他步骤。另外注意做好幂等,尤其是失败回滚时,不要退费退多了
    realpg
        30
    realpg  
       29 天前
    @yankebupt #10
    MQ 真的不适合用来做涉钱的账务操作
    没故障时候什么都好
    有故障就火葬场,甚至监狱
    yankebupt
        31
    yankebupt  
       29 天前
    @realpg 所以说得动数据结构么。
    都不能包在一个 Transcation 里,一定得加个状态等钱扣完,扣了才继续……
    有点脱裤子放屁,但是没办法,人家要并发
    realpg
        32
    realpg  
       29 天前
    @yankebupt #31
    处理过程唯一话后设计好可以利用 MQ 但是不能分解步骤放到不可靠的 MQ 里面 大部分用 MQ 的都是设计不到位

    涉及金钱最好是多账本完全一致才能操作 然后确保处理队列的唯一性 各种异常要充分照顾到并且能自动化处理

    如果系统或者数据库是分布式的 就更麻烦 这个真的不太建议自己能力不足单纯靠问的去设计架构

    如果评估自己能力不足设计这个架构,就简单化成单处理逻辑

    你这种业务 基本就是阿里云的通用储值卡 完全一摸一样的逻辑

    跟京东 E 卡也有点区别 因为 E 卡是主动使用 顶天多人登陆账号同时使用
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3448 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 10:49 · PVG 18:49 · LAX 02:49 · JFK 05:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.