此文为转载,先说一个我在生产上真实存在的案例
业务场景:我们做的是仓库管理系统,产线上需要零件的时候,首先生成一个拉动,我需要把拉动,生成拣货单,然后根据拣货单去拣货,上线。
操作流程是:在web上选择多个拉动单,点击组单按钮,即可生产拣货单。
但是存在这样一个问题:原来拉动单:拣货单 1:1,结果一个拉动单生成了多个拣货单,且生成的拣货单创建时间完全一样,但是操作人,不完全一样。
分析原因:
1,一个人连续点击组单按钮,导致重复请求后台接口。
2,两个人在不同电脑同时组单,也就是存在并发操作,这也是为什么生成拣货单的创建人不同。
解决方案:
1,前端控制:针对原因1,前端可做防重复提交控制。
2,加锁:使用redis对拉动单加锁,过滤请求的拣货单。即先对请求参数校验,判断是否已加锁,是过滤掉,否保存并加锁。
3,唯一索引:对拣货单中拉动单号加唯一索引,如果已经组过单,报错,写入失败。
3,幂等:新增之前,根据拉动单号查询拣货单,存在就不新增。
但是通过上面的控制,一定程度上减少的重复组单的数据,后来在生产上还有重复组单的情况。
至今不知道原因,和解决方案,请看到此篇文章的大神指点
qq:1941599064
电话:18298182575
--------------------------------------------------以下为装载内容----------------------------------------
解决方案:如何防止数据重复插入?
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢!
目录
- 为啥要解决数据重复插入?
- 解决方案实战
- 可落地小总结
一、为啥要解决数据重复插入?
问题起源,微信小程序抽风 wx.request() 重复请求服务器提交数据。后端服务也很简单,伪代码如下:
2 |
public void saveSignLog(SignLogDO log) { |
4 |
SignLogDAO.insert(log); |
发现数据库会存在重复数据行,提交时间一模一样。但业务需求是不能有多余的 log 出现,这明显是个问题。
问题是,重复请求导致的数据重复插入。这问题造成的后果很明显:
- 数据冗余,可能不单单多一条
- 有些业务需求不能有多余数据,造成服务问题
问题如图所示:
解决方式:如何将 同请求 A,不执行插入,而是读取前一个请求插入的数据并返回。解决后流程应该如下:
二、解决方案实战
1.单库单表解决方案
上面说的那种业务场景:sign_log 表会有 user_id、sign_id、sign_time 等。那么每次签到,每个人每天只有一条签到记录。
数据库层采取唯一索引的形式,保证数据记录唯一性。即 UNIQUE 约束,UNIQUE 约束唯一标识数据库表中的每条记录。另外,user_id,sign_id,sign_time 三个组合适唯一字段。创表的伪代码如下:
7 |
CONSTRAINT unique_sign_log UNIQUE (user_id,sign_id,sign_time) |
重点是 CONSTRAINT unique_sign_log UNIQUE (user_id,sign_id,sign_time)
。有个小问题,数据量大的时候,每条记录都会有对应的唯一索引,比较耗资源。那么这样就行了吗?
答案是不行,服务不够健壮。第一个请求插入成功,第二个请求直接报错,Java 服务会抛出 DuplicateKeyException
。
简单的幂等写法操作即可,伪代码如下:
01 |
class SignLogService { |
02 |
public SingLogDO saveSignLog(SignLogDO log) { |
04 |
SignLogDO insertLog = null; |
06 |
insertLog = signLogDAO.insert(log); |
07 |
} catch (DuplicateKeyException e) { |
08 |
insertLog = selectByUniqueKeys(userId,signId,signTime); |
的确,流量不是很大,也不算很高并发。重复写问题,这样处理即可。那大流量、高并发场景咋搞
2.分库分表解决方案
流量大了后,单库单表会演变成分库分表。那么基于单表的唯一索引形式,在碰到分表就无法保证呢,插入的地方可能是两个分表 A1 和 A2。
解决思路:将数据的唯一性条件放到其他存储,并进行锁控制
还是上面的例子,每天,每次签到,每个人只有一条签到记录。那么使用分布式锁 Redis 的解决方案。大致伪代码如下:
a.加锁
2 |
jedis.set(lockKey, requestId, "NX", "PX", expireTime); |
- lockKey 最简单的是 user_id + sign_id + sign_time
- expireTime 设置为一天
b.解锁
2 |
jedis.eval(script, lockKey,requestId); |
c.幂等代码加强
01 |
class SignLogService { |
02 |
public SingLogDO saveSignLog(SignLogDO log) { |
05 |
SignLogDO existLog = selectByUniqueKeys(userId,signId,signTime); |
06 |
if(Objects.nonNull(existLog)) { |
13 |
SignLogDO insertLog = signLogDAO.insert(log); |
这个方案还是不是很成熟,大家参考下即可。
三、可落地小总结
解决方案实战中,了解具体术。归纳如下:
- 幂等:保证多次同意请求后结果一致
- 并发控制:单表唯一索引、分布式多表分布式锁
- 降级兜底方案:分布式锁锁失效 – 考虑乐观锁兜底
参考资料
- 重复插入方案: http://www.bysocket.com/archives/2266
- 《阿里巴巴 Java 开发手册》
以下专题教程也许您会有兴趣
- 《Spring Boot 2.x 系列教程》
- 《Java 核心系列教程》