目录
- 背景
- 问题分析
- 分析数据
- 分析代码
- 验证分析结果
- 原因分析
- 验证MySQL参数
- 解决办法
- 修改MySQL配置参数
- 修改代码
背景
因公司业务及预算调整,系统部署从原有云服务提供商迁移到另外一家云服务提供商,在测试新服务能力的时候,发现应用系统某个功能不能正常使用,仅仅是第一次成功。
为了分析问题,笔者使用以下环境还原报错场景进行讲解。
- Spring Boot: 3.0.2
- MySQL: 5.7.31
- MyBatis: 3.5.1
问题分析
通过查看服务日志,发现后端接口报SQL异常-主键冲突,如下图所示:
刚开始看到这个错误信息,直接就懵了,怎么在另外一个服务商那里跑得好好的,到这边就主键冲突了呢。一通百度、Google之后,突然之间有个想法,会不会是这两个云服务商提供的MySQL服务,某些参数有区别。
分析数据
因为数据是迁移过来的,表中有大量旧数据,不好确定到底是那个值冲突了。
分析代码
然后查看我们代码,找到对应的PO类代码,代码类似以下,主要关注主键属性 id 的数据类型。
public class UserPo {
private int id;
private String name;
private String email;
private String password;
}
代码中发现id属性是int类型的,我们知道,Java中int类型默认值为0,在新增数据的时候并没有为id属性设置值,代码类似下面这样:
UserPo user = new UserPo();
user.setEmail("test@qq.com");
user.setName("test");
userMapper.insertUser(user);
insert方法的代码如下所示:
@Insert("insert into tb_user(id,name,email,password) values (#{user.id}, #{user.name}, #{user.email}, #{user.password})")
int insertUser(@Param("user") UserPo user);
从代码上看,应该是id值传了0,第一次数据保存成功后,以后就会发生主键冲突异常了。
验证分析结果
通过代码分析,我们去查表中确实有一条id为0的数据,类似下图所示:
在新服务器上开启应用的DEBUG日志,也在日志中查看到SQL日志中id为0的日志。
原因分析
为了查找0值可以作为主键的原因,又是一通百度、Google,最后找到了sql_mode这个参数。这个参数中有个NO_AUTO_VALUE_ON_ZERO的值。通常我们可以通过在AUTO_INCREMENT列上插入0或null值来获取下一个序列的值,但是NO_AUTO_VALUE_ON_ZERO参数阻止了0值的这种行为。也即0值将会作为一个有效的值存入id列中。
可以通过SQL Modes页面查看详细的sql_mode参数的详情,Server System Variables页面查看sql_mode简要说明。
验证MySQL参数
验证下我们分析步骤中查到的sql_mode参数的具体现象。
我们创建一张测试表tb_user,建表语句如下:
CREATE TABLE tb_user (
id int NOT NULL AUTO_INCREMENT,
name varchar(64) NOT NULL DEFAULT '',
email varchar(128) NOT NULL DEFAULT '',
password varchar(128) NOT NULL DEFAULT '',
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
查看当前MySQL版本及sql_mode参数。我们看到当前的MySQL版本5.7.37,且sql_mode中没有NO_AUTO_VALUE_ON_ZERO,MySQL5.7中sql_mode默认没有NO_AUTO_VALUE_ON_ZERO参数。
插入两条数据看下效果
insert into tb_user values(0, 'Rock', 'rock@otc.cc', '123456');
insert into tb_user values(0, 'Kitty', 'kitty@otc.cc', '123456');
数据插入成功,且id值从1开始自增。
我们修改下sql_mode参数,在原有参数上加NO_AUTO_VALUE_ON_ZERO。
SET sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,NO_AUTO_VALUE_ON_ZERO';
然后再插入两条数据,我们就会看到主键冲突的报错。查看数据也是有一条id为0的记录。
解决办法
修改MySQL配置参数
为了能快速在新的云平台上部署应用,并且避免出现其他的未知问题,我们首先修改了MySQL的sql_mode参数和就平台的一致。
当然我们不能通过以上命令去设置我们的MySQL服务,而是通过云服务提供商的管理平台去设置,操作麻烦一点而以。
修改代码
为了后期维护方便,将对应的实体类id属性类型改为Integer,与其他的代码保持一致。