INSERT ON DUPLICATE KEY UPDATE返回值引起的小乌龙

news2024/11/16 16:37:23

一、东窗事发

某个版本送测,测试大佬给提了一个缺陷,且听我描述描述:

  • 一个学习任务:

  • 两个一模一样的学习动态:

  • 产品定义:学习任务(生字学习)完成后,会在小程序生成一个动态,再次完成不重复生成

obviously,上边出现的两个动态不符合“罗辑”

二、排查看看

既然出现了两个动态,那就来看看动态的源头是不是生成了两个

1.先看动态生成的触发点

下边为简化版的伪代码

// student_id - learn_type 是唯一索引
val rt = execSql(
"""
INSERT INTO t_learn_state (student_id, learn_type, create_time, update_time)
VALUES ("bc6b5e6979af11e8a10c1c1b0d1c49aa",10,now(),now()) 
ON DUPLICATE KEY UPDATE update_time = now()
""")
//判断是否首次完成
if (rt == 1) {
    //发送完成MQ消息
    sendMq("StudyXXXTopic","ResourceFinish","bc6b5e6979af11e8a10c1c1b0d1c49aa finish 10")
}
复制代码

看起来没什么问题,难道是mq重复消费了?

2.看看mq的消息情况

直接看mq后台的消息记录:

可见有两条时间非常接近的mq消息,展开消息后发现内容是一致的,也就是重复生成了消息,而不是重复消费,怎么会这样呢?难道唯一索引没创建?

3.看看表中有多少条记录

only one!

也就是唯一索引是生效的,表中确实只有一条记录

4.难道对 INSERT ON DUPLICATE KEY UPDATE 理解有误?

首先确认了jdbc的链接参数并没有使用useAffectedRows=true,也就是该sql的返回值是matched的行数!(后文也是基于此进行的分析)
再经过几番搜索及请教大佬,返回值确实是这样子的:

  • 如果是新插入的记录,那么返回值是1
  • 如果发生了唯一键冲突并更新了记录,那么返回值是2

如此这般,那问题到底出在哪了呢?

4.还是复现看看吧!

找了下对应时间的流量,符合的其实就两条:

经过代码排查,这两个请求最终都会执行到上边的伪代码,在清除数据并重放几次之后,复现了!

5.结果都是1

经过debug发现,两次execSql的结果都是1,通过mybatis的日志也能拿到准确的sql,如下:

//两次非常临近执行的sql是一模一样的,并且rt都是1
val rt = execSql(
"""
INSERT INTO t_learn_state (student_id, learn_type, create_time, update_time)
VALUES ("bc6b5e6979af11e8a10c1c1b0d1c49aa",10,now(),now()) 
ON DUPLICATE KEY UPDATE update_time = now()
"""
)
复制代码

但是为什么两次执行的结果都是1呢?按照说明应该是第一次为1,第二次应该会触发唯一键冲突而导致更新进而返回2才对呀!

6.恍然大明白

将sql拿到idea中执行,也能够复现,多执行几次,终于发现了华点:

在同一秒内执行的多次该sql,其返回值都是1,跨秒之后则会出现一次2

再喵一眼表结构:

