Spring Boot微服务中Set元素意外变更及重复删除问题排查
Spring Boot订阅者集合同步问题:分析与修复
问题描述
- 新增订阅者时,日志中所有
ConfigurationWrapper对象的tenantId显示为同一值,但实际功能正常 - 移除退订租户后,下次同步时该租户实例仍存在于集合中,重复执行移除操作
相关代码
Service类
@Service public class Service { private final Configuration configuration; private final ApplicationDao appDao; private static final HashSet<ConfigurationWrapper> configurations = new HashSet<ConfigurationWrapper>(); @Autowired public Service(Configuration configuration, ApplicationDao appDao) { this.configuration = configuration; this.appDao = appDao; try {this.scheduledSubscriptionsSync(); } catch (IOException | HttpClientException e) {} } @Scheduled(cron = "0 0 0/1 * * ?") private void scheduledSubscriptionsSync() throws IOException, HttpClientException { List<User> subs = appDao.getAllSubscriptions(); addNewSubs(subs); removeSubs(subs); } private void addNewSubs(List<User> subs) { for (User sub : subs) { Configuration config = configuration; config.setUserName(sub.getName()); config.setPassword(sub.getPassword()); config.setTenantid(sub.getTenant()); ConfigurationWrapper newSubConfig = new ConfigurationWrapper(config, sub); boolean isNewSubConfigAdded = configurations.add(newSubConfig); if (isNewSubConfigAdded) { LOGGER.info("New subscription found: {}", sub.toString()); int counter = 0; for (ConfigurationWrapper configuration : Service.configurations) { LOGGER.debug("!DEBUG! BOOTSTRAPSERVICE - PRINTING HASHSET AFTER ADD: " + configuration.toString() + ", POSITION: " + counter); counter++; } } } } private void removeSubs(List<User> subs) { for (ConfigurationWrapper configuration : Service.configurations) { if (!subs.contains(configuration.getSubscription())) { configurations.remove(configuration); LOGGER.info("Subscription removed: {}", configuration.getSubscription().toString()); } } } public static HashSet<ConfigurationWrapper> getConfigurations() { return configurations; } }
ConfigurationWrapper类
public class ConfigurationWrapper { private Configuration configuration; private final HttpClient httpClient; private final Daos daos; private final Timestamps timestamps; private final User subscription; public ConfigurationWrapper(Configuration configuration, User subscription) { this.configuration = configuration; this.httpClient = new HttpClient(configuration, CredentialsProviderFactory.getConfigCredentials(configuration)); this.daos = new Daos(configuration, this.httpClient); this.timestamps = new Timestamps(); this.subscription = subscription; } public Configuration getConfiguration() { return configuration; } public HttpClient getHttpClient() { return httpClient; } public Daos getDaos() { return daos; } public Timestamps getTimestamps() { return timestamps; } public User getSubscription() { return subscription; } @Override public String toString() { return "ConfigurationWrapper [configuration=" + configuration + ", subscription=" + subscription + "]"; } @Override public int hashCode() { return Objects.hash(configuration, subscription); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ConfigurationWrapper other = (ConfigurationWrapper) obj; return Objects.equals(configuration, other.configuration) && Objects.equals(subscription, other.subscription); } }
问题根源分析
问题1:日志显示异常的原因
在addNewSubs方法中,所有循环都复用同一个Configuration成员变量实例。每次修改该实例的属性后创建ConfigurationWrapper,导致所有Wrapper持有同一个配置对象的引用。后续循环修改配置属性时,会覆盖之前的属性值,因此日志打印时所有Wrapper的configuration都会显示最后一次修改的结果。但实际功能正常是因为HttpClient等依赖在Wrapper初始化时已基于当时的配置创建,后续配置对象的修改不影响已初始化的实例。
问题2:重复移除的原因
- 遍历移除的并发修改问题:直接遍历
HashSet并调用remove方法,会触发隐性的并发修改问题,导致遍历逻辑混乱,部分元素无法被正确处理。 - equals/hashCode依赖可变对象:
ConfigurationWrapper的equals和hashCode依赖可变的Configuration对象,当配置属性被修改后,对象哈希值发生变化,导致HashSet无法正确定位元素,移除操作失效,下次同步时该元素仍会被判定为存在。
修复方案
针对问题1:创建独立的Configuration实例
每次新增订阅者时,创建全新的Configuration实例,避免引用复用:
private void addNewSubs(List<User> subs) { for (User sub : subs) { // 创建新的Configuration实例,避免共享引用 Configuration config = new Configuration(); // 复制原有配置的基础属性(按需调整) BeanUtils.copyProperties(this.configuration, config); config.setUserName(sub.getName()); config.setPassword(sub.getPassword()); config.setTenantid(sub.getTenant()); ConfigurationWrapper newSubConfig = new ConfigurationWrapper(config, sub); boolean isNewSubConfigAdded = configurations.add(newSubConfig); if (isNewSubConfigAdded) { LOGGER.info("New subscription found: {}", sub.toString()); int counter = 0; for (ConfigurationWrapper wrapper : configurations) { LOGGER.debug("!DEBUG! BOOTSTRAPSERVICE - PRINTING HASHSET AFTER ADD: {} , POSITION: {}", wrapper.toString(), counter); counter++; } } } }
针对问题2:安全移除元素 + 调整equals/hashCode
- 使用迭代器安全移除:避免遍历集合时直接修改集合结构
private void removeSubs(List<User> subs) { Iterator<ConfigurationWrapper> iterator = configurations.iterator(); while (iterator.hasNext()) { ConfigurationWrapper wrapper = iterator.next(); if (!subs.contains(wrapper.getSubscription())) { iterator.remove(); // 使用迭代器的remove方法安全删除 LOGGER.info("Subscription removed: {}", wrapper.getSubscription().toString()); } } }
- 修改equals/hashCode依赖不可变对象:仅基于唯一且不可变的
subscription对象判断相等性(需确保User类正确实现equals和hashCode,基于租户ID等唯一标识)
@Override public int hashCode() { return Objects.hash(subscription); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ConfigurationWrapper other = (ConfigurationWrapper) obj; return Objects.equals(subscription, other.subscription); }
额外优化:线程安全处理
HashSet并非线程安全,定时任务和其他操作可能并发访问集合,建议改用线程安全的集合实现:
// 替换原有集合定义 private static final Set<ConfigurationWrapper> configurations = new CopyOnWriteArraySet<>();
内容的提问来源于stack exchange,提问作者Omar Hossam
相关产品推荐
相关产品推荐

