SpringBoot 实战 - 基于【强加密主存 + HMAC 分片索引】的加密数据模糊查询方案

在这里插入图片描述


一、方案选型:常见敏感信息处理方案对比

在深入我们的核心方案之前,有必要对业界常见的几种处理方式进行横向对比,明确其优劣势,从而做出最适合自身业务场景的选择。

方案类型核心逻辑优势劣势适用场景
明文匹配数据库存储密文,查询时将全量或大量数据拉取到应用内存中,解密后进行匹配。实现逻辑简单,无需引入额外组件依赖。性能灾难。数据量稍大就会导致内存溢出(OOM),网络IO和CPU开销巨大。仅限于小规模数据、内部后台或单节点测试环境。
数据库函数解密利用数据库内置的解密函数(如 MySQL 的 AES_DECRYPT)在查询时进行解密和匹配。开发速度快,对现有业务逻辑侵入性小。无法利用索引,导致查询时全表扫描,性能随数据量增长而急剧下降。数据量明确小于 10 万的小型数据表。
ES 分词检索将敏感信息加密后,同步一份到 Elasticsearch 中,利用其强大的分词和检索引擎实现模糊查询。检索性能极强,支持各种复杂的查询和聚合分析。架构复杂化,引入新组件(ES),需要额外解决数据同步、一致性、运维等问题。数据规模达到千万级甚至亿级的超大型业务。
分片存储(本文方案)原文滚动切片后生成 HMAC 索引,通过查询索引间接定位密文,“以密取密”。无新增组件依赖,性能可控且易于维护,安全性高。需要额外维护索引表,索引数据量会随分片长度的减小而增加。数据量在 10 万到千万级之间的中大型业务。

综合来看,分片存储方案在性能、成本和安全性之间取得了最佳平衡,尤其适合绝大多数既要合规又不想引入复杂技术栈的中大型项目。


二、原理介绍

写入流程

用户 应用服务 索引表 data_piece_ciphertext_mapping 主业务表 users 提交手机号 "19266889900" 使用 AES-256-GCM 加密 生成 phone_ciphertext 写入 users 表 (id, phone_ciphertext) 返回写入成功 滚动切片 k=3 → ["192","926","266","688","889","899","990","900"] 对每个片段执行 HMAC-SHA256 生成指纹集合 批量写入映射关系 (piece_ciphertext, biz_id) 返回写入成功 返回“手机号存储完成” 用户 应用服务 索引表 data_piece_ciphertext_mapping 主业务表 users
  • 两条并行存储路径:

    • 一条走 AES-256-GCM → 主表存储密文。

    • 一条走 切片 + HMAC → 索引表存储指纹映射。

  • 索引表只存指纹,不存原文,攻击者即使拖库也无法逆推出手机号。

  • 主表与索引表通过 biz_id 关联,保证查询时可以“索引先行,密文回表”。

查询流程

  • 用户输入 → 服务端切片、HMAC 指纹生成

  • 去 索引表 查找候选数据 → 得到 biz_id

  • 去 主表 查询密文 → 解密还原原文

形成了一个 “索引先行,密文回表” 的闭环。

用户 应用服务 索引表 data_piece_ciphertext_mapping 主业务表 users 输入关键词 "6688" 滚动切片 k=3 → ["668","688"] 对每个片段执行 HMAC-SHA256 生成指纹 查询指纹匹配的 biz_id 集合 返回匹配的 biz_id 列表 根据 biz_id 回表查询 获取 phone_ciphertext 返回加密手机号 使用 AES-256-GCM 解密密文 返回解密后的原始手机号 用户 应用服务 索引表 data_piece_ciphertext_mapping 主业务表 users

其根本思想可以概括为 “密文隔离,索引先行”。它巧妙地将 不可直接查询的强加密数据可供间接查询的安全索引 分离开来,通过“以密查密”的方式,在不牺牲数据安全性的前提下,实现了高效的模糊查询。

整个原理可以拆解为两大核心机制:数据写入时的“分片建索”数据查询时的“索引匹配”


核心机制 1:数据写入时的“分片建索” (Divide and Index)

当一条新的敏感数据(例如手机号 19266889900)需要被存储时,系统会执行以下两步操作:

1. 对原文进行强加密并存储(密文隔离)

  • 目的:确保原始数据的最高安全性,防止数据库被拖库后信息直接泄露。
  • 操作:使用如 AES-256-GCM 这类业界公认的强对称加密算法,将完整的手机号 19266889900 加密成一长串密文(例如 Base64(IV+密文+认证标签))。
  • 存储:将这串完整的、不可直接查询的密文存入主业务表(如 users 表的 phone_ciphertext 字段)。

