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

文章目录

    • 一、问题背景
    • 二、解决方案设计
      • [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. 可扩展:易于添加新的表隔离规则

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

相关推荐
雨墨✘29 分钟前
golang如何实现设备指纹识别_golang设备指纹识别实现详解
jvm·数据库·python
程序员大辉32 分钟前
没想到!一直要开会员的Navicat 终于有免费版了
数据库
数厘1 小时前
2.15 sql基础查询(SELECT、FROM、字段别名、常量与表达式)
数据库·sql·oracle
可观测性用观测云1 小时前
观测云数据转发和存档最佳实践
数据库
披着羊皮不是狼1 小时前
(7)为 RAG 系统接入 Redis Stack 实现向量持久化
数据库·redis·缓存
SelectDB2 小时前
基于 SelectDB 实现 Hive 数据湖统一分析:洋钱罐全球一体化探索分析平台升级实践
大数据·数据库·数据分析
飞yu流星2 小时前
mysql 基础
数据库·mysql·oracle
零陵上将军_xdr2 小时前
MySQL 事务写入流程详解
android·数据库·mysql
若阳安好2 小时前
【提效小工具】IN SQL、UPDATE SQL、INSERT SQL
java·数据库·sql
二月十六2 小时前
SQL Server 2022 新函数:DATETRUNC 日期截断详解
数据库·sqlserver·datetrunc