最近在对Redis数据进行遍历代码进行优化的时候,发现redis中的forEach不会触发并发修改异常,本文将为带你了解其中缘由
代码复现
原代码通过list收集待删除商品id,然后遍历删除,非常好的避免了并发修改异常,没有任何问题。
RMap<String, CartInfoDTO> cartMap = redissonClient.getMap(getCartKey(userId));
ArrayList<String> deleteList = new ArrayList<>();
// 遍历Map,获取被选中的商品Id
cartMap.forEach((k, v) -> {
if (v.getIsChecked().equals(1)) deleteList.add(k);
});
// 遍历删除商品Id
deleteList.forEach(skuId -> cartMap.remove(String.valueOf(skuId)));
但下面这段代码,这里直接使用forEach循环,并且在循环过程当中,直接对RMap中的数据进行了修改,但是并没有触发并发修改异常,这就非常不符合我们的认知了。因为如果是Java中的普通Map对象,对此进行相同操作必然报出ConcurrentModificationException
。
RMap<String, CartInfoDTO> cartMap = redissonClient.getMap(getCartKey(userId));
cartMap.forEach((k, v) -> {
if (v.getIsChecked().equals(1)) cartMap.fastRemove(k);
});
什么是并发修改异常?
在重点之前,先复习一下什么是并发修改异常,在Java中,并发修改异常(ConcurrentModificationException
)是在用迭代器遍历集合对象时,如果集合内容被修改了就会抛出这个异常。
主要原因是迭代器在遍历时直接依赖集合的内部状态,一旦集合被修改,迭代器无法感知,导致其遍历结果不确定,所以会抛出这个异常提示用户集合正在被修改。
详见:https://www.jianshu.com/p/f3f6b12330c1
常见的并发修改异常场景
在foreach循环中直接删除集合元素
for (String str : list) {
if (...) {
list.remove(str); // 会抛出并发修改异常
}
}
在遍历中直接修改集合内容
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String str = it.next();
list.add("new"); // 会抛出并发修改异常
}
并发修改异常有什么用?
- 安全性 - 防止遍历时出现不可预期的结果,避免程序出错。
- 一致性 - 迭代器可以以一致的视图遍历集合,不会看到中间状态的数据。
- 可见性 - 强制程序员注意到集合不能在遍历时被修改,考虑使用其他方法安全地删除。
报出该异常,对我们的编码规范带来一定好处:
- 强制处理修改集合的安全性 - 不能随意在遍历时修改集合,会抛异常提醒。
- 避免遍历时看到脏数据 - 一致的遍历视图,不会看到还未完全修改的中间状态。
- 引导使用更好的方式 - 删除时使用迭代器自身的删除方法而不是直接删除等。
所以并发修改异常为遍历时对集合的修改添加了一层保护,维护代码的正确性和一致性。
Redis为什么能够避免并发修改异常?
当我们直接对 RMap 进行forEach遍历删除时,不会抛出并发修改异常,这是为什么呢?
主要原因如下:
- RMap的遍历不是基于Iterator模式实现的
普通的Java集合在遍历时使用了Iterator模式来跟踪遍历状态。这会严格依赖集合内部的状态,一旦状态变化就会引发并发修改异常。
而RMap是基于Redis实现的分布式Map,它的遍历不依赖于集合内部的迭代器状态。
- RMap的遍历是在一个一致性的快照上进行的
RMap的遍历会构建出一个遍历时的一致性视图,即使Map的数据变化,该快照也不会改变。所以遍历视图不会因为数据变化而引发异常。
- 分布式Map的设计考虑了并发修改的问题
分布式Map天生需要面对多线程并发修改的情况,其遍历实现已经处理了并发修改的问题,不会由于数据变化而抛出异常。
总结
RMap 的forEach遍历不是依赖于 Iterator,而是内部实现了一个 Map 结构的快照,用于遍历处理。
所以直接对 RMap 清除数据时,是安全的,不会有并发修改异常。