Spring 使用 Hazelcast 的缓存抽象不保留元素的顺序

Spring Cache Abstraction with Hazelcast doesn't preserve the order of elements

我们将 Hazelcast 从 3.12.12 升级到 5.0.2,现在 Spring 缓存不会保留我们存储在缓存中的地图中元素的顺序。它曾经在升级之前工作。我们存储在缓存中的 java.util.TreeMap 是使用自定义 java.util.Comparator.

排序的

下面是代码。 getSortedCountriesFromCountryCodes() 方法,在调用时,returns 一个 Map,其元素根据自定义比较器正确排序,但是当从缓存中检索相同的 map 时,顺序丢失并且 map 项按字母顺序排序使用相应的键。 Spring 版本为 5.3.14。有没有人见过这种行为,也许知道如何解决?

@Cacheable(value = COUNTRIES_CACHE, key = "#locale?:'default'")
@Override
public Map<String, String> getSortedLocaleCountriesFullMap(Locale locale) {
    return getSortedCountriesFromCountryCodes(locale, ALL_COUNTRY_CODES);
}

(...)

private TreeMap<String, String> getSortedCountriesFromCountryCodes(Locale locale, List<String> countryCodes) {

    final TreeMap<String, String> sortedCountries = new TreeMap<>(new MessageSourceComparator(messageSource, "label.country.", locale));

    for (String countryCode : countryCodes) {
        String label;
        try {
            label = messageSource.getMessage("label.country." + countryCode, null, locale);
        } catch (final NoSuchMessageException nsme) {
            LOG.error("NoSuchMessageException: 'label.country.{}' not found in messages file. " + "Setting country label to countryCode({})", countryCode,
                    countryCode);
            label = countryCode;
        }
        sortedCountries.put(countryCode, label);
    }
    return sortedCountries;
}

    /**
     * Comparator to sort based on messages retrieved using the keys
     */
    protected static class MessageSourceComparator implements Comparator<String>, Serializable {
        private static final long serialVersionUID = 1L;

        private final transient Locale locale;
        private final transient MessageSource messageSource;
        private final transient String messagePrefix;
        private final transient Collator collator;

        public MessageSourceComparator(MessageSource messageSource, String messagePrefix, Locale locale) {
            this.messageSource = messageSource;
            this.messagePrefix = messagePrefix;
            this.locale = locale;
            if (locale != null) {
                this.collator = Collator.getInstance(locale);
            } else {
                this.collator = Collator.getInstance();
            }

        }

        @Override
        public int compare(final String key1, final String key2) {
            if (collator != null) {
                String message1 = getMessage(key1);
                String message2 = getMessage(key2);
                return collator.compare(message1, message2);
            } else {
                return key1.compareTo(key2);
            }
        }

        private String getMessage(String key) {
            String message;
            try {
                message = messageSource.getMessage(messagePrefix + key, null, locale);
            } catch (NoSuchMessageException nsme) {
                message = key;
            }
            return message;
        }
    }

这是缓存配置

@Configuration
@EnableCaching
@EnableAsync
@EnableScheduling
@ComponentScan({ "xxx.yyyyy.zzzzzz" })
public class AppConfig {

    /**
     * The number of backup copies of cache data to use for resilience
     */
    private static final int DEFAULT_BACKUP_COUNT = 2;

    @Bean
    public LogSanitiser logSanitiser() {
        BasicLogSanitiser basicLogSanitiser = new BasicLogSanitiser();
        return basicLogSanitiser;
    }

    /*
     * Use Hazelcast for managing our caches
     * 
     * Takes an autowired list of CacheSimpleConfig objects This allows us to set up our caches in separate config files / modules
     */
    @Bean
    public CacheManager cacheManager(HazelcastInstance hazelcastInstance, List<MapConfig> mapConfigs) {

        for (MapConfig mapConfig : mapConfigs) {
            hazelcastInstance.getConfig()
                    .addMapConfig(mapConfig);
        }

        HazelcastCacheManager cacheManager = new HazelcastCacheManager(hazelcastInstance);
        return cacheManager;
    }



    @Bean
    public MapConfig countriesCacheConfig() {
        return getDefaultMapConfig(DefaultLocaleService.COUNTRIES_CACHE, 25);
    }


    private static MapConfig getDefaultMapConfig(String mapName, int maxHeapUsed) {
        MapConfig mapConfig = new MapConfig(mapName);
        mapConfig.setBackupCount(DEFAULT_BACKUP_COUNT)
                .setEvictionConfig(new EvictionConfig().setEvictionPolicy(EvictionPolicy.LRU)
                        .setSize(maxHeapUsed)
                        .setMaxSizePolicy(MaxSizePolicy.USED_HEAP_SIZE));
        return mapConfig;
    }

 (...)

}

问题出在您的 MessageSourceComparator,在反序列化后排序不同。

在 3.x 和 4.x 之间处理 TreeMap 的方式发生了变化。

在 3.x 中,TreeMap 使用普通 Java 序列化进行序列化。显然,它按照数据存储在地图中的顺序反序列化数据。

在 4.x+ 中添加了一个用于 TreeMap 的特殊序列化程序,此序列化程序在反序列化时所做的是它使用反序列化的比较器创建新的 TreeSet 并将所有元素添加到其中。现在因为你的反序列化比较器是不同的元素以错误的顺序结束。

我认为当比较器在 ser/de 上发生变化时期望保持顺序是不合理的。您可以做的是改为缓存 LinkedHashMap:

    return new LinkedHashMap<>(sortedCountries);

这样您就可以避免完全序列化比较器。看来你之后没有修改设置,所以应该没有问题。