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

求助一个高并发的数据校验与保存问题

  •  
  •   mercurius · 2023-03-11 18:48:14 +08:00 · 2185 次点击
    这是一个创建于 624 天前的主题,其中的信息可能已经有所发展或是发生改变。
    背景:
    1 、一个商品对应着多个 SKU 数据,它们是一个整体
    2 、SKU 是自定义输入的,但相同店铺下 SKU 不能重复
    3 、因为其他业务逻辑,不能从数据库层面加唯一索引

    流程:
    商品数据在保存前,会先校验数据,查询数据库,确定相同店铺不存在重复 SKU 后才给通过

    伪代码:
    {
    // 数据校验,需查询数据库进行判断
    check();

    // 数据保存
    save();
    }

    问题:
    校验与保存之间存在极短的时间差,高并发下,存在相同 SKU 在该时间差内通过校验,最终导致插入重复 SKU 数据。

    例子:
    商品 A ,其 SKU 有 S1 、S2 、S3
    商品 B ,其 SKU 有 S3 、S4 、S5
    它们恰好在同一时间进行校验,并且通过了校验,到时候该店铺就会出现 S2 的重复 SKU

    自己想的解决方案:
    使用 Redisson 的 RSet.add 方法可以在添加时就判断是否重复,如果添加失败就说明存在上面描述的情况,直接校验失败。
    在查询数据库后,使用 RSet 去缓存这些 SKU ,等到保存成功再删掉缓存,同时为防止服务出问题,设置下 set 的过期时间。

    但这种解决方案,会在短时间内连续调用 RSet.add ,单个商品的次数可能是 1~200 ,考虑到网络开销,感觉性能应该会很差吧……
    大佬们有没有支持在短时间内大量去重并且支持过期 /删除的东西? orz
    布隆过滤器已经被 pass 了,它不支持元素的删除
    21 条回复    2023-03-12 15:07:15 +08:00
    546L5LiK6ZOt
        1
    546L5LiK6ZOt  
       2023-03-11 19:01:12 +08:00
    Redisson 分布式锁不可以吗,对商品进行加锁
    hhjswf
        2
    hhjswf  
       2023-03-11 19:04:59 +08:00 via Android
    搞不懂,就特么一个 sku 管理也要高并发吗。。很多个店铺管理员在疯狂添加 sku 吗。。
    Chad0000
        3
    Chad0000  
       2023-03-11 19:10:36 +08:00 via iPhone
    你都高并发时检测数据库了,那么就算 redis 挂也是在数据库之后。

    所以多虑了。就用 redis 没什么的。
    alexleee
        4
    alexleee  
       2023-03-11 19:10:39 +08:00
    用分布式缓存,以店铺 ID+SKU 为 key 弄个分布式锁,插入的时候先拿到锁,拿到锁的线程去 check()+save()?
    kwh
        5
    kwh  
       2023-03-11 19:12:10 +08:00
    redis 自增,通过校验就自增,首次自增会是 1 ,非首次自增就会大于 1 了。
    而且商户保存操作,MySQL 行锁应该也行吧?
    mercurius
        6
    mercurius  
    OP
       2023-03-11 19:36:01 +08:00
    @546L5LiK6ZOt 有考虑过,但要解决该问题,锁只能加在店铺维度,粒度有点大

    @hhjswf 用爬虫爬别人商品,然后批量创建到自己店铺,那个内部服务直接用多线程打过来时发现的问题……

    @Chad0000 但检测数据库那里只是一条 SQL ,而这里校验时连续调用 200 次 RSet.add ,这个性能会不会有点夸张?我对这块没啥概念……

    @alexleee 有考虑过,但这个锁是 SKU 维度,那加锁次数是跟 SKU 数量一致的,而商品与 SKU 是一对多的整体关系,不能说一个个 SKU 单独校验和保存,所以单个商品可能加上几十甚至上百个锁……

    @kwh 自增是商品通过校验,还是 SKU 通过校验?它们是一个整体的,并且是一对多的关系……MySQL 行锁应该不行吧,问题不是保存操作,而是校验操作那里的高并发
    alexleee
        7
    alexleee  
       2023-03-11 19:57:16 +08:00
    @mercurius 你的外键约束是商品 id+SKU 这两个的组合呀,我理解你说的怕竞争条件下出问题的资源不就是店铺 id+sku 的创建权吗,你又怕不同线程创建太多锁...那你牺牲一点并发性能锁 sku 或者锁店铺吧...
    leonshaw
        8
    leonshaw  
       2023-03-11 20:03:45 +08:00
    要强一致就要加锁(或者等效于加锁的操作)。考虑到 IO RT ,可以按店铺分区,实现一个批量加锁的接口。不知道有没有现成的轮子。
    mercurius
        9
    mercurius  
    OP
       2023-03-11 20:37:42 +08:00
    @alexleee 锁店铺粒度大但只锁一个,锁 sku 粒度小但需要连续锁多个(假设按最差的情况为 200 ),我还没实际遇过连续锁这么多次只为单个商品的创建,总觉得不太对劲,这性能正常吗……

    @leonshaw 对,我那个方案其实就相当于等效的加锁操作,但不确定这种单次操作中,包含了几十甚至上百次 redis 请求的性能如何
    546L5LiK6ZOt
        10
    546L5LiK6ZOt  
       2023-03-11 20:57:11 +08:00
    几十、上百次 redis 请求不算高,腾讯云最低配的 redis 也能支持上万 qps
    546L5LiK6ZOt
        11
    546L5LiK6ZOt  
       2023-03-11 20:59:38 +08:00
    我觉得严谨的做法还是得在数据库用唯一键约束来做。原先的表不能加唯一键,那就新增一个表,商品和 sku 作为唯一键,在一个事务里插入。用分布式锁不能保证 100% 一致的。
    k9982874
        12
    k9982874  
       2023-03-11 21:04:13 +08:00 via Android
    sku 生成规则制定好,根本不需要去考虑冲突问题。
    例如商品 sku 由商铺 uid 为前缀+品类 uid+时间戳+用户自定义输入组成,基本上就很难重复
    xwayway
        13
    xwayway  
       2023-03-11 21:05:01 +08:00 via iPhone
    那要不你扔 mq ?慢慢去保存就是咯
    mercurius
        14
    mercurius  
    OP
       2023-03-11 21:45:18 +08:00
    @546L5LiK6ZOt 是的,最纯粹的做法就是唯一索引,只是原先表的业务逻辑不支持……不过确实新增一个中间表就能解决,并且比起各种加锁要简单得多,操作只需要批量插入和删除,性能也只是多一两次 SQL 操作。非常感谢,这是新思路!

    @k9982874 背景就是 sku 只能是商家自定义的,要原模原样,不能再在上面改东西……

    @xwayway 这种操作是会同步返回结果的,从同步改成异步,这逻辑变化有点大了
    k9982874
        15
    k9982874  
       2023-03-11 21:50:06 +08:00 via Android
    @mercurius 非要用锁,11 楼已经给答案了,建一张外键表,sku 加唯一约束,先插入这个表,成功后再插入商品表。
    mercurius
        16
    mercurius  
    OP
       2023-03-11 21:55:36 +08:00
    @k9982874 是的,11 楼的答案是之前没想到的新思路,完全可行且不复杂,正是我想要的那种

    感谢上述各位的讨论与回复 _(:з」∠)_
    potatowish
        17
    potatowish  
       2023-03-11 23:34:14 +08:00 via iPhone
    加一个映射表,包含自增 ID 、商铺 UID 、商品品类 ID 、用户自定义 SKU 。自增 ID 作为实际的 SKU ,在业务中使用到 SKU 时只需要查询它对应的实际 SKU 。
    xiaop1ng
        18
    xiaop1ng  
       2023-03-12 00:45:21 +08:00 via iPhone
    @546L5LiK6ZOt 这个方法算是基于数据库的一种分布式锁吧,我认为和基于 redis 对商品 id+sku 上分布式锁的逻辑是一样的,流程变为 lock->check->save->unlock ,不明白为什么分布式锁不能保证 100%一致,想讨论一下
    546L5LiK6ZOt
        19
    546L5LiK6ZOt  
       2023-03-12 01:18:17 +08:00
    @xiaop1ng

    参考这里 https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html

    GC 的 STW 可能会导致锁过期了,但是进程还认为占用锁。即使用没有 GC 的语言,操作系统的进程调度也可能会出现这种情况。理论上来说,分布式锁不可能保证完全一致的。还是得靠底层数据库
    bushenx
        20
    bushenx  
       2023-03-12 03:14:21 +08:00 via Android
    如果 check 只是一条 sql 且场景需要强一致,不如直接上事务吧,select 语句直接上锁。
    xiaop1ng
        21
    xiaop1ng  
       2023-03-12 15:07:15 +08:00
    @546L5LiK6ZOt 了解,thanks~
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1167 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 18:08 · PVG 02:08 · LAX 10:08 · JFK 13:08
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.