MessageDigest 使用注意,并发问题

在开发中遇到并发请求时MD5校验错误的问题。由于MessageDigest实例作为成员变量,多个线程共享导致并发计算错误。MD5工具类未正确重置MessageDigest,线程间状态交错影响计算结果。解决方案是确保每个线程拥有独立的MessageDigest实例或在使用后及时reset。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说一下最近在开发过程中遇到加密相关的问题,先引用一段MD5的解释。

百度百科 MD5

Message Digest Algorithm MD5(中文名为消息摘要算法第五版)为计算机安全领域广泛使用的一种散列函数,用以提供消息的完整性保护。该算法的文件号为RFC 1321(R.Rivest,MIT Laboratory for Computer Science and RSA Data Security Inc. April 1992)。

需求:每次请求对请求体进行MD5值计算,服务端做验证并响应(虽然此功能并没什么用,但是需求提出来了,就是要做)。

开发:因为这些常用的工具类之前都写好了,用的时候没有多想就直接Copy过来了,请求是并发的,刚刚开始的时候,并发请求较少(1-2)个,没有出现什么问题,后来请求3-4个同时发的时候,服务端偶尔抛出MD5值验证错误的信息,后来翻看了MD5工具类之后才发现,原来这个类写的方式并不支持并发,MessageDigest被声明为成员变量,多线程环境下会共享同一个MessageDigest对象,为什么共享它在并发情况会出问题呢?

查看MessageDigest源码

/**
     * Updates the digest using the specified array of bytes.
     *
     * @param input the array of bytes.
     */
    public void update(byte[] input) {
        engineUpdate(input, 0, input.length);
        state = IN_PROGRESS;
    }

可以看到这里调用了engineUpdate方法,此方法进行一个更新操作。

Updates the digest using the specified array of bytes, starting at the specified offset.

然后state属性的状态就被改变了,表明当前计算正在处理过程中。

state默认属性

private int state = INITIAL;

然后需要调用MessageDigest.digest()方法计算哈希值

/**
     * Completes the hash computation by performing final operations
     * such as padding. The digest is reset after this call is made.
     *
     * @return the array of bytes for the resulting hash value.
     */
    public byte[] digest() {
        /* Resetting is the responsibility of implementors. */
        byte[] result = engineDigest();
        state = INITIAL;
        return result;
    }

到这里已经完成了MD5值的计算,state属性恢复初始状态,如果想要重用MessageDigest对象,还需要调用MessageDigest.reset()方法进行重置,以免这次计算数据会对下一次的计算造成影响,从而导致计算结果错误。

而我所遇到的问题就是,在MessageDigest在多线程的环境下,Thread-1的计算还没有完成的情况下,Thread-2又开始使用该MessageDigest对象进行下一次的计算,Thread-2修改了MessageDigest的状态,Thread-1使用被修改过后的MessageDigest进行计算,从而导致了计算结果错误。