2. 对原文进行分片并建立安全索引(索引先行)

  • 目的:为后续的模糊查询构建一个可供查找的“目录”,这个目录本身必须是安全的。
  • 操作
    • a. 滚动切片 (Sharding/Slicing):将原始手机号 19266889900 按照预设的固定长度(例如 k=3)进行“滚动”切割,生成一组连续的文本片段。

      19266889900["192", "926", "266", "688", "889", "899", "990", "900"]

      这一步是实现“模糊”匹配的关键。任何包含在原文中的连续片段都会被提取出来。

    • b. 生成指纹 (HMAC Hashing):对上一步得到的每一个文本片段,使用一种特殊的哈希算法——HMAC(带密钥的哈希),例如 HMAC-SHA256,来生成一个固定长度的、不可逆的“指纹”。

      HMAC("688", secret_key)a1b2c3d4... (一个64位的十六进制字符串)

    • c. 存储映射关系:将生成的每个指纹与主数据的业务ID(user.id)关联起来,存入专门的索引表(data_piece_ciphertext_mapping)。

为什么索引必须用 HMAC 而不是普通哈希或对称加密?

这是一个关键的技术决策点:

  • 对比普通哈希 (如 SHA-256):普通哈希虽然也是单向的,但如果攻击者获取了索引表,他们可以通过彩虹表攻击,轻松地反推出常见的、由数字组成的短分片(如 “668”, “888”)。HMAC因为加入了密钥(secret_key),相当于给哈希加了“盐”,使得彩虹表攻击变得不可行。
  • 对比对称加密 (如 AES):AES 加密为了安全,通常会使用一个随机的初始向量(IV),导致相同的明文片段每次加密后得到的密文都不同。这就无法进行等值查询,也就失去了建立索引的意义。而 HMAC 的特性是确定性:只要明文片段和密钥不变,其输出的指纹就永远是相同的,这就保证了索引的可查询性。

核心机制 2:数据查询时的“索引匹配” (Match and Retrieve)

当用户输入一个关键词(例如 6688)进行模糊查询时,系统会执行以下反向流程:

关键词分片与指纹生成

  • 首先,对用户输入的关键词 6688 应用完全相同的滚动切片规则(k=3),得到 ["668", "688"]
  • 然后,使用完全相同的 HMAC 密钥和算法,计算出这两个分片的指纹:HMAC("668")HMAC("688")

查询索引表

  • 系统拿着计算出的指纹集合,去索引表中进行查询:
    SELECT biz_id FROM data_piece_ciphertext_mapping WHERE piece_ciphertext IN ('hmac_of_668', 'hmac_of_688');
    
  • 由于 piece_ciphertext 字段建立了数据库索引,这一步查询非常高效。数据库会快速返回所有包含这些指纹的记录所关联的业务ID(user.id)。

回表查询与解密

  • 拿到业务 ID 集合后,系统会回到主业务表(users 表)中查询这些 ID 对应的完整记录。
    SELECT * FROM users WHERE id IN (...);
    
  • 最后,取出 phone_ciphertext 字段中的强加密密文,使用 AES 密钥进行解密,得到原始的手机号,并将其展示给用户。

原理小结

总的来说,分片存储的原理就是 “化整为零,以索引规避直接解密查询”

  • 化整为零:将一个完整的、不可查的敏感信息,拆解成多个微小的、可索引的片段。
  • 安全索引:使用 HMAC 算法为这些片段创建了一个既安全(不可逆、防破解)又确定(可用于等值匹配)的“指纹索引”。
  • 查询流程:查询时,先通过查询关键词生成的“指纹”去索引表中快速定位,找到匹配项的“门牌号”(业务 ID),最后才根据“门牌号”找到真正的、被严密保管的原始数据并解密。

这个方案成功地将数据存储的安全性数据检索的灵活性解耦,实现了一个既合规又实用的敏感信息处理闭环。


三、核心原理拆解:分片存储方案是如何工作的?

分片存储的核心思想是 “拆分敏感信息为片段,用片段索引间接匹配密文”。它巧妙地绕开了对密文直接进行模糊匹配的难题,具体可以分为以下三个步骤:

1. 数据存储:双表设计 + 双层加密

