4. Mock数据生成策略:使用pgcrypto与自定义函数生成符合业务规则的假数据

在依赖隔离的Mock环境中,数据不仅要“看起来像”,更要“符合业务规则”。本章聚焦如何利用PostgreSQL内置的pgcrypto扩展与自定义函数,构建一套可复用的假数据生成管线,确保每条Mock记录在格式、关联性和安全约束上均与生产环境对齐。

4.1 核心挑战:从随机字符串到业务合规数据

直接使用random()或md5()生成的假数据往往存在以下问题:

  • 格式失效:身份证号、手机号、邮箱等字段不符合国家标准或正则校验。
  • 关联断裂:外键引用、状态机流转、时间序列等业务逻辑无法通过纯随机函数表达。
  • 安全泄露:直接复制生产数据会引入敏感信息,违反GDPR/PIPL合规要求。

解决方案是建立“规则驱动+加密脱敏+确定性随机”的三层生成模型:

三层生成模型
1. 规则层:定义字段格式模板(如手机号前缀、邮箱域名白名单)。
2. 脱敏层:使用pgcrypto对真实数据进行不可逆变换,保留统计特征。
3. 随机层:基于种子(seed)生成可重复的伪随机序列,便于测试复现。
三层Mock数据生成模型 规则层 格式模板 · 正则约束 脱敏层 pgcrypto不可逆变换 随机层 种子驱动 · 可重复序列 业务合规Mock数据 格式正确 · 关联完整 · 安全合规 规则层优先匹配业务格式 脱敏层保障数据安全

4.2 pgcrypto扩展安装与基础函数

pgcrypto是PostgreSQL官方提供的加密扩展,在Mock场景中主要用于:确定性哈希脱敏、随机字节生成、安全随机数。安装命令:

-- 创建扩展(需要超级用户权限)
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- 验证安装
SELECT gen_random_uuid();           -- 返回UUID v4
SELECT gen_salt('md5');             -- 返回随机盐值
SELECT crypt('test_password', gen_salt('bf'));  -- bcrypt哈希
函数 用途 Mock场景示例
gen_random_uuid() 生成随机UUID 主键、业务流水号
gen_random_bytes(n) 生成n字节随机数据 Token、密钥占位
digest(data, type) 哈希摘要(sha256等) 脱敏身份证号、手机号
crypt() / gen_salt() 密码哈希 用户密码Mock
最佳实践:使用digest()对真实数据进行SHA-256哈希后,再截取前12-16位作为脱敏标识符,既保留唯一性又无法还原原文。

4.3 自定义函数:构建业务规则引擎

仅靠pgcrypto无法生成“符合业务规则”的数据。需要编写PL/pgSQL自定义函数,将规则编码为可调用的生成器。以下是一个面向金融风控Mock场景的完整示例:

-- 1. 生成符合Luhn算法的银行卡号(16位)
CREATE OR REPLACE FUNCTION mock_bank_card(prefix text DEFAULT '622848')
RETURNS text LANGUAGE plpgsql AS $$
DECLARE
  card text;
  checksum int := 0;
  i int;
  digit int;
BEGIN
  -- 生成后9位随机数字
  card := prefix || lpad(floor(random() * 1e9)::text, 9, '0');
  -- Luhn校验位计算
  FOR i IN 1..length(card) LOOP
    digit := substring(card, i, 1)::int;
    IF (length(card) - i) % 2 = 0 THEN
      digit := digit * 2;
      IF digit > 9 THEN digit := digit - 9; END IF;
    END IF;
    checksum := checksum + digit;
  END LOOP;
  RETURN card || ((10 - checksum % 10) % 10)::text;
END;
$$;

-- 2. 生成符合GB/T 2260的身份证号(18位,随机地区+出生日期+顺序码+校验码)
CREATE OR REPLACE FUNCTION mock_id_card(birth_date date DEFAULT '1980-01-01')
RETURNS text LANGUAGE plpgsql AS $$
DECLARE
  area_code text := lpad(floor(random() * 9999)::text, 6, '0');
  birth_str text := to_char(birth_date + (random() * 365*50)::int, 'YYYYMMDD');
  seq_code text := lpad(floor(random() * 999)::text, 3, '0');
  raw text;
  checksum int := 0;
  weight int[] := '{7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2}';
  check_char text[] := '{1,0,X,9,8,7,6,5,4,3,2}';
  i int;
BEGIN
  raw := area_code || birth_str || seq_code;
  FOR i IN 1..17 LOOP
    checksum := checksum + substring(raw, i, 1)::int * weight[i];
  END LOOP;
  RETURN raw || check_char[(checksum % 11) + 1];
END;
$$;

-- 3. 生成符合业务状态机的订单状态序列
CREATE OR REPLACE FUNCTION mock_order_status(seed int DEFAULT 1)
RETURNS text LANGUAGE plpgsql AS $$
DECLARE
  r float;
BEGIN
  -- 基于种子生成确定性随机
  r := (sin(seed * 12.9898 + 78.233) * 43758.5453) % 1.0;
  RETURN CASE
    WHEN r < 0.6 THEN 'PAID'
    WHEN r < 0.8 THEN 'SHIPPED'
    WHEN r < 0.95 THEN 'DELIVERED'
    ELSE 'REFUNDED'
  END;
