记录一次跨库连表的坑

一、背景

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层

java 复制代码
    @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层接口

java 复制代码
    /**
     * 获取文章信息
     * 
     * @param req 文章请求参数
     * @return 文章信息
     * @author admin
     * @since v1.0
     */
    ArticleDto selectArticleDetail(ArticleInfoDto dto);

Service层实现类

java 复制代码
    @Resource
    private ApArticleMapper apArticleMapper;
    /**
     * 获取文章信息
     * 
     * @param req 文章请求参数
     * @return 文章信息
     * @author admin
     * @since v1.0
     */
    @Override
    public ArticleDto selectArticleDetail(ArticleInfoDto dto) {
        return apArticleMapper.selectArticleDetail(dto.getArticleId());
    }

Mapper层接口

java 复制代码
    /**
     * 获取文章信息
     * 
     * @param req 文章请求参数
     * @return 文章信息
     * @author admin
     * @since v1.0
     */
    ArticleDto selectArticleDetail(String articleId);

Mapper层xml文件

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分别进行如下配置:

bash 复制代码
# application-dev.properties
system.manage.db=system_manage_dev
bash 复制代码
# application-test.properties
system.manage.db=system_manage_test

此时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 ${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调用:

java 复制代码
    @GetMapping("/api/sys/user/getUserById")
    ResponseResult getUserById(@PathVariable("userId") String userId);

Service层实现类改为:

java 复制代码
    @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文件改为:

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层

java 复制代码
    @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),导致测试环境错误。

五、最终办法

1. 配置文件动态配置数据库名。

2. 放弃跨库联表,分别在两微服务查询并使用Feign调用。

相关推荐
牛马baby22 分钟前
Java高频面试之集合-03
java·开发语言·面试
用手手打人24 分钟前
多线程&JUC(二)
java·开发语言
剑走偏锋o.O1 小时前
Jedis学习笔记
java·redis·笔记·学习·jedis
顽疲1 小时前
uniapp 小红书源码 后台java springboot
java·spring boot·uni-app
用针戳左手中指指头1 小时前
Netty笔记3:NIO编程
java·nio
Craaaayon1 小时前
Docker基础-自定义镜像与容器网络
java·运维·网络·数据库·后端·docker·容器
南棱笑笑生2 小时前
20250304在飞凌OK3588-C的linux R4下提高温度控制阈值为95度
java·linux·算法
Yuanymoon2 小时前
【由技及道】模块化架构设计的量子纠缠态破解指南【人工智障AI2077的开发日志004】
java·devops·spirng
述雾学java2 小时前
多线程的使用、同步和异步、synchronized、线程安全的单例模式、死锁、解决死锁
java·python·单例模式
多多*2 小时前
谈谈单例模式中通过Htools包的SpringUtil.getBean获取Bean的好处
java·开发语言·windows·单例模式·面试·log4j