You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

求助:MAC的doFinal方法对相同数据生成不同值的并发场景问题

问题分析与解决方案

哈哈,这个问题我之前在并发场景下踩过一模一样的坑!核心问题出在Mac实例不是线程安全的,不管是你用并行流测试的场景,还是Spring响应式应用里把Mac当成单例Bean注入的场景,本质都是多个线程共用了同一个有状态的Mac实例,导致计算结果混乱。

为什么会出现不一致的摘要?

Mac类内部维护了哈希计算的上下文状态(比如中间哈希值),当多个线程同时调用doFinal()时,会互相干扰这个内部状态——比如线程A刚更新了部分状态,线程B就过来修改,最终导致每个线程计算出来的摘要都不是基于完整输入的正确结果。

你的测试代码里用了parallel()并行流,11个线程共用同一个mac实例;而Spring响应式应用里默认Bean是单例的,并发请求时所有请求线程都会复用同一个Mac实例,自然会出现同样的问题。

解决方案

1. 普通并发代码场景(比如你的测试用例)

最直接的方式是让每个线程拥有独立的Mac实例,避免线程间状态干扰。如果担心频繁创建实例影响性能,可以用ThreadLocal来缓存每个线程的Mac实例:

方案1:每个线程创建新实例

public static void main(String[] args) { 
    String key = "test"; 
    byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
    
    IntStream.range(1, 12).parallel().forEach(d->{ 
        try {
            // 每个线程都创建并初始化自己的Mac实例
            SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA512"); 
            Mac mac = Mac.getInstance("HmacSHA512"); 
            mac.init(secretKeySpec);
            
            final byte[] bytes = "somestring".getBytes(StandardCharsets.UTF_8); 
            final byte[] doFinal = mac.doFinal(bytes); 
            String digest = Base64.getEncoder().encodeToString(doFinal); 
            LOGGER.info("digest -- {} ", digest); 
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            LOGGER.error("生成HMAC失败", e);
        }
    }); 
}

方案2:用ThreadLocal缓存线程专属实例

public static void main(String[] args) { 
    String key = "test"; 
    byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
    
    // ThreadLocal确保每个线程有自己的Mac实例
    ThreadLocal<Mac> macThreadLocal = ThreadLocal.withInitial(() -> {
        try {
            Mac mac = Mac.getInstance("HmacSHA512");
            SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "HmacSHA512");
            mac.init(secretKeySpec);
            return mac;
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("初始化Mac实例失败", e);
        }
    });

    IntStream.range(1, 12).parallel().forEach(d->{ 
        Mac mac = macThreadLocal.get();
        final byte[] bytes = "somestring".getBytes(StandardCharsets.UTF_8); 
        // doFinal会自动重置Mac的内部状态,所以同一线程多次调用也安全
        final byte[] doFinal = mac.doFinal(bytes); 
        String digest = Base64.getEncoder().encodeToString(doFinal); 
        LOGGER.info("digest -- {} ", digest); 
    }); 
}

2. Spring响应式应用场景

Spring默认Bean是单例的,所以绝对不能把Mac实例直接做成单例Bean。推荐以下两种方案:

方案1:将Mac Bean设置为Prototype Scope

让Spring每次获取Bean时都创建新实例,确保每个请求线程拿到独立的Mac:

@Configuration
public class HmacConfig {

    @Value("${hmac.key}")
    private String hmacKey;

    @Bean
    @Scope("prototype") // 每次注入都会创建新实例
    public Mac hmacSha512Mac() throws NoSuchAlgorithmException, InvalidKeyException {
        SecretKeySpec secretKeySpec = new SecretKeySpec(hmacKey.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
        Mac mac = Mac.getInstance("HmacSHA512");
        mac.init(secretKeySpec);
        return mac;
    }
}

方案2:用Supplier提供Mac实例

通过Supplier包装,每次需要时创建新实例,更灵活:

@Configuration
public class HmacConfig {

    @Value("${hmac.key}")
    private String hmacKey;

    @Bean
    public Supplier<Mac> hmacSha512MacSupplier() {
        return () -> {
            try {
                SecretKeySpec secretKeySpec = new SecretKeySpec(hmacKey.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
                Mac mac = Mac.getInstance("HmacSHA512");
                mac.init(secretKeySpec);
                return mac;
            } catch (NoSuchAlgorithmException | InvalidKeyException e) {
                throw new RuntimeException("创建HMAC实例失败", e);
            }
        };
    }
}

在业务类中注入Supplier<Mac>,每次生成摘要时获取新实例:

@Service
public class HmacService {

    private final Supplier<Mac> hmacSha512MacSupplier;

    // 构造注入
    public HmacService(Supplier<Mac> hmacSha512MacSupplier) {
        this.hmacSha512MacSupplier = hmacSha512MacSupplier;
    }

    public String generateHmac(String data) {
        Mac mac = hmacSha512MacSupplier.get();
        byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8);
        byte[] hmacBytes = mac.doFinal(dataBytes);
        return Base64.getEncoder().encodeToString(hmacBytes);
    }
}

关键提醒

永远不要在多线程环境下共用同一个Mac、MessageDigest这类有状态的加密实例——它们的设计初衷就是单线程使用,并发场景下必须保证实例的线程隔离性,要么每个线程一个实例,要么使用ThreadLocal缓存,要么在Spring中调整Bean作用域。

内容的提问来源于stack exchange,提问作者Salman

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.01 01:32:30