添加微信BGM7756领取文末MySQL相关资料福利!
最近在业务中遇到一个问题,业务是这样的:在插入新用户时需要校验用户的某些信息是否唯一,而在程序中校验结果永远是不唯一的。然后我把 MyBatis 打印的执行 SQL 语句拿了出来在数据库中执行,发现没有数据。
然后我就奇怪了,数据库是同一个啊、SQL 是同一个啊、查询结果都没有变啊,为什么执行的结果在程序里面是 1,而在数据库中是0。
难道是因为 MyBatis 和数据库执行的结果不一样?
后来我才明白不一致的原因。
我编写了一个与实际业务类似的代码,用来模拟上述的问题。
2. 复现问题
2.1. 表结构
MySQL 数据库中创建了一张用户表,只有4个字段。
CREATE TABLE `user` ( `user_id` varchar(36) NOT NULL COMMENT '用户主键id', `user_name` varchar(55) NULL DEFAULT NULL COMMENT '账号', `password` varchar(55) NULL DEFAULT NULL COMMENT '密码', `email` varchar(55) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (`user_id`) USING BTREE); //+V:BGM7756,免费领取资料
2.2. 项目依赖
示例项目是一个 SpringBoot 工程,pom 文件中除了 web 一来还有 mysql 的驱动、MyBatis 和 lombok。
//+V:BGM7756,免费领取资料<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.6.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency></dependencies> //+V:BGM7756,免费领取资料
2.2. 业务
业务流程是这样:新建一个用户,在新建用户之前首先校验邮箱是否已在数据库中存在,然后执行一些其他的业务,然后执行 insert 方法插入数据库,然后执行一些其他的业务,最后再校验 user_name 是否已存在。
//+V:BGM7756,免费领取资料@Slf4j@RestControllerpublic class TestController { @Resource private UserMapper userMapper; /** * springboot Dao层查询结果与数据库实际执行结果不一致? */ @GetMapping("test") @Transactional(rollbackFor = RuntimeException.class) public void transactionalDemo() { // 要插入的数据 User user = new User(); user.setUserId("userId"); user.setUserName("planeswalker"); user.setPassword("password"); user.setEmail("123@gmail.com"); // 校验邮箱 if (userMapper.countByEmail(user.getEmail())>0) { throw new RuntimeException("插入失败,user_id 重复"); } // 执行插入用户操作 userMapper.insert(user); // 校验 user_name if (userMapper.countByName(user.getUserName())>0) { throw new RuntimeException("插入失败,user_name 重复"); } log.info("do something others..."); }} //+V:BGM7756,免费领取资料
userMapper 接口类的代码如下:
//+V:BGM7756,免费领取资料@Repositorypublic interface UserMapper { /** * 查询 email 是否重复 * @param email * @return */ @Select("select count(*) from user where email=#{email}") int countByEmail(String email); /** * 查询 name 是否重复 * @param userName * @return */ @Select("select count(*) from user where user_name=#{userName}") int countByName(String userName);} //+V:BGM7756,免费领取资料
我承认这个方法确实可能不是特别好,比如校验重复的方法为什么有两次,比如 user_id 校验重复方法的合理性。但为了与我在项目中遇到的问题做模拟,这是很类似的,在项目中就是在插入后又校验了一次(因为公用的校验方法会查询两张表而其中一张表的数据是在校验之前插入的)。
可能很多同学已经知道这个业务的问题所在了,先不多说,执行就行了。
2.3. 测试
当我在浏览器上访问这个接口 http://127.0.0.1:8080/test 后,控制台输出了如下的内容:
//+V:BGM7756,免费领取资料2020-05-27 14:07:09.183 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByEmail : ==> Preparing: select count(*) from user where email=? 2020-05-27 14:07:09.208 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByEmail : ==> Parameters: 123@gmail.com(String)2020-05-27 14:07:09.218 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByEmail : <== Total: 12020-05-27 14:07:09.233 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( user_id,user_name,password,email ) VALUES( ?,?,?,? ) 2020-05-27 14:07:09.234 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.mapper.UserMapper.insert : ==> Parameters: userId(String), planeswalker(String), password(String), 123@gmail.com(String)2020-05-27 14:07:09.237 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.mapper.UserMapper.insert : <== Updates: 12020-05-27 14:07:09.237 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByName : ==> Preparing: select count(*) from user where user_name=? 2020-05-27 14:07:09.237 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByName : ==> Parameters: planeswalker(String)2020-05-27 14:07:09.238 DEBUG 18375 --- [nio-8080-exec-6] c.b.d.m.i.a.m.UserMapper.countByName : <== Total: 12020-05-27 14:07:09.250 ERROR 18375 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 插入失败,user_name 重复] with root causejava.lang.RuntimeException: 插入失败,user_name 重复 at com.biosan.databasehandler.TestController.transactionalDemo(TestController.java:43) ~[classes/:na] at com.biosan.databasehandler.TestController$$FastClassBySpringCGLIB$$dadcb476.invoke(<generated>) ~[classes/:na] ...... //+V:BGM7756,免费领取资料
在第二个校验方法的时候抛出了错误,说明数据库中存在相同 user_name 的数据,然后我又把 SQL 拿出来单独去数据库中执行,发现没有数据!
不信邪的我又在第二个校验方法上打了断点,当程序执行到此处时,它的执行结果是:
也就是说确实这时候存在这样的数据!
而此时我又在数据库当中查询,竟然也查不到这条数据!
这就让我开始考虑到,可能不是代码或者框架的原因,而是其他的问题了,比如数据库事务。
2.4. 原因
我们知道在 SpringBoot 的接口上标注了 @Transactional 注解,就相当于开启了一个事务。
MySQL 默认的事务隔离级别是读已提交,即一个事务提交之后,它做的变更才会被其他事务看到。而在同一个事务中,如果先插入后查询,如果查询条件符合,是可以查询到插入的数据的。
当我的程序在执行完 insert 方法后,又去根据 user_name 查询,就可以查询到插入的数据,而此时我直接在数据库中查询该 user_name,相当于又开启了一个事务进行查询,由于读已提交的隔离级别,一个事务提交之后,它做的变更才会被其他事务看到,且业务方法未提交,所以在数据库中查询不到数据。
这也就是我在程序中和数据库中用同样的 SQL 进行查询,但查询结果却不相同的原因。
2.5. 修复
这个问题从业务上来说原本就是不合理的,我在查询重复数据时本就应该排除与将要插入数据相同 id 的数据,即 SQL 应该是:
select count(*) from user where user_name='planeswalker' and user_id!='userId'
篇幅有限,部分资料图片如下,有需要的朋友添加微信BGM7756 获取资料的免费领取方式吧,希望能给您找工作提供很好的帮助!