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

求教一个高并发插入数据幂等的问题

  •  1
     
  •   wym7223645 · 2019-09-23 19:24:08 +08:00 · 2664 次点击
    这是一个创建于 1674 天前的主题,其中的信息可能已经有所发展或是发生改变。

    因为业务十分复杂,简化描述问题如下

    假设现在有一个 A 表,字段如下 ID 主键,自增 BILL_NO 单据编号 BILL_NAME 单据名称 BACKUP_COLUMN 备用列,用于存储外部系统传递的单据编号 TASK_TYPE 数据类型 …… 其余字段

    接口:

    外部系统传递 BACKUP_COLUMN,根据 BACKUP_COLUMN 从我们系统的 B、C 表中查询数据拼接并插入 A 表。 此类接口进入 A 表的时候 TASK_TYPE 为 002,且只有通过这个接口进来的是 002

    问题:

    此接口因为外部系统原因经常会在几毫秒甚至几纳秒内重发 N 次,导致 A 表出现重复数据

    目前解决思路

    方式 1:

    一开始打算 BACKUP_COLUMN 加唯一约束,但是因为这个表还存其他业务数据,其他数据中 BACKUP_COLUMN 的值允许重复(例如 TASK_TYPE 为 001 时 BACKUP_COLUMN 允许为空或者重复),改动代价太大,时间及人员上不允许,故此放弃增加唯一约束的方式

    方式 2:

    在插入之前做一次查询,如果存在则不插入,本地及测试环境简单测试发现拦截住了,就部署到了生产环境 上了生产环境之后发现,还是有重复数据,经过查询日志发现,对方系统多次重复请求在几毫秒甚至及纳秒之内(数据库日志只记录到秒,最后还是通过打印时间发现的时间这么短) 这么短的间隔导致了第一次请求的数据还没执行到 inser 之前第二个请求就进来了,此时第二个请求的查重还是没有数据,最终第一次、第二次都插入成功。 这种情况只适用于两次重发间隔不是很短的情况

    方式 3:

    基于方式 2 的拦截失败,我们在数据插入成功之后,根据 BACKUP_COLUMN、TASK_TYPE 进行一次查重,如果有多个,保留时间最早的一个,删除(业务回退)其余重复数据,暂时没发现问题

    ** **

    目前我们是通过方式 2 和方式 3 结合的方式保证数据不重复推送 因为现在方式 3 中的删除(回退)逻辑并不复杂,但是之后这里逻辑会变的超级复杂,或者出现复杂接口,个人个感觉方式 3 并不是很合适

    请问各位大佬是否还有其他方式保证接口的幂等性或者说让重复数据不入库

    11 条回复    2019-09-24 17:58:52 +08:00
    killergun
        1
    killergun  
       2019-09-23 22:14:29 +08:00
    感觉使用 Redis 锁最简单粗暴
    reus
        2
    reus  
       2019-09-23 22:33:11 +08:00
    如果数据库不是 MySQL,可以用条件性唯一索引,也就是符合 where 子句的行,才增加唯一索引。
    用 MySQL 的话,就没有这个功能了。
    hspeed18
        3
    hspeed18  
       2019-09-23 23:33:40 +08:00
    联合索引不行吗?
    intermole
        4
    intermole  
       2019-09-23 23:40:34 +08:00 via iPhone
    联合索引➕1
    Vegetable
        5
    Vegetable  
       2019-09-23 23:51:41 +08:00
    同意 1 楼,可以加一个中间层过滤数据.两次间隔时间这么近不要忘了检查是不是系统有 bug.感觉用单纯用数据库解决,对数据库压力比较大一点.插入的时候检查索引有一点点违和.如果量级不大倒是没什么.
    aliipay
        6
    aliipay  
       2019-09-24 01:12:44 +08:00
    BACKUP_COLUMN 如果能建索引的话,直接用悲观锁,两个事务同时执行时候有一个会触发 deadlock 退出事务,一个会成功写入, 避免其它逻辑导致死锁误判,可以在延时一小段时间后查询再决定是否重新写入数据库
    lihongjie0209
        7
    lihongjie0209  
       2019-09-24 08:58:29 +08:00
    把请求放入队列中,单线程读取,查询数据库, 插入
    wym7223645
        8
    wym7223645  
    OP
       2019-09-24 09:56:58 +08:00
    @killergun 我们这个项目的机房在北京,已经没有地方部署 Redis 了,想部署 Redis 需要部署到西安的机房,两边有延迟还挺厉害

    @reus 我们是 DB2 的库,简单搜了一下没发现条件性唯一索引,回头我再搜搜

    @Vegetable 的确可以增加一个中间表,数据线入中间表,然后再插入正式表。
    两次间隔这么近的确不正常,找了对端系统他们说使用了某消息中间件,中间件有问题导致,但是中间件在甲方手里维护,甲方不解决,对端系统也不想通过程序解决,所以只能我们来想办法解决了
    ivyxjc
        9
    ivyxjc  
       2019-09-24 10:30:36 +08:00 via Android
    最简单的方法就建一张有唯一索引的中间表,不需要额外的服务,也不用改框架。

    一定要先把数据重复和时序问题解决了再做业务,否则业务一复杂,就很难写,很难测了。
    justRua
        10
    justRua  
       2019-09-24 11:19:53 +08:00
    方式 2 出现的问题是 写偏差,用可序列化的事务级别可以避免。或加一张辅助表,也可以加一辅助列(值为 TASK_TYPE TASK_TYPE +BACKUP_COLUMN,使其有唯一性,然后加上唯一索引)。
    wym7223645
        11
    wym7223645  
    OP
       2019-09-24 17:58:52 +08:00
    @justRua 貌似只能加辅助表,我们是集群部署,事务单机可以,集群下负载到不同的集群就不行了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3286 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 13:26 · PVG 21:26 · LAX 06:26 · JFK 09:26
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.