首先说明业务场景:
对公司数据的driver增加离线节点上线重连的功能。大致功能需求,在线连接中有 节点1、节点2、节点3······,这时节点2离线了,将节点2移除在线连接池,加入离线节点池,然后定时访问离线节点池,尝试重新连接。由于driver连接自身没有保存连接的uri信息,所以我对driver连接自己封装了一层,大致如下:
原driver连接新建方式
Driver driver = XXX.driver(uri, db, username, password);
封装的Driver:
package com.xxx.xxx.xxx;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* <p> 对xxx封装一层,增加uri属性 <p>
*
* @author: chen
* @create: 2021-04-27
**/
@Slf4j
@Data
public class DriverAgent {
/**
* bolt地址
*/
private String uri;
private Driver driver;
private String db;
private String username;
private String password;
public DriverAgent () {
}
public DriverAgent (String uri, String db, String username, String password) {
this.uri = uri;
this.db= db;
this.username = username;
this.password = password;
this.driver= xxx.driver(uri, db, username, password);
}
public Boolean reconnect(){
try {
this.graph = xxx.driver(uri, db, username, password);
log.info(uri + " has reconnected");
}catch (Exception e){
log.error("reconnection Failed:" + e.getMessage());
return false;
}
return true;
}
}
坑就出现在这里,首先这里我使用了lombok的@Data注解,它会生成以下方法
- 所有属性的get和set方法
- toString 方法
- hashCode方法
- equals方法
注意,重写了hashCode方法和equals方法。
出现的问题:使用HashSet作为离线节点存储的容器,重连时使用迭代器遍历HashSet,调用reconnect方法,返回成功后调用iterator.remove()将当前节点从离线节点池中移除。但是测试过程中发现离线的节点重连成功后,iterator.remove()并未将其从HashSet中移除。
singleThreadExecutor.execute(() -> {
while (true){
Iterator<DriverAgent> iterator = offlineNodes.iterator();
while (iterator.hasNext()){
DriverAgent agent = iterator.next();
if(agent.reconnect()){
agentList.add(agent);
iterator.remove();
}
}
try {
TimeUnit.SECONDS.sleep(60);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
经过阅读源码发现,HashSet实现借助的是HashMap,HashSet的Iterator获取的是HashMap.keySet().Iterator()。其返回的是一个EntryIterator,而EntryIterator继承自HashIterator,HashIterator的remove()方法是通过调用HashMap的removeNode方法实现的。
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
removeNode()方法源码
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//重点看下面这个if
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
可以看到,方法中会通过调用hashCode方法和equals方法去判断当前元素是不是要被删除的元素,而在前面,这两个方法是被重写过的,重写过程中,两个方法的返回值会与各个属性值挂钩,所以这里在我调用了reconnect方法后,如果通过hashCode和equals来比较两个类,其实已经不是同一个类了,因此导致iterator.remove()找不到要删除的元素。