文章目录
-
- 一、问题背景
- 二、解决方案设计
-
- [2.1 核心思路](#2.1 核心思路)
- [2.2 架构设计](#2.2 架构设计)
- [2.3 环境变量配置](#2.3 环境变量配置)
- 三、代码实现
-
- [3.1 DAO 接口层](#3.1 DAO 接口层)
- [3.2 Provider 实现层](#3.2 Provider 实现层)
- [3.3 SelectProvider 工作原理](#3.3 SelectProvider 工作原理)
- 四、数据库脚本
-
- [4.1 初始化脚本(example_prepare_001.sql)](#4.1 初始化脚本(example_prepare_001.sql))
- [4.2 数据同步脚本(example_prepare_fixed_001.sql)](#4.2 数据同步脚本(example_prepare_fixed_001.sql))
- [4.3 Liquibase 标签机制](#4.3 Liquibase 标签机制)
- 五、部署流程
-
- [5.1 首次部署](#5.1 首次部署)
- [5.2 日常开发流程](#5.2 日常开发流程)
- [5.3 数据同步流程](#5.3 数据同步流程)
- 八、注意事项
-
- [8.1 环境变量配置](#8.1 环境变量配置)
- [8.2 数据一致性](#8.2 数据一致性)
- [8.3 SQL 注入防护](#8.3 SQL 注入防护)
- 九、总结
一、问题背景
在微服务架构中,预生产环境(pre)和生产环境(prod)通常共享同一个数据库实例。这种设计虽然降低了运维成本,但也带来了一个严重问题:
预生产环境的数据库操作会影响生产环境的数据,导致生产环境数据被污染或误操作。
问题场景
- 预生产环境进行功能测试时,可能会修改、删除或插入测试数据
- 这些操作直接影响生产环境的数据表
- 生产环境的真实数据可能被测试数据覆盖或污染
- 无法安全地在预生产环境进行大规模数据变更测试
业务影响
以某个业务功能为例,涉及以下核心表:
table_name_1:业务配置表1table_name_2:业务配置表2
这些表的配置直接影响生产环境的核心功能,如果在预生产环境误操作,会导致生产环境功能异常。
二、解决方案设计
2.1 核心思路
通过表名隔离实现环境隔离:
- 预生产环境使用带
_prepare后缀的表名 - 生产环境使用原始表名
- 代码层面根据环境变量动态选择表名
2.2 架构设计
┌─────────────────────────────────────────────────────────┐
│ 应用代码层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ExampleMetaDao (DAO接口) │ │
│ │ @SelectProvider → ExampleMetaDaoProvider │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ MyBatis Provider 层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ ExampleMetaDaoProvider │ │
│ │ - 读取环境变量: management.metrics.tags.environ│ │
│ │ - 动态生成 SQL: 根据环境选择表名 │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 数据库层 │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 生产环境表 │ │ 预生产环境表 │ │
│ │ table_name_* │ │ table_name_*_ │ │
│ │ │ │ prepare │ │
│ └──────────────────┘ └──────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.3 环境变量配置
| 环境 | 配置值 | 使用的表名 |
|---|---|---|
| 预生产环境 | management.metrics.tags.environ=pre |
table_name_1_prepare table_name_2_prepare |
| 生产环境 | management.metrics.tags.environ=prod |
table_name_1 table_name_2 |
三、代码实现
3.1 DAO 接口层
使用 MyBatis 的 @SelectProvider 注解,将 SQL 生成逻辑委托给 Provider 类:
java
@Mapper
public interface ExampleMetaDao {
@SelectProvider(type = ExampleMetaDaoProvider.class, method = "getConfig1")
String getConfig1(@Param("id") Integer id);
@SelectProvider(type = ExampleMetaDaoProvider.class, method = "getConfig2")
String getConfig2(@Param("id") Integer id);
@SelectProvider(type = ExampleMetaDaoProvider.class, method = "getStatus")
Integer getStatus(@Param("id") Integer id);
@SelectProvider(type = ExampleMetaDaoProvider.class, method = "getType")
Integer getType(@Param("id") Integer id);
}
优势:
- 方法签名保持不变,调用方无需修改
- SQL 生成逻辑集中管理
- 支持动态 SQL 构建
3.2 Provider 实现层
核心实现类,负责根据环境变量动态生成 SQL:
java
@Component
public class ExampleMetaDaoProvider {
private static String env;
@Value("${management.metrics.tags.environ:}")
public void setEnv(String env) {
ExampleMetaDaoProvider.env = env;
}
/**
* 根据环境变量获取表名
* 预生产环境返回: baseName_prepare
* 生产环境返回: baseName
*/
private String getTableName(String baseName) {
return "pre".equals(env) ? baseName + "_prepare" : baseName;
}
public String getConfig1(Integer id) {
return "select config_value from " + getTableName("table_name_1") + " where id = #{id}";
}
public String getConfig2(Integer id) {
return "select config_value from " + getTableName("table_name_2") + " where id = #{id}";
}
public String getStatus(Integer id) {
return "select status from " + getTableName("table_name_1") + " where id = #{id}";
}
public String getType(Integer id) {
return "select type from " + getTableName("table_name_1") + " where id = #{id}";
}
}
关键点:
- 环境变量注入 :通过
@Value注解注入环境变量 - 表名动态选择 :
getTableName()方法统一处理表名逻辑 - SQL 动态构建:在运行时根据环境生成对应的 SQL
3.3 SelectProvider 工作原理
调用 DAO 方法
↓
MyBatis 识别 @SelectProvider 注解
↓
通过反射调用 Provider 类的指定方法
↓
Provider 方法返回 SQL 字符串
↓
MyBatis 解析 SQL,处理参数绑定(#{id})
↓
执行 SQL 并返回结果
执行时机:每次调用 DAO 方法时,MyBatis 都会调用 Provider 方法生成 SQL,确保表名始终根据当前环境动态选择。
四、数据库脚本
4.1 初始化脚本(example_prepare_001.sql)
用于创建预生产环境表并初始化数据:
sql
-- liquibase formatted sql
-- changeSet author:1 labels:1.9
-- 创建预生产环境表(结构与生产环境表相同)
CREATE TABLE table_name_1_prepare LIKE table_name_1;
INSERT INTO table_name_1_prepare SELECT * FROM table_name_1;
CREATE TABLE table_name_2_prepare LIKE table_name_2;
INSERT INTO table_name_2_prepare SELECT * FROM table_name_2;
作用:
- 创建预生产环境专用表
- 从生产环境表复制初始数据
- 确保预生产环境有完整的测试数据
4.2 数据同步脚本(example_prepare_fixed_001.sql)
用于将预生产环境的测试结果同步到生产环境:
sql
-- liquibase formatted sql
-- changeSet author:2 labels:1.9,unsafe
-- unsafe:仅 pre 环境不执行
-- 清空生产环境表
TRUNCATE TABLE table_name_1;
TRUNCATE TABLE table_name_2;
-- 从预生产环境表同步数据到生产环境
INSERT INTO table_name_1 SELECT * FROM table_name_1_prepare;
INSERT INTO table_name_2 SELECT * FROM table_name_2_prepare;
关键特性:
- unsafe 标签:标识为危险操作
- 环境限制:仅在非 pre 环境执行(通过 Liquibase 的 labels 机制控制)
- 数据同步流程:先清空生产表,再从预生产表同步数据
4.3 Liquibase 标签机制
Liquibase 通过 labels 和 unsafe 标签控制脚本执行:
| 标签 | 说明 | 执行环境 |
|---|---|---|
labels:1.9 |
版本标签,标识脚本所属版本 | 所有环境 |
labels:1.9,unsafe |
unsafe 标签,标识危险操作 | 仅非 pre 环境 |
执行逻辑:
- 预生产环境:执行
example_prepare_001.sql,跳过example_prepare_fixed_001.sql - 生产环境:执行
example_prepare_001.sql,执行example_prepare_fixed_001.sql
五、部署流程
5.1 首次部署
pre prod 部署应用代码 执行 Liquibase 脚本 环境判断 创建 _prepare 表 创建 _prepare 表
同步数据到生产表 应用启动 根据环境变量选择表名
5.2 日常开发流程
1. 开发人员在预生产环境测试
↓
2. 修改预生产环境表(table_name_*_prepare)
↓
3. 测试通过后,准备上线
↓
4. 执行数据同步脚本(仅生产环境)
↓
5. 将预生产环境的数据同步到生产环境
5.3 数据同步流程
标准流程:
- 预生产环境测试 :在
_prepare表中进行数据变更和测试 - 验证通过:确认预生产环境功能正常
- 执行同步脚本 :在生产环境执行
example_prepare_fixed_001.sql - 数据同步:将预生产环境的数据同步到生产环境
注意事项:
- 同步脚本仅在非 pre 环境执行
- 同步前会清空生产环境表,确保数据一致性
- 建议在低峰期执行同步操作
八、注意事项
8.1 环境变量配置
确保各环境的配置文件正确设置:
yaml
# 预生产环境配置
management:
metrics:
tags:
environ: pre
# 生产环境配置
management:
metrics:
tags:
environ: prod
8.2 数据一致性
- 预生产环境表需要定期从生产环境同步基础数据
- 同步脚本执行前需要确认数据正确性
- 建议在低峰期执行数据同步操作
8.3 SQL 注入防护
Provider 方法中必须使用 #{} 参数占位符,不能使用字符串拼接:
java
// ✅ 正确:预编译,防 SQL 注入
return "select * from " + tableName + " where id = #{id}";
// ❌ 错误:直接拼接,有 SQL 注入风险
return "select * from " + tableName + " where id = " + id;
九、总结
本方案通过 代码层面的动态表名选择 和 数据库层面的表隔离,实现了预生产环境和生产环境的完全隔离。核心特点:
- 零侵入:DAO 接口保持不变,调用方无需修改
- 自动化:通过 Liquibase 自动管理数据库变更
- 安全性:通过 unsafe 标签控制危险操作
- 可扩展:易于添加新的表隔离规则
该方案已在多个业务功能中成功应用,有效解决了预生产环境对生产环境数据的影响问题,为后续类似场景提供了可复用的解决方案。