END;
$$;

4.4 确定性随机:种子驱动与测试复现

Mock数据必须支持“相同输入 → 相同输出”,否则测试用例无法复现。PostgreSQL的random()默认不可控,需要借助setseed()或自定义确定性哈希函数:

-- 方法1:使用setseed()控制会话级随机序列
SELECT setseed(0.42);
SELECT random(), random(), random();  -- 每次会话相同

-- 方法2:自定义确定性伪随机函数(不依赖会话状态)
CREATE OR REPLACE FUNCTION deterministic_random(seed bigint)
RETURNS float LANGUAGE sql IMMUTABLE AS $$
  SELECT (sin(seed * 12.9898 + 78.233) * 43758.5453) % 1.0;
$$;

-- 使用示例:为每个用户ID生成固定的手机号后4位
SELECT 
  user_id,
  '138' || lpad((deterministic_random(user_id) * 10000)::int::text, 4, '0') AS phone_tail
FROM users LIMIT 5;
方法 优点 缺点 推荐场景
setseed() 简单,原生支持 会话级,多连接不隔离 单线程测试脚本
deterministic_random() IMMUTABLE,可索引 需自行保证分布均匀 大规模数据生成
pgcrypto + seed 密码学级均匀性 性能略低 安全敏感字段脱敏

4.5 完整Mock生成管线:批量插入与关联维护

将上述函数组合为批量生成管线,一次性插入1000条关联完整的Mock记录:

-- 批量生成用户+订单数据(保持外键关联)
WITH user_batch AS (
  INSERT INTO mock_users (user_id, id_card, phone, created_at)
  SELECT 
    gen_random_uuid(),
    mock_id_card('1970-01-01'),
    '1' || lpad((deterministic_random(seq) * 1e10)::bigint::text, 10, '0'),
    now() - (random() * interval '365 days')
  FROM generate_series(1, 1000) AS seq
  RETURNING user_id, created_at
)
INSERT INTO mock_orders (order_id, user_id, amount, status, created_at)
SELECT 
  gen_random_uuid(),
  u.user_id,
  round((deterministic_random(seq) * 10000)::numeric, 2),
  mock_order_status(seq),
  u.created_at + (random() * interval '30 days')
FROM user_batch u
CROSS JOIN generate_series(1, 3) AS seq;  -- 每个用户3笔订单
安全提醒:pgcrypto的crypt()函数在批量调用时性能开销较大。对于非密码字段的脱敏,优先使用digest() + 截断,避免不必要的bcrypt计算。
批量Mock生成管线流程 generate_series mock_id_card() deterministic_random() INSERT mock_users RETURNING user_id, created_at mock_order_status() INSERT mock_orders 每用户3笔订单 外键自动关联

4.6 数据质量校验:自动化规则检查

生成完成后,必须通过断言式查询验证数据合规性:

-- 校验1:身份证号长度与校验位
SELECT count(*) AS invalid_id_card
FROM mock_users
WHERE length(id_card) != 18
   OR substring(id_card, 18, 1) NOT IN ('0','1','2','3','4','5','6','7','8','9','X');

-- 校验2:手机号格式(1开头的11位数字)
SELECT count(*) AS invalid_phone
FROM mock_users
WHERE phone !~ '^1[0-9]{10}$';

-- 校验3:订单状态机合法性
SELECT status, count(*) 
FROM mock_orders 
WHERE status NOT IN ('PAID','SHIPPED','DELIVERED','REFUNDED')
GROUP BY status;

-- 校验4:外键完整性(孤儿订单)
SELECT count(*) AS orphan_orders
FROM mock_orders o
LEFT JOIN mock_users u ON o.user_id = u.user_id
WHERE u.user_id IS NULL;
生产级建议:将上述校验封装为ASSERT或RAISE EXCEPTION形式的测试函数,集成到CI/CD管线中。每次Mock数据生成后自动执行,确保数据质量门禁。

4.7 性能优化:批量生成与并行策略

当Mock数据量超过百万级时,需注意以下性能瓶颈:

  • 函数调用开销:每行调用多个自定义函数会增加解析成本。建议使用IMMUTABLE标记确定性函数,允许表达式索引。
  • WAL写入量:批量INSERT时使用UNLOGGED表或pg_force_disk_write控制(测试环境可接受)。
  • 并行生成:利用pgbench或COPY协议分段写入,避免单连接瓶颈。
-- 性能优化示例:使用UNLOGGED表加速批量写入
CREATE UNLOGGED TABLE mock_users_temp AS
SELECT * FROM mock_users WHERE 1=0;

-- 批量插入(关闭自动提交,减少事务开销)
BEGIN;
INSERT INTO mock_users_temp
SELECT ... FROM generate_series(1, 1000000) AS seq;
COMMIT;

-- 完成后转换为普通表
ALTER TABLE mock_users_temp SET LOGGED;

通过上述策略,可以在10秒内生成100万条符合业务规则的Mock数据,且每条记录均可通过格式校验、外键关联和状态机约束。