记录一次跨库连表的坑
一、背景
1. 业务背景
一个微服务项目,本次业务主要涉及两个板块,分别是 文章管理 和 系统管理。具有开发环境、测试环境、生产环境三个环境。其中,开发环境和测试环境用的是同一个服务器(nacos和MySQL都是用的同一个服务器中的)。
文章管理模块是系统的业务模块之一,放在article_server微服务中。在开发环境中的相关数据表放在article_server_dev数据库中,在测试环境中的相关数据表放在article_server_test数据库中。
系统管理是另外一个微服务system_manage中,包括用户管理、角色管理、组织管理等功能。在开发环境中系统管理的相关数据表放在system_manage_dev数据库中,在测试环境中系统管理的相关数据表放在system_manage_test数据库中。
具体数据库信息如下
此时有一个界面的需求是展示文章信息的列表,同时展示订单信息的创建人相关信息(创建人姓名、工号、电话号码)。由于是业务模块,所以在article_server微服务中编写业务逻辑代码,文章相关信息在文章数据库article_server_dev和article_server_test中的article表中;而要同时带出的文章编辑用户信息,则存储在system_manage_dev和system_manage_test数据库中的sys_user表中。在article表中有一个create_user_id字段,存放着与用户的关联字段。
2. 踩坑时期的做法
在一开始做这个业务的时候,偷了个懒,直接在mapper层使用了跨库联表,本地自测一切正常,发版到开发环境后,前端界面一切正常。具体如下:
Controller层
@Resource
private ApArticleService apArticleService;
/**
* 获取文章信息
*
* @param req 文章请求参数
* @return 文章信息
* @author admin
* @since v1.0
*/
@PostMapping(name = "获取文章信息",value = "/selectArticleDetail")
@ApiOperation("获取文章信息")
public ResponseResult selectArticleDetail(@RequestBody ArticleInfoDto req){
if (Objects.isNull(req) || StringUtils.isBlank(req.getArticleId())) {
return ResponseResult.errorResult(400, "参数有误");
}
ArticleDto articleDto = apArticleService.selectArticleDetail(req);
return ResponseResult.okResult(articleDto);
}
Service层接口
/**
* 获取文章信息
*
* @param req 文章请求参数
* @return 文章信息
* @author admin
* @since v1.0
*/
ArticleDto selectArticleDetail(ArticleInfoDto dto);
Service层实现类
@Resource
private ApArticleMapper apArticleMapper;
/**
* 获取文章信息
*
* @param req 文章请求参数
* @return 文章信息
* @author admin
* @since v1.0
*/
@Override
public ArticleDto selectArticleDetail(ArticleInfoDto dto) {
return apArticleMapper.selectArticleDetail(dto.getArticleId());
}
Mapper层接口
/**
* 获取文章信息
*
* @param req 文章请求参数
* @return 文章信息
* @author admin
* @since v1.0
*/
ArticleDto selectArticleDetail(String articleId);
Mapper层xml文件
<select id="selectArticleDetail" resultMap="resultMap">
SELECT
aa.*, su.real_name AS realName, su.phone, su.employ_no AS employNo
FROM `ap_article` aa
LEFT JOIN sys_user su ON aa.create_user_id = su.id
<where>
and aa.is_delete != 1
and su.is_delete != 1
<if test="articleId != null and articleId != ''">
and aa.id = #{articleId}
</if>
</where>
</select>
二、踩坑问题与原因排查
1. 踩坑问题
此时开发环境一切正常,发版到测试环境之后,发现测试环境有几篇文章的所有用户信息全部消失不见。
2. 原因排查
排查后发现,测试环境环境所有消失不见的用户信息,都仅限于某两个用户 张三 和 李四 。其他用户发表的文章都可以正常发现。
到数据库中排查这两个用户的文章信息,发现根据article_server_test数据库中article表,查找一篇张三写的文章的记录,该记录 create_user_id 为 “c01d7916f74811efb5c78c8caa27867d”。以其为主键id,可以在system_manage_test数据库中的sys_user 表中的 id 匹配到 张三 的用户信息。
既然可以找到信息,那为什么没有查询到结果呢?
只有张三和李四这两个用户匹配不上用户信息,而其他用户可以匹配上,于是开始寻找这两个用户与其他用户的区别。比较后发现,其他用户的数据都是直接从开发数据库迁移过来的,所以开发数据库和测试数据库的主键Id相同。而这两个用户开发数据库和测试数据库的主键Id不同,开发环境system_manage_dev数据库的sys_user表,张三 的id为“f380dcc6f74811efb5c78c8caa27867d”,测试环境system_manage_test数据库的sys_user表,张三 的id为之间提到的“c01d7916f74811efb5c78c8caa27867d”。
开发环境两张表示意图:
测试环境两张表示意图:
排查到这里就想到了之前的跨库联表操作,意识到有可能是跨库联表后,导致测试环境的情况下,连的是system_manage_dev的sys_user,而不是system_manage_test的sys_user表。
为验证此猜想,手动暂时先将article_server_test数据库中article表中对应记录的create_user_id改为system_manage_dev的sys_user中 张三 的主键id,发现可以查到相应数据。结论得到验证。
三、解决方案
1. 在sql语句中将数据库名写死(不可行)
首先想到在sql语句中将数据库名写死,但是不可行,因为开发环境、测试环境数据库名都有细微的差别,采用此方案会导致每个环境的这段sql语句不一致,所以此方案不可行。
2. 使用Spring Boot的配置属性动态注入数据库名,实现环境隔离(可行)
在开发环境和测试环境的配置文件application-dev.yml和application-test.yml分别进行如下配置:
# application-dev.properties
system.manage.db=system_manage_dev
# application-test.properties
system.manage.db=system_manage_test
此时mapper层的连表语句如下:
<select id="selectArticleDetail" resultMap="resultMap">
SELECT
aa.*, su.real_name AS realName, su.phone, su.employ_no AS employNo
FROM `ap_article` aa
LEFT JOIN ${system.manage.db}.sys_user su ON aa.create_user_id = su.id
<where>
and aa.is_delete != 1
and su.is_delete != 1
<if test="articleId != null and articleId != ''">
and aa.id = #{articleId}
</if>
</where>
</select>
3. 远程Feign调用(可行)
在system_manage微服务写一个根据用户Id获取用户信息的方法,并用Feign远程调用。代码如下:
远程Feign调用:
@GetMapping("/api/sys/user/getUserById")
ResponseResult getUserById(@PathVariable("userId") String userId);
Service层实现类改为:
@Resource
private ApArticleMapper apArticleMapper;
@Resource
private IArticleClient articleClient;
/**
* 获取文章信息
*
* @param req 文章请求参数
* @return 文章信息
* @author admin
* @since v2.0
*/
@Override
public ArticleDto selectArticleDetail(ArticleInfoDto dto) {
ArticleDto articleDto = apArticleMapper.selectArticleDetail(dto.getArticleId());
ResponseResult responseResult = articleClient.getUserById(articleDto.getCreateUserId());
if (Objects.nonNull(responseResult) && Objects.nonNull(responseResult.getData()) ) {
SysUser user = responseResult.getData();
articleDto.setCreateUserName(user.getRealName);
articleDto.setCreateUserNo(user.getEmployNo);
articleDto.setPhone(user.getPhone);
}
return articleDto;
}
Mapper层xml文件改为:
<select id="selectArticleDetail" resultMap="resultMap">
SELECT * FROM `ap_article`
<where>
and is_delete != 1
<if test="articleId != null and articleId != ''">
and id = #{articleId}
</if>
</where>
</select>
同时在system_manager微服务中创建根据用户id获取用户信息的接口。
Controller层
@Resource
private SysUserService sysUserService ;
/**
* 获取用户信息
*
* @param req 用户Id
* @return 用户信息
* @author admin
* @since v2.0
*/
@PostMapping(name = "获取用户信息",value = "/getUserById")
@ApiOperation("获取用户信息")
public ResponseResult getUserById(@RequestParam("userId") String userId){
if (StringUtils.isBlank(userId)) {
return ResponseResult.errorResult(400, "参数有误");
}
SysUser user = apArticleService.getById(userId);
return ResponseResult.okResult(user);
}
四、原因分析
1.为什么这两个用户的主键id变了?
这两个用户是产品经理的个人账号,虽然也是从开发数据库迁移过来的,但是迁移后,产品经理在分配权限的时候把这两个账号先删除,再新建的,导致重新生成了uuid,所以主键id变了。
2.为什么跨库连表出现了问题?
首先,问题出现在连表时连错数据库了。
理想状态为:在开发环境是article_server_dev数据库的aritcle表和system_manage_dev数据库的sys_user表。在测试环境是article_server_test数据库的aritcle表和system_manage_test数据库的sys_user表。
能查到用户数据:
事实为:在开发环境是article_server_dev数据库的aritcle表和system_manage_dev数据库的sys_user表。在测试环境是article_server_test数据库的aritcle表和system_manage_dev数据库的sys_user表。
不能查到用户数据:
其次,思考为什么会出现这种状态。在article_server微服务中,开发环境配置的数据源为article_server_dev数据库,测试环境配置的数据源为article_server_test数据库。在system_manage微服务中,开发环境配置的数据源为system_manage_dev数据库,测试环境配置的数据源为system_manage_test数据库。
我在mapper层的xml文件中连表查询的时候,没有指定sys_user所在的数据库。这时候SpringBoot会做什么?
Spring Boot通过Profile(如dev、test)管理不同环境的配置。每个微服务的数据源(如article_server的datasource.url)会正确指向对应环境的数据库(如article_server_test)。
尽管article_server和system_manage各自的数据源配置正确,但联表查询涉及跨库时,需显式指定目标数据库名。若未通过配置动态注入数据库名,数据库会在当前连接的库中查找表,而sys_user表并不在article_server_test数据库中,所以SQL会固定指向某个环境(如system_manage_dev),导致测试环境错误。