CREATE TABLE `t_learn_state`
(
    `id`            int(11) unsigned    NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `student_id`    char(32)            NOT NULL COMMENT '学生id',
    `learn_type`    int(4)              NOT NULL COMMENT '学习类型',
    `create_time`   datetime            NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time`   datetime            NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`),
    UNIQUE KEY `udx_student_learn_type` (`student_id`, `learn_type`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8mb4;
复制代码

时间用的是 datetime 类型,这意味着时间精度是秒,也就是当我同一秒内多次进行更新的时候,实际上该行的记录是没有变动的!!!在这种情况下,返回值也是1!!!

7.补充INSERT ON DUPLICATE KEY UPDATE的一种情况

完整如下:

  • 如果是新插入的记录,那么返回值是1
  • 如果发生了唯一键冲突并更新了记录,那么返回值是2
  • 如果唯一键发生了冲突,但是并没有更新记录,那么返回值将会是1

三、场景回顾和分析

  • 为什么需要使用到INSERT ON DUPLICATE KEY UPDATE的返回值呢?
  • 能不能有其它的方式达到这个效果?

1.原始需求和场景

描述下几个关键的点:

  • 只有首次完成任务的时候,才会触发一系列的操作(可以理解为发mq消息)
  • 完成的判定标准则是在表里边有一条完成记录(唯一键冲突时可认为非首次完成)
  • 当用户重复完成的时候,会更新一些属性字段,但是不应触发mq消息
  • 存在并发完成的操作场景

从代码逻辑上看,也就是并发情况下能区分出该用户是否为首次完成即可

2.常见的解决方案有哪些?

各有取舍、各有优劣!

a.朴素版

//伪代码
var data = "select * from t_learn_state where student_id = bc6b5e6979af11e8a10c1c1b0d1c49aa and learn_type=10"
if (data == null){
    //首次完成
    "insert into t_learn_state(student_id, learn_type) values ('bc6b5e6979af11e8a10c1c1b0d1c49aa','10')"
}else{
    //非首次完成
    "update t_learn_state set update_time = now()"
}
复制代码

b.事务版

//伪代码
transaction.open //打开事务
var data = "select * from t_learn_state where student_id = bc6b5e6979af11e8a10c1c1b0d1c49aa and learn_type=10"
if (data == null){
    //首次完成
    "insert into t_learn_state(student_id, learn_type) values ('bc6b5e6979af11e8a10c1c1b0d1c49aa','10')"
}else{
    //非首次完成
    "update t_learn_state set update_time = now()"
}
transaction.commit //提交事务
复制代码

c.互斥锁版

//伪代码
transaction.open //打开事务
var data = "select * from t_learn_state where student_id = bc6b5e6979af11e8a10c1c1b0d1c49aa and learn_type=10 for update"
if (data == null){
    //首次完成
    "insert into t_learn_state(student_id, learn_type) values ('bc6b5e6979af11e8a10c1c1b0d1c49aa','10')"
}else{
    //非首次完成
    "update t_learn_state set update_time = now()"
}
transaction.commit //提交事务
复制代码

d.冲突更新版

//伪代码
val rt =
"""
INSERT INTO t_learn_state (student_id, learn_type, create_time, update_time)
VALUES ("bc6b5e6979af11e8a10c1c1b0d1c49aa",10,now(),now()) 
ON DUPLICATE KEY UPDATE update_time = now()
"""
//判断是否首次完成
if (rt == 1) {
   //首次完成
   
}else{
    //非首次完成
}
复制代码

e.分布式锁版

//伪代码 
distributeLock.lock //获取分布式锁
var data = "select * from t_learn_state where student_id = bc6b5e6979af11e8a10c1c1b0d1c49aa and learn_type=10"
if (data == null){
    //首次完成
    "insert into t_learn_state(student_id, learn_type) values ('bc6b5e6979af11e8a10c1c1b0d1c49aa','10')"
}else{
    //非首次完成
    "update t_learn_state set update_time = now()"
}
distributeLock.unlock  //释放分布式锁
复制代码

3.常见的做法的比较

版本实现复杂度对db的压力对业务服务的压力并发出错概率备注
朴素版✦✦✦✦✦不处理并发
事务版✦✦✦✦✦mysql的RR级别
互斥锁版✦✦✦✦✦✦✦竞争完全下放到db,并且多次分离执行sql
冲突更新版✦✦✦✦一条sql搞定
分布式锁版✦✦✦✦✦✦✦✦✦竞争完全放到业务服务

4.本文选择 朴素版+冲突更新版

目前遇到的场景及要求是,并发较小、但是数据是用户可见的,因此对并发错误的容忍度是比较低的,但是又不想把整个流程搞得非常复杂,所以可以将朴素版+冲突更新版进行结合,示意:

//伪代码
var data = "select * from t_learn_state where student_id = bc6b5e6979af11e8a10c1c1b0d1c49aa and learn_type=10"
if (data == null){
   val rt =
    """
    INSERT INTO t_learn_state (student_id, learn_type, create_time, update_time)
    VALUES ("bc6b5e6979af11e8a10c1c1b0d1c49aa",10,now(),now()) 
    ON DUPLICATE KEY UPDATE update_time = now()
    """
    //判断是否首次完成
    if (rt == 1) {
       //首次完成

    }else{
        //非首次完成
    }
}else{
    //非首次完成
    "update t_learn_state set update_time = now()"
}
复制代码

综上,我们还是得解决INSERT ON DUPLICATE KEY UPDATE的返回值问题

四、回到 INSERT ON DUPLICATE KEY UPDATE 问题

目前根据搜集到的资料和请教大佬,得到如下几种解法

1.变更返回值类型

也就是前文提到的通过在jdbc中配置useAffectedRows=true,可以将matched rows变为 updated rows,这样也能解决,但是目前jdbc已经使用已久,很多返回值已经有在使用,因此不可贸然变更

配置插入更新无变化
useAffectedRows=true120
useAffectedRows=false121

2.细化时间精度

可见,由于datetime是精确到秒的,因此秒内的依据now()更新时实际上是不更新的,因此我们可以把这个类型细化到更细的粒度,如毫秒级,这样只有在毫秒内的并发才会出现重复

3.增加一个版本号

如何保证每次INSERT ON DUPLICATE KEY UPDATE都更新到表记录呢?那就是每次都手动更新,通过增加一个version字段,每次冲突时都进行+1操作:

val rt = execSql(
"""
INSERT INTO t_learn_state (student_id, learn_type, version, create_time, update_time)
VALUES ("bc6b5e6979af11e8a10c1c1b0d1c49aa",10,1,now(),now()) 
ON DUPLICATE KEY UPDATE update_time = now() and version = version + 1
"""
)
复制代码

这样就能屏蔽唯一索引冲突时,没有更新行记录的情况

五、写在最后

thanks for reading.有其它方案或想法可以一起交流!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/136727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

dubbo源码实践-transport 网络传输层的例子

1 Transporter层概述Transporter层位于第2层,已经实现了完整的TCP通信,定义了一套Dubbo自己的API接口,支持Netty、Mina等框架。官方定义:transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为…

剑指 Offer 09. 用两个栈实现队列

用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 ) 示例 1: 输入: ["…

自然语言处理 第11章 问答系统 复习

问答系统问答系统概述问答系统定义问答(QA)系统发展历程问答系统分类:问答系统框架:内容提要专家系统检索式问答系统1.问题分析主要功能:问题分类 和 关键词提取问题分类实现方法2.关键词提取检索模块相关文档检索句段检索3. 答案抽取模块检索…

回顾2022年,展望2023年

这里写目录标题回顾2022年博客之星你参加了吗?学习方面写博客方面在涨粉丝方面展望2023回顾2022年 时间如梭,转眼间已经2023年了。 你开始做总结了吗? 博客之星你参加了吗? 这是 2022 博客之星 的竞选帖子, 请你在这…

【从零开始学习深度学习】36. 门控循环神经网络之长短期记忆网络(LSTM)介绍、Pytorch实现LSTM并进行训练预测

上一篇文章介绍了一种门控循环神经网络门控循环单元GRU,本文将介绍另一种常用的门控循环神经网络:长短期记忆(long short-term memory,LSTM),它比GRU稍复杂一点。 本文将介绍其实现方法,并使用其…

leetcode 221. 最大正方形-java题解

题目所属分类 动态规划 前面写过一个面积最大的长方形 传送门 f[i, j]表示:所有以(i,j)为右下角的且只包含 1 的正方形的边长最大值 原题链接 在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。 代…

最邻近方法和最邻近插入法求TSP问题近似解(可视化+代码)

摘要:本文总体内容为介绍用最邻近方法(Nearest Neighbor Algorithm) 和最邻近插入法求解旅行商问题(Traveling Saleman Problem,TSP)。同时使用python实现算法,并调用networkx库实现可视化。此文为本人图论课下作业的成品,含金量:…

【若依】前后端分离版本

一、何为框架?若依框架又是什么?具备什么功能? 框架的英文为Framework,带有骨骼,支架的含义。在软件工程中,框架往往被定义为整个或部分系统的可重用设计,是一个可重复使用的设计构件。类似于一…

leetcode1801:积压订单中的订单总数(1.2日每日一题)

迟来的元旦快乐!!! 题目表述: 给你一个二维整数数组 orders ,其中每个 orders[i] [pricei, amounti, orderTypei] 表示有 amounti 笔类型为 orderTypei 、价格为 pricei 的订单。 订单类型 orderTypei 可以分为两种…

电子档案利用安全控制的办法与实现

这篇文章是笔者2015年发表在《保密科学技术》第2期的一篇文章,时隔7年半温习了一遍之后感觉还有一定的可取之处,所以在结合当前档案法律法规相关要求并修改完善其中部分内容之后分享给大家。 引言 INTRODUCTION 21世纪是一个信息化高度发展的时代&#…

网站漏洞与漏洞靶场(DVWA)

数据来源 本文仅用于信息安全学习,请遵守相关法律法规,严禁用于非法途径。若观众因此作出任何危害网络安全的行为,后果自负,与本人无关。 为什么要攻击网站?常见的网站漏洞有哪些? 在互联网中,…

Java安装详细步骤(win10)

一、下载JDK JDK下载地址:Java Archive | Oracle,下图为win10版本 二、安装过程 2.1 以管理员方式运行exe 2.2 更改JDK安装目录和目标文件夹的位置 2.3 安装完成 三、配置环境变量 3.1 快速打开环境变量设置 WinR打开运行对话框,输入…

【计组】CPU并行方案--《深入浅出计算机组成原理》(四)

课程链接:深入浅出计算机组成原理_组成原理_计算机基础-极客时间 一、Superscalar和VLIW 程序的 CPU 执行时间 指令数 CPI Clock Cycle Time CPI 的倒数,又叫作 IPC(Instruction Per Clock),也就是一个时钟周期…

软件测试新手入门必看

随着软件开发行业的日益成熟,软件测试岗位的需求也越来越大。众所周知,IT技术行业一直以来都是高薪岗位的代名词,零基础想要转业的朋友想要进入这个行业,入门软件测试是最佳的途径之一。考虑到大多数软件测试小白对这个行业的一片…

动态规划——树形dp

树形dp 文章目录树形dp概述树形dp 路径问题树的最长路径思路代码树的中心换根DP思路代码数字转换思路代码树形dp 有依赖的背包二叉苹果树思路代码树形dp 状态机没有上司的舞会思路代码战略游戏思路代码皇宫看守思路代码总结概述 树形 DP,即在树上进行的 DP。由于…

springboot常用启动初始化方法

在日常开发时,我们常常需要 在SpringBoot 应用启动时执行某一些逻辑,如下面的场景: 1、获取一些当前环境的配置或变量; 2、连接某些外部系统,读取相关配置和交互; 3、启动初始化多线程(线程池…

Linux 网络编程套接字

目录 一.网络知识 1.网络通信 2.端口号 (1)介绍 (2)端口号和进程ID 3.TCP协议 4.UDP协议 5.网络字节序 二. socket编程接口 1.socket常见API 2.sockaddr结构 (1)sockaddr结构 (2&a…

JavaScript 语句

文章目录JavaScript 语句JavaScript 语句分号 ;JavaScript 代码JavaScript 代码块JavaScript 语句标识符JavaScript 语句 JavaScript 语句向浏览器发出的命令。语句的作用是告诉浏览器该做什么。 JavaScript 语句 JavaScript 语句是发给浏览器的命令。 这些命令的作用是告诉浏…

顶象入选信通院“数字政府建设赋能计划”成员单位

为进一步推动数字政府建设提质增效,由中国信息通信研究院(以下简称“中国信通院”)联合数字政府相关企业、科研机构共同成立“数字政府建设赋能计划”,旨在凝聚各方力量,整合优质资源,开展技术攻关&#xf…

FlinkSQL基本语法和概念

Flink Sql1、简介2、网址3、SQL客户端4、Queries5、Create6、Drop7、Alter8、Insert9、ANALYZE10、Describe11、Explain12、Use13、Show14、Load15、Unload16、Set17、Reset18、Jar19、Windowing TVF19.1、TUMBLE(滚动窗口)19.2、HOP(滑动窗口…