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

2023-03-11 18:48:14 +08:00
 mercurius
背景:
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 了,它不支持元素的删除
1944 次点击
所在节点    程序员
21 条回复
546L5LiK6ZOt
2023-03-11 19:01:12 +08:00
Redisson 分布式锁不可以吗,对商品进行加锁
hhjswf
2023-03-11 19:04:59 +08:00
搞不懂,就特么一个 sku 管理也要高并发吗。。很多个店铺管理员在疯狂添加 sku 吗。。
Chad0000
2023-03-11 19:10:36 +08:00
你都高并发时检测数据库了,那么就算 redis 挂也是在数据库之后。

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

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

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

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

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

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

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

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

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

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

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

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

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

© 2021 V2EX