不能直接用replace()脱敏姓名手机号,因其全局替换无法按位置保留前缀/后缀,且substring()返回值不能动态作search参数,中文长度、空格、null易导致错误;需按起始位、长度、掩码字符三维度控制。

为什么不能直接用 REPLACE() 处理姓名、手机号这类脱敏?
因为 REPLACE() 是全局替换,无法按位置保留前缀/后缀;比如想“张三丰”变成“张**”,REPLACE(name, SUBSTRING(name,2), '**') 会出错——SUBSTRING() 返回值不能直接作为 REPLACE() 的 search 参数动态计算。更麻烦的是,中文字符长度、空格、NULL 值都会让简单替换崩掉。真正要做的,是按「起始位 + 长度 + 掩码字符」三个维度控制脱敏范围,而这必须靠变量和逻辑判断。
mask_inner() 函数的参数设计与边界处理
Percona 的 mask_inner() 是少数能复用的通用函数,签名是 mask_inner(str, left_len, right_len, mask_char)。但它不校验输入长度,容易在短字符串上出错:
- 当
str长度小于left_len + right_len时,SUBSTRING()会返回空或截断,最终结果可能漏掉后缀 -
mask_char若传入多字节字符(如 '★'),REPEAT()仍按字符数重复,但显示宽度可能异常 - 对 NULL 输入,该函数直接返回 NULL,但业务中常需 fallback 成空字符串
所以实际使用前建议包一层安全调用:COALESCE(mask_inner(IFNULL(name,''), 1, 0, '*'), '')
自己写通用函数时必须加的三道防线
手写 mask_generic() 类函数,光拼 CONCAT() 不够,得防住这三类问题:
-
长度越界:用
GREATEST(0, CHAR_LENGTH(str) - left_len - right_len)算中间掩码长度,避免负数传给REPEAT() -
空值穿透:开头就写
IF str IS NULL OR str = '' THEN RETURN ''; END IF;,别依赖调用方过滤 -
编码兼容:声明
RETURNS VARCHAR(255) CHARSET utf8mb4,否则中文名返回乱码或截断
示例关键片段:
DELIMITER $$
CREATE FUNCTION mask_generic(
str VARCHAR(255),
left_len INT,
right_len INT,
mask_char CHAR(1)
) RETURNS VARCHAR(255) CHARSET utf8mb4
BEGIN
IF str IS NULL OR str = '' THEN RETURN ''; END IF;
SET @total_len = CHAR_LENGTH(str);
SET @middle_len = GREATEST(0, @total_len - left_len - right_len);
RETURN CONCAT(
IF(left_len > 0, LEFT(str, left_len), ''),
REPEAT(mask_char, @middle_len),
IF(right_len > 0, RIGHT(str, right_len), '')
);
END$$
DELIMITER ;
UPDATE 场景下函数性能和事务风险
用自定义函数做 UPDATE users SET name = mask_generic(name, 1, 0, '*'); 看似简洁,但要注意:
- 函数每次调用都重新解析执行,百万级表更新会明显慢于纯 SQL 写法(如直接
CONCAT(LEFT(name,1), REPEAT('*', CHAR_LENGTH(name)-1))) - 若函数里用了临时表或游标,可能触发
ERROR 1442 (HY000): Can't update table 'xxx' in stored function/trigger - 没加
WHERE条件时,全表扫描 + 函数调用 = 锁表时间拉长,线上慎用
真正批量脱敏时,优先走单条 SQL 拼装,函数只用于查询层或小量数据补丁。复杂规则才值得封装——比如需要根据字段长度动态切分掩码段数的场景。
脱敏函数不是越通用越好,而是越贴近你真实字段长度分布、越避开 NULL 和超短值陷阱,才越可靠。写完一定要拿 “王”、“”、“阿依古丽·阿不都热西提” 这类极端值测一遍。