我们设计两张表来协同工作:主业务表和分片索引表。

  • 主表 (users):负责存储完整的业务数据。其中,敏感信息字段(如 phone_ciphertext)存储的是经过 强加密算法(如 AES-256-GCM) 加密后的密文。这份数据仅在需要精确展示给用户时才进行解密,最大限度地保证了原始信息的安全。

  • 索引表 (data_piece_ciphertext_mapping):这张表的关键是存储敏感信息原文的“分片 HMAC 指纹”。具体流程如下:

    1. 滚动切片:将原文(如手机号 19266889900)按照一个固定的长度 k(例如 k=3)进行滚动切片,得到一组片段:["192", "926", "266", "688", "889", "899", "990", "900"]
    2. 生成指纹:对每一个分片,使用 HMAC-SHA256 算法生成一个固定长度的、不可逆的哈希指纹。
    3. 存储映射:将生成的指纹与主表的业务 ID(如 user_id)关联存储起来。

2. 模糊查询:分片匹配 + 回表解密

当用户输入关键词(如 “6688”)进行模糊查询时,系统将执行以下流程:

  1. 关键词分片:按照数据存储时完全相同的规则(如 k=3)对查询关键词 “6688” 进行滚动切片,得到 ["668", "688"]
  2. 生成 HMAC 指纹:计算每个关键词分片的 HMAC 值,得到一组用于查询的指纹。
  3. 索引表匹配:使用这组 HMAC 指纹去索引表(data_piece_ciphertext_mapping)中进行 IN 查询,高效地找出所有匹配的记录,并获取关联的主表业务 ID 集合。
  4. 主表回表解密:利用获取到的业务 ID,去主表 (users) 中查询对应的记录,然后解密 phone_ciphertext 字段,最终将清晰的原文信息返回给前端。

四、实操Code

下面我们基于 SpringBoot 3.3.2 + JPA + MySQL 来实现这套方案。

1. 环境准备

(1)关键依赖 (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

(2)配置文件 (application.yml)

严重警告:生产环境中,密钥(aes-key, hmac-key)绝不能硬编码在配置文件中!必须通过环境变量、配置中心或专业的密钥管理服务(KMS)进行注入。

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/security_demo?useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root # Replace with your password
  jpa:
    hibernate:
      ddl-auto: none # 生产环境建议使用 Flyway 或 Liquibase 管理表结构
    properties:
      hibernate:
        format_sql: true

app:
  crypto:
    aes-key: "uE2mFq7nA1b4C7d9uE2mFq7nA1b4C7d9" # 必须是32字节 (256位) 的 AES-256-GCM 密钥
    hmac-key: "HmacKey-ChangeMe-And-Keep-It-Secret" # 生产环境务必替换为强密钥
    piece-length: 3 # 敏感信息默认分片长度

(3)表结构 (schema.sql)

我们创建 users 主表和 data_piece_ciphertext_mapping 索引表。注意,索引表需要对 piece_ciphertext 字段建立索引以保证查询性能。

-- 主表:存储用户基本信息和加密后的手机号
CREATE TABLE IF NOT EXISTS users (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(64) NOT NULL,
  phone_ciphertext VARCHAR(512) NOT NULL COMMENT '手机号AES密文(Base64编码,含IV和认证标签)',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 索引表:存储敏感信息分片的HMAC指纹
CREATE TABLE IF NOT EXISTS data_piece_ciphertext_mapping (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  biz_id BIGINT NOT NULL COMMENT '关联的业务ID,即 users.id',
  piece_ciphertext CHAR(64) NOT NULL COMMENT '分片的HMAC-SHA256指纹(十六进制)',
  piece_len INT NOT NULL DEFAULT 3 COMMENT '分片时的长度',
  INDEX idx_piece (piece_ciphertext), -- 核心查询索引!
  UNIQUE KEY uk_biz_piece (biz_id, piece_ciphertext, piece_len), -- 防止同一业务ID的同一分片重复存储
  FOREIGN KEY (biz_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 核心服务实现

(1)加密服务 (CryptoService)

这个 Service 是所有加密操作的核心,封装了 AES-256-GCM 加密/解密 和 HMAC-SHA256 指纹生成。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

@Service
public class CryptoService {

    @Value("${app.crypto.aes-key}")
    private String aesKeyStr;
    @Value("${app.crypto.hmac-key}")
    private String hmacKeyStr;

    private SecretKey aesKey;
    private SecretKey hmacKey;
    private final SecureRandom random = new SecureRandom();
    private static final int GCM_IV_LENGTH = 12; // 96 bits
    private static final int GCM_TAG_LENGTH = 128; // bits

    @PostConstruct
    public void init() {
        this.aesKey = new SecretKeySpec(aesKeyStr.getBytes(StandardCharsets.UTF_8), "AES");
        this.hmacKey = new SecretKeySpec(hmacKeyStr.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    }

    /**
     * AES-256-GCM 加密
     * @param plaintext 原始明文
     * @return Base64编码的字符串,格式为:IV + 密文 + 认证标签
     */
    public String encryptField(String plaintext) {
        try {
            byte[] iv = new byte[GCM_IV_LENGTH];
            random.nextBytes(iv);
            
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
            cipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec);
            
            byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
            
            // 合并 IV 和密文
            ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + ciphertext.length);
            byteBuffer.put(iv);
            byteBuffer.put(ciphertext);
            
            return Base64.getEncoder().encodeToString(byteBuffer.array());
        } catch (Exception e) {
            throw new IllegalStateException("AES encryption failed", e);
        }
    }

    /**
     * AES-256-GCM 解密
     * @param base64Ciphertext Base64编码的密文
     * @return 原始明文
     */
    public String decryptField(String base64Ciphertext) {
        try {
            byte[] decoded = Base64.getDecoder().decode(base64Ciphertext);
            
            ByteBuffer byteBuffer = ByteBuffer.wrap(decoded);
            byte[] iv = new byte[GCM_IV_LENGTH];
            byteBuffer.get(iv);
            
            byte[] ciphertext = new byte[byteBuffer.remaining()];
            byteBuffer.get(ciphertext);

            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
            cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec);
            
            return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
        } catch (Exception e) {
            // 在生产环境中应记录更详细的错误日志
            throw new IllegalStateException("AES decryption failed", e);
        }
    }

    /**
     * HMAC-SHA256 计算分片指纹
     * @param piece 明文分片
     * @return 64位的十六进制字符串
     */
    public String hmacPiece(String piece) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(hmacKey);
            byte[] rawHmac = mac.doFinal(piece.getBytes(StandardCharsets.UTF_8));
            
            // 转换为十六进制字符串
            StringBuilder hexString = new StringBuilder(2 * rawHmac.length);
            for (byte b : rawHmac) {
                hexString.append(String.format("%02x", b));
            }
            return hexString.toString();
        } catch (Exception e) {
            throw new IllegalStateException("HMAC calculation failed", e);
        }
    }
}

