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

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:重复移除的原因

  1. 遍历移除的并发修改问题:直接遍历HashSet并调用remove方法,会触发隐性的并发修改问题,导致遍历逻辑混乱,部分元素无法被正确处理。
  2. 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

  1. 使用迭代器安全移除:避免遍历集合时直接修改集合结构
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());
        }
    }
}
  1. 修改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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 20:25:36