【后端】预生产环境与生产环境数据库表隔离方案

文章目录

    • 一、问题背景
    • 二、解决方案设计
      • [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:业务配置表1
  • table_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}";
    }
}

关键点

  1. 环境变量注入 :通过 @Value 注解注入环境变量
  2. 表名动态选择getTableName() 方法统一处理表名逻辑
  3. 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 通过 labelsunsafe 标签控制脚本执行:

标签 说明 执行环境
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 数据同步流程

标准流程

  1. 预生产环境测试 :在 _prepare 表中进行数据变更和测试
  2. 验证通过:确认预生产环境功能正常
  3. 执行同步脚本 :在生产环境执行 example_prepare_fixed_001.sql
  4. 数据同步:将预生产环境的数据同步到生产环境

注意事项

  • 同步脚本仅在非 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;

九、总结

本方案通过 代码层面的动态表名选择数据库层面的表隔离,实现了预生产环境和生产环境的完全隔离。核心特点:

  1. 零侵入:DAO 接口保持不变,调用方无需修改
  2. 自动化:通过 Liquibase 自动管理数据库变更
  3. 安全性:通过 unsafe 标签控制危险操作
  4. 可扩展:易于添加新的表隔离规则

该方案已在多个业务功能中成功应用,有效解决了预生产环境对生产环境数据的影响问题,为后续类似场景提供了可复用的解决方案。

相关推荐
过期动态5 小时前
JDBC进阶篇:拓展功能与连接池运用详解
java·开发语言·数据库·mysql·oracle·intellij-idea·mybatis
yuezhilangniao5 小时前
PostgreSQL vs MySQL:从零开始基础命令对比指南
数据库·mysql·postgresql
枫叶丹45 小时前
【Qt开发】Qt窗口(十) -> QInputDialog 输入对话框
c语言·开发语言·数据库·c++·qt
程序媛青青5 小时前
MVCC 原理
服务器·数据库·oracle
期待のcode11 小时前
MyBatisX插件
java·数据库·后端·mybatis·springboot
安审若无13 小时前
oracel迁移数据文件至其他目录操作步骤
数据库
sunxunyong14 小时前
doris运维命令
java·运维·数据库
小鸡吃米…14 小时前
Python PyQt6教程七-控件
数据库·python
忍冬行者15 小时前
清理三主三从redis集群的过期key和键值超过10M的key
数据库·redis·缓存