(2)分片服务 (PieceMatchService)

这个 Service 负责实现文本的滚动切片逻辑。

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;

@Service
public class PieceMatchService {

    @Value("${app.crypto.piece-length}")
    private int defaultPieceLen;

    /**
     * 对给定的明文进行滚动分片
     * @param plaintext 原始文本
     * @param pieceLen  分片长度,如果为null或<=0,则使用默认值
     * @return 分片列表
     */
    public List<String> rollingPieces(String plaintext, Integer pieceLen) {
        int k = (pieceLen == null || pieceLen <= 0) ? defaultPieceLen : pieceLen;
        
        if (plaintext == null || plaintext.isEmpty()) {
            return List.of();
        }
        // 如果文本长度不足一个分片,则将整个文本作为一个分片
        if (plaintext.length() <= k) {
            return List.of(plaintext);
        }

        List<String> pieces = new ArrayList<>();
        for (int i = 0; i <= plaintext.length() - k; i++) {
            pieces.add(plaintext.substring(i, i + k));
        }
        return pieces;
    }
}

(3)业务服务 (UserService)

这个 Service 负责业务流程的编排,整合加密和分片服务,实现用户的创建和模糊查询。

import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final DataPieceCiphertextMappingRepository mappingRepository;
    private final CryptoService cryptoService;
    private final PieceMatchService pieceMatchService;

    @Value("${app.crypto.piece-length}")
    private int defaultPieceLen;

    @Transactional
    public UserView createUser(UserCreateRequest req) {
        // 1. 加密手机号,并保存主表信息
        String phoneCipher = cryptoService.encryptField(req.getPhone());
        User user = User.builder()
                .username(req.getUsername())
                .phoneCiphertext(phoneCipher)
                .createdAt(LocalDateTime.now())
                .build();
        user = userRepository.save(user);

        // 2. 对手机号原文进行分片,并生成HMAC指纹存入索引表
        List<String> pieces = pieceMatchService.rollingPieces(req.getPhone(), defaultPieceLen);
        if (pieces.isEmpty()) { // 处理长度不足一个分片的情况
            pieces = List.of(req.getPhone());
        }
        int pieceLength = Math.min(defaultPieceLen, req.getPhone().length());

        final Long userId = user.getId();
        List<DataPieceCiphertextMapping> mappings = pieces.stream()
                .map(piece -> DataPieceCiphertextMapping.builder()
                        .bizId(userId)
                        .pieceCiphertext(cryptoService.hmacPiece(piece))
                        .pieceLen(pieceLength)
                        .build())
                .collect(Collectors.toList());
        mappingRepository.saveAll(mappings);

        // 实际项目中,返回DTO时是否包含明文手机号需根据权限和场景决定
        return UserView.fromEntity(user, req.getPhone());
    }

    @Transactional(readOnly = true)
    public List<UserView> searchByKeyword(String keyword, Integer pieceLen) {
        if (keyword == null || keyword.isBlank()) {
            return List.of();
        }
        int k = (pieceLen == null || pieceLen <= 0) ? defaultPieceLen : pieceLen;

        // 1. 对查询关键词进行分片,并计算HMAC指纹
        List<String> keywordPieces = pieceMatchService.rollingPieces(keyword, k);
        if (keywordPieces.isEmpty()) { // 处理关键词长度不足一个分片的情况
            keywordPieces = List.of(keyword);
            k = keyword.length();
        }
        List<String> keywordHmacs = keywordPieces.stream()
                .map(cryptoService::hmacPiece)
                .collect(Collectors.toList());

        // 2. 通过指纹在索引表中查找匹配的业务ID
        List<DataPieceCiphertextMapping> hits = mappingRepository
                .findByPieceCiphertextInAndPieceLen(keywordHmacs, k);
        if (hits.isEmpty()) {
            return List.of();
        }
        Set<Long> userIds = hits.stream()
                .map(DataPieceCiphertextMapping::getBizId)
                .collect(Collectors.toSet());

        // 3. 回到主表查询完整信息,解密并返回
        return userRepository.findAllById(userIds).stream()
                .map(user -> {
                    String decryptedPhone = cryptoService.decryptField(user.getPhoneCiphertext());
                    return UserView.fromEntity(user, decryptedPhone);
                })
                .collect(Collectors.toList());
    }
    
    // 省略更新手机号的逻辑,其核心是:删除旧手机号的所有索引,然后为新手机号创建新索引
}

3. 接口测试 (UserController)

最后,提供一个简单的 Controller 来暴露 API 接口,并使用 curl 进行快速验证。

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping
    public UserView create(@Valid @RequestBody UserCreateRequest req) {
        return userService.createUser(req);
    }

    @GetMapping("/search")
    public List<UserView> search(@RequestParam String keyword,
                                 @RequestParam(required = false) Integer pieceLen) {
        return userService.searchByKeyword(keyword, pieceLen);
    }

    // 示例:更新手机号接口(具体实现略)
    @PutMapping("/{id}/phone")
    public UserView updatePhone(@PathVariable Long id, @RequestParam String phone) {
        // return userService.updatePhone(id, phone);
        return null; // 待实现
    }
}

测试命令示例:

# 1. 新增一个用户,手机号为 19266889900
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"username":"alice","phone":"19266889900"}'

# 2. 模糊查询包含 "6688" 的手机号
curl "http://localhost:8080/api/users/search?keyword=6688"

# 预期会返回 Alice 的用户信息

五、生产环境运维建议

将方案部署到生产环境时,还需要考虑以下几点以确保其健壮性和可扩展性:

  1. 片长选择 (k)k=3k=4 是手机号、身份证号等场景下的一个均衡选择。

    • k 值越小,匹配越灵敏(如 k=2 可以匹配任意连续两位),但索引表会急剧膨胀。
    • k 值越大,索引表越小,但查询灵活性降低。需要根据业务需求权衡。
  2. 密钥安全:再次强调,绝不能硬编码密钥。建议专业的密钥管理系统,通过应用启动时拉取或API调用获取。

  3. 索引表扩展:当索引表数据量超过千万时,单一索引的查询性能会下降。可以考虑:

    • 数据库分区:使用 MySQL 的分区功能,例如按 piece_ciphertext 的前一两位字符进行 HASH 或 LIST 分区,将查询压力分散到不同分区。
    • 分库分表:引入 Sharding-JDBC 等中间件,对索引表进行水平切分。
  4. 性能优化

    • 布隆过滤器:对于极高频的查询分片(如手机号中常见的“138”、“888”),可能会导致大量回表查询。可以引入布隆过滤器作为前置缓存,快速判断某个分片是否存在,从而拦截掉大量无效的数据库查询。
    • 分页查询:查询接口必须实现分页逻辑,避免一次性返回大量数据导致服务内存压力。

六、总结

在敏感信息处理的实践中,不存在一招鲜的“银弹”,核心在于在安全合规与业务可用性之间找到平衡。明文匹配简单但不安全,ES 方案性能强大但运维复杂。我们这里提出的 “强加密主存 + HMAC 分片索引” 方案,在不引入新组件、不显著增加架构复杂度的前提下,通过巧妙的设计,实现了接近原生 LIKE 的模糊查询体验

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小工匠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值