防止业务数据重复插入的另一种思路
开门见山
近日降雨颇多,偶然迸发灵感,对于防止重复插入这个问题想到了另一种解决方案(方案三)。
一个场景
电商项目中,一个商品可以绑定多个标签,一个标签可以绑定多个商品,所以肯定会存在一个中间表对商品和标签的关联,假设关联表goods_label
结构如下:
字段 | 类型 | 注释 |
---|---|---|
id | bigint | 主键 |
goods_id | bigint | 商品id |
label_id | bigint | 标签id |
is_delete | tinyint | 逻辑删除标识,1为已删除,2为未删除 |
A和B同时对商品做标签绑定的操作:
A -> 绑定商品1和标签1
B -> 绑定商品1和标签1
那么我们的期望是A和B的操作只会成功其一,另一个提醒他操作失败。
接下来将会有几种方案来达到我们想要的预期~
方案一:分布式锁
伪代码:
var goods_id = 1
var label_id = 1
var id = getId()
var count = select count(0) from goods_label where goods_id = $goods_id and label_id = $label_id and is_delete = 2
if count > 0{
return "已存在关联"
}
var l = lock.try(GOODS_LABEL_LOCK_KEY + goods_id)
if l != nil{
try{
var row = insert into goods_label(id, goods_id, label_id, is_delete) values ($id, $goods_id, $label_id, 2)
if row > 0{
return "成功"
}
return "失败"
} finally{
l.release()
}
}else{
return "操作超时"
}
这种很常用,也很普通...没有一丝灵魂
方案二:联合唯一索引
将goods_id
和label_id
以及is_delete
设为联合唯一索引,那么伪代码可以这样写:
var goods_id = 1
var label_id = 1
var id = getId()
var count = select count(0) from goods_label where goods_id = $goods_id and label_id = $label_id and is_delete = 2
if count > 0{
return "已存在关联"
}
var row = insert into goods_label(id, goods_id, label_id, is_delete) values ($id, $goods_id, $label_id, 2)
if row > 0{
return "成功"
}
return "失败"
代码简洁了不少,但是表的索引结构会复杂导致tps
下降,is_delete
字段也被限制了同一条记录只会存在1和2两个结果。
方案三:预删除(自命名)
var goods_id = 1
var label_id = 1
var id = getId()
var count = select count(0) from goods_label where goods_id = $goods_id and label_id = $label_id and is_delete = 2
if count > 0{
return "已存在关联"
}
var trans = beginTrans()
// 1
var row = insert into goods_label(id, goods_id, label_id, is_delete) values ($id, $goods_id, $label_id, 1)
if row > 0{
// 2
row = update goods_label set is_delete = 2 where id = $id and (select t.count from (select count(0) as count from goods_label where goods_id = $goods_id and label_id = $label_id and is_delete = 2) t) = 0
if row > 0{
trans.commit()
return "成功"
}
trans.rollback()
return "失败"
}
return "失败"
这种方案会执行两条操作事务sql:
- tag 1: 先插入数据,先将状态置为已删除。
- tag 2: 再将之前插入的数据状态更新为未删除,但是前提是当前关联不存在。
可以发现,如果tag 2执行失败,整个事务会回滚掉,那么tag 1的操作也会撤销,所以也不会产生脏数据。
方案对比
- 方案一: 传统,但很实用,没有灵魂。
- 方案二: 局限性太大,如果唯一的条件太多会导致索引更加复杂,另外is_delete有些场景也可能允许同类资源删除多次,这样的话无法达到唯一索引的效果。优点是业务代码很简洁!
- 方案三: 本人偶尔突发奇想,不知道好坏,但是能达到目的 xD
转载自:https://juejin.cn/post/6844903936252444685