<think>我们之前讨论了Redis缓存穿透问题,并提到布隆过滤器是解决方案之一。现在用户希望使用Java实现布隆过滤器,并需要代码示例和原理说明。 根据引用[1][2][3],布隆过滤器是一种概率型数据结构,用于快速判断一个元素是否在集合中,其优点是空间效率和查询效率高,缺点是存在误判率(可能将不在集合中的元素误判为存在)且不支持删除操作。 实现方案有两种: 1. 手动实现布隆过滤器(基于位数组和多个哈希函数) 2. 使用现有库(如Google Guava或Redisson) 下面我将分别介绍这两种实现方式,并附上代码示例。 原理说明: 布隆过滤器由一个长度为m的位数组(初始全0)和k个不同的哈希函数组成。每个哈希函数将输入映射到位数组的一个位置(0到m-1)。添加元素时,用k个哈希函数计算元素的k个哈希值,将位数组对应位置置为1。查询元素时,同样计算k个哈希值,如果所有位置都为1,则认为元素可能存在(可能有误判);如果有任何一个位置为0,则元素一定不存在。 误判率与位数组大小m、哈希函数个数k以及插入元素数量n有关。公式为:$P \approx (1 - e^{-kn/m})^k$。 现在,我们分别实现两种方案: 方案一:手动实现(简单版,不考虑并发) 步骤: 1. 定义位数组(使用BitSet) 2. 定义k个哈希函数(可以用多个不同的种子来模拟多个哈希函数) 3. 实现添加元素方法(计算多个哈希值,设置位数组) 4. 实现查询元素方法(检查多个哈希值对应的位是否都为1) 方案二:使用Guava库(Google开源工具包) 步骤: 1. 添加Guava依赖 2. 创建布隆过滤器实例 3. 添加元素和查询元素 方案三:使用Redisson(结合Redis,适用于分布式系统) 步骤: 1. 添加Redisson依赖 2. 配置Redisson客户端 3. 获取布隆过滤器实例 4. 初始化布隆过滤器(设置预期插入数量和误判率) 5. 添加元素和查询元素 下面给出代码示例: 方案一:手动实现 ```java import java.util.BitSet; import java.util.function.ToIntFunction; public class SimpleBloomFilter { private final int size; private final int hashFunctions; private final BitSet bitSet; private final ToIntFunction<String>[] hashFuncs; public SimpleBloomFilter(int size, int hashFunctions) { this.size = size; this.hashFunctions = hashFunctions; this.bitSet = new BitSet(size); // 初始化多个哈希函数(这里用不同的种子模拟多个哈希函数) hashFuncs = new ToIntFunction[hashFunctions]; for (int i = 0; i < hashFunctions; i++) { final int seed = i; hashFuncs[i] = s -> { int hash = 0; for (char c : s.toCharArray()) { hash = seed * hash + c; } return Math.abs(hash) % size; }; } } public void add(String element) { for (ToIntFunction<String> func : hashFuncs) { int index = func.applyAsInt(element); bitSet.set(index); } } public boolean mightContain(String element) { for (ToIntFunction<String> func : hashFuncs) { int index = func.applyAsInt(element); if (!bitSet.get(index)) { return false; } } return true; } public static void main(String[] args) { SimpleBloomFilter filter = new SimpleBloomFilter(100, 5); filter.add("hello"); filter.add("world"); System.out.println(filter.mightContain("hello")); // true System.out.println(filter.mightContain("world")); // true System.out.println(filter.mightContain("hi")); // false(可能误判,但这里100的数组大小和5个函数,插入两个元素误判率较低) } } ``` 方案二:使用Guava(需要添加依赖) 在pom.xml中添加: ```xml <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</version> <!-- 请使用最新版本 --> </dependency> ``` Java代码: ```java import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class GuavaBloomFilterExample { public static void main(String[] args) { // 创建布隆过滤器,设置预期插入量10000,误判率0.01 BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(Charset.forName("UTF-8")), 10000, 0.01); // 添加元素 bloomFilter.put("hello"); bloomFilter.put("world"); // 查询元素 System.out.println(bloomFilter.mightContain("hello")); // true System.out.println(bloomFilter.mightContain("world")); // true System.out.println(bloomFilter.mightContain("hi")); // 可能返回false(但根据误判率,有1%的几率返回true) } } ``` 方案三:使用Redisson(分布式布隆过滤器,需要Redis服务器) 在pom.xml中添加Redisson依赖: ```xml <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.17.7</version> <!-- 请使用最新版本 --> </dependency> ``` Java代码(参考引用[3]): ```java import org.redisson.Redisson; import org.redisson.api.RBloomFilter; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedissonBloomFilterExample { public static void main(String[] args) { // 配置Redisson客户端 Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 替换为你的Redis服务器地址 // 创建Redisson客户端 RedissonClient redisson = Redisson.create(config); // 获取或创建布隆过滤器 RBloomFilter<String> bloomFilter = redisson.getBloomFilter("myBloomFilter"); // 初始化布隆过滤器,设置预期插入数量为10000,误判率为0.03 bloomFilter.tryInit(10000L, 0.03); // 添加元素 bloomFilter.add("hello"); bloomFilter.add("world"); // 检查元素 System.out.println("是否可能包含'hello': " + bloomFilter.contains("hello")); // true System.out.println("是否可能包含'world': " + bloomFilter.contains("world")); // true System.out.println("是否可能包含'hi': " + bloomFilter.contains("hi")); // 可能为false(有3%的误判率) // 关闭客户端 redisson.shutdown(); } } ``` 总结: 1. 手动实现适合学习原理,但生产环境建议使用成熟库。 2. 单机应用推荐Guava,简单高效。 3. 分布式系统推荐Redisson,利用Redis共享布隆过滤器状态。 注意:布隆过滤器只能确认一个元素“一定不存在”或“可能存在”,因此适用于允许少量误判的场景(如缓存穿透防护)。 相关问题: 1. 如何选择布隆过滤器的参数(大小m和哈希函数个数k)? 2. 布隆过滤器为什么不能删除元素?有什么变体支持删除? 3. 在分布式系统中,如何同步多个节点的布隆过滤器? 4. 布隆过滤器的误判率如何测试和调整?</think>### Java实现布隆过滤器:原理与代码示例 #### 1. **布隆过滤器核心原理** 布隆过滤器是一种**概率型数据结构**,通过**位数组**和**多个哈希函数**实现高效元素存在性检测: - **位数组**:长度为 $m$ 的二进制数组(初始全0) - **$k$ 个哈希函数**:将元素映射到 $[0, m-1]$ 的多个位置 - **添加元素**:计算 $k$ 个哈希值,将位数组对应位置设为1 - **查询元素**:检查 $k$ 个位置是否全为1 - 全为1 → **可能存在**(可能有误判) - 任一为0 → **一定不存在** - **误判率公式**:$P \approx (1 - e^{-kn/m})^k$ ($n$=元素数量,$m$=位数组长度,$k$=哈希函数数量)[^1][^2] #### 2. **Java手动实现(基础版)** ```java import java.util.BitSet; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class BloomFilter { private final BitSet bitSet; private final int size; // 位数组长度 m private final int hashCount; // 哈希函数数量 k public BloomFilter(int size, int hashCount) { this.bitSet = new BitSet(size); this.size = size; this.hashCount = hashCount; } // 添加元素 public void add(String element) { for (int i = 0; i < hashCount; i++) { int hash = getHash(element, i); bitSet.set(Math.abs(hash % size)); } } // 检查元素是否存在 public boolean mightContain(String element) { for (int i = 0; i < hashCount; i++) { int hash = getHash(element, i); if (!bitSet.get(Math.abs(hash % size))) { return false; // 一定不存在 } } return true; // 可能存在 } // 哈希函数(基于MD5生成不同偏移量) private int getHash(String element, int seed) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update((element + seed).getBytes()); byte[] digest = md.digest(); return bytesToInt(digest); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } // 字节数组转整数 private int bytesToInt(byte[] bytes) { int result = 0; for (byte b : bytes) { result = (result << 8) | (b & 0xFF); } return result; } public static void main(String[] args) { BloomFilter filter = new BloomFilter(1000, 3); filter.add("apple"); filter.add("banana"); System.out.println(filter.mightContain("apple")); // true System.out.println(filter.mightContain("banana")); // true System.out.println(filter.mightContain("orange")); // false(可能误判) } } ``` #### 3. **使用Guava库实现(生产推荐)** ```java import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class GuavaBloomFilterExample { public static void main(String[] args) { // 创建布隆过滤器:预期元素量10000,误判率1% BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(Charset.forName("UTF-8")), 10000, 0.01 ); // 添加元素 bloomFilter.put("data1"); bloomFilter.put("data2"); // 检查元素 System.out.println(bloomFilter.mightContain("data1")); // true System.out.println(bloomFilter.mightContain("invalid")); // false(误判率<1%) } } ``` > **依赖**: > ```xml > <dependency> > <groupId>com.google.guava</groupId> > <artifactId>guava</artifactId> > <version>31.1-jre</version> > </dependency> > ``` #### 4. **Redis集成方案(分布式场景)** ```java import org.redisson.Redisson; import org.redisson.api.RBloomFilter; import org.redisson.config.Config; public class RedisBloomFilter { public static void main(String[] args) { // 连接Redis Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); // 获取布隆过滤器 RBloomFilter<String> bloomFilter = redisson.getBloomFilter("sampleFilter"); bloomFilter.tryInit(10000L, 0.03); // 预期元素量10000,误判率3% // 添加元素 bloomFilter.add("key1"); bloomFilter.add("key2"); // 检查元素 System.out.println(bloomFilter.contains("key1")); // true System.out.println(bloomFilter.contains("unknown")); // false redisson.shutdown(); } } ``` > **优势**:分布式共享,数据持久化[^3] #### 5. **关键参数选择建议** | 参数 | 计算公式 | 示例值($n=10000, P=0.01$) | |---------------|------------------------------|----------------------------| | 位数组长度 $m$ | $m = -\frac{n \ln P}{(\ln 2)^2}$ | 95850 bits (≈12KB) | | 哈希函数数量 $k$ | $k = \frac{m}{n} \ln 2$ | 7 | > 通过公式 $k = \frac{m}{n} \ln 2$ 优化哈希函数数量可降低误判率[^1] #### 6. **适用场景与注意事项** - **适用场景**: - 缓存穿透防护 - 垃圾邮件过滤 - 爬虫URL去重 - **注意事项**: 1. **不支持删除**:传统布隆过滤器删除元素会导致误判 2. **误判率控制**:实际元素数量超过预设值时误判率飙升 3. **哈希函数质量**:应选用均匀分布的高质量哈希函数
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值