4. Mock数据生成策略:使用pgcrypto与自定义函数生成符合业务规则的假数据
在依赖隔离的Mock环境中,数据不仅要“看起来像”,更要“符合业务规则”。本章聚焦如何利用PostgreSQL内置的pgcrypto扩展与自定义函数,构建一套可复用的假数据生成管线,确保每条Mock记录在格式、关联性和安全约束上均与生产环境对齐。
4.1 核心挑战:从随机字符串到业务合规数据
直接使用random()或md5()生成的假数据往往存在以下问题:
- 格式失效:身份证号、手机号、邮箱等字段不符合国家标准或正则校验。
- 关联断裂:外键引用、状态机流转、时间序列等业务逻辑无法通过纯随机函数表达。
- 安全泄露:直接复制生产数据会引入敏感信息,违反GDPR/PIPL合规要求。
解决方案是建立“规则驱动+加密脱敏+确定性随机”的三层生成模型:
三层生成模型
1. 规则层:定义字段格式模板(如手机号前缀、邮箱域名白名单)。
2. 脱敏层:使用pgcrypto对真实数据进行不可逆变换,保留统计特征。
3. 随机层:基于种子(seed)生成可重复的伪随机序列,便于测试复现。
1. 规则层:定义字段格式模板(如手机号前缀、邮箱域名白名单)。
2. 脱敏层:使用pgcrypto对真实数据进行不可逆变换,保留统计特征。
3. 随机层:基于种子(seed)生成可重复的伪随机序列,便于测试复现。
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计算。
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数据,且每条记录均可通过格式校验、外键关联和状态机约束。