如何防止数据重复插入? - 泥瓦匠BYSocket

点击蓝色“泥瓦匠BYSocket”,关注我哟

加个星标”,不忘文末签到哦

作者:泥瓦匠@bysocket.com

目录

  1. 为啥要解决数据重复插入?
  2. 解决方案实战
  3. 可落地小总结

一、为啥要解决数据重复插入?

问题起源,微信小程序抽风 wx.request() 重复请求服务器提交数据。后端服务也很简单,伪代码如下:

  1. class SignLogService {
  2. public void saveSignLog(SignLogDO log) {
  3. // 简单插入做记录
  4. SignLogDAO.insert(log);
  5. }
  6. }

发现数据库会存在重复数据行,提交时间一模一样。但业务需求是不能有多余的 log 出现,这明显是个问题。

问题是,重复请求导致的数据重复插入。这问题造成的后果很明显:

  • 数据冗余,可能不单单多一条
  • 有些业务需求不能有多余数据,造成服务问题

问题如图所示:

解决方式:如何将 同请求 A,不执行插入,而是读取前一个请求插入的数据并返回。解决后流程应该如下:

二、解决方案实战

1.单库单表解决方案

  • 唯一索引 + 唯一字段
  • 幂等

上面说的那种业务场景:signlog 表会有 userid、signid、signtime 等。那么每次签到,每个人每天只有一条签到记录。

数据库层采取唯一索引的形式,保证数据记录唯一性。即 UNIQUE 约束,UNIQUE 约束唯一标识数据库表中的每条记录。另外,userid,signid,sign_time 三个组合适唯一字段。创表的伪代码如下:

  1. CREATE TABLE sign_log
  2. (
  3. id int NOT NULL,
  4. user_id int NOT NULL,
  5. sign_id int,
  6. sign_time int,
  7. CONSTRAINT unique_sign_log UNIQUE (user_id,sign_id,sign_time)
  8. )

重点是 CONSTRAINT unique_sign_log UNIQUE(user_id,sign_id,sign_time)。有个小问题,数据量大的时候,每条记录都会有对应的唯一索引,比较耗资源。那么这样就行了吗?

答案是不行,服务不够健壮。第一个请求插入成功,第二个请求直接报错,Java 服务会抛出 DuplicateKeyException 。

简单的幂等写法操作即可,伪代码如下:

  1. class SignLogService {
  2. public SingLogDO saveSignLog(SignLogDO log) {
  3. // 幂等处理
  4. SignLogDO insertLog = null;
  5. try {
  6. insertLog = signLogDAO.insert(log);
  7. } catch (DuplicateKeyException e) {
  8. insertLog = selectByUniqueKeys(userId,signId,signTime);
  9. }
  10. return insertLog;
  11. }
  12. }

的确,流量不是很大,也不算很高并发。重复写问题,这样处理即可。那大流量、高并发场景咋搞

2.分库分表解决方案

流量大了后,单库单表会演变成分库分表。那么基于单表的唯一索引形式,在碰到分表就无法保证呢,插入的地方可能是两个分表 A1 和 A2。

解决思路:将数据的唯一性条件放到其他存储,并进行锁控制

还是上面的例子,每天,每次签到,每个人只有一条签到记录。那么使用分布式锁 Redis 的解决方案。大致伪代码如下:

a.加锁

  1. // 加锁
  2. jedis.set(lockKey, requestId, "NX", "PX", expireTime);
  • lockKey 最简单的是 userid + signid + sign_time
  • expireTime 设置为一天

b.解锁

  1. // 解锁
  2. jedis.eval(script, lockKey,requestId);

c.幂等代码加强

  1. class SignLogService {
  2. public SingLogDO saveSignLog(SignLogDO log) {
  3. // 幂等校验
  4. SignLogDO existLog = selectByUniqueKeys(userId,signId,signTime);
  5. if(Objects.nonNull(existLog)) {
  6. return existLog;
  7. }
  8. // 加锁
  9. jedis.set
  10. SignLogDO insertLog = signLogDAO.insert(log);
  11. // 解锁
  12. jedis.eval
  13. return insertLog;
  14. }
  15. }

这个方案还是不是很成熟,大家参考下即可。

三、可落地小总结

解决方案实战中,了解具体术。归纳如下:

  • 幂等:保证多次同意请求后结果一致
  • 并发控制:单表唯一索引、分布式多表分布式锁
  • 降级兜底方案:分布式锁锁失效 - 考虑乐观锁兜底

参考资料

以下专题教程也许您会有兴趣

由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!

-The End-

号外:为读者持续几份最新教程,覆盖了 Spring Boot、Spring Cloud、微服务架构等。

获取方式:下面关注公众号,并回复 java 或 666领取

最新整理:下面关注公众号,并回复 webflux 或 888领取《Spring Boot WebFlux 必会必知系列教程》

 热门文章:

长按二维码,扫扫关注哦

✬关注即可得 Spring Boot Cloud、微服务等干货✬

文末签到打卡

如果文章对你有帮助的话

“在看”,转发朋友圈**

↓↓↓↓


Original url: Access
Created at: 2019-04-12 14:23:39
Category: default
Tags: none

请先后发表评论
  • 最新评论
  • 总共0条评论