线程安全的In-Memory缓存实现

3
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.URI;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.naming.NamingException;

import org.joda.time.DateTime;
import org.kp.oppr.esb.logger.Logger;
import org.springframework.beans.factory.annotation.Autowired;

public class CachedCrlRepository {

    private static final Logger LOGGER = new Logger("CachedCrlRepository");

    private final Map<URI, SoftReference<X509CRL>> crlCache = Collections
            .synchronizedMap(new HashMap<URI, SoftReference<X509CRL>>());;

    private static int DEFAULT_CACHE_AGING_HOURS;

    @Autowired
    private DgtlSgntrValidator validator;

    @Autowired
    private  CrlRepository crlRepository;

    public X509CRL findCrl(URI crlUri, X509Certificate issuerCertificate,
            Date validationDate) throws DigitalValdiationException,
            CertificateException, CRLException, IOException, NamingException {
        SoftReference<X509CRL> crlRef = this.crlCache.get(crlUri);
        if (null == crlRef) {
            LOGGER.info("Key CRL URI : " + crlUri +  "  not found in the cache " );
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        }
        X509CRL crl = crlRef.get();
        if (null == crl) {
            LOGGER.info("CRL Entry garbage collected: " + crlUri);
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        }
        if (validationDate.after(crl.getNextUpdate())) {
            LOGGER.info("CRL URI  no longer valid: " + crlUri);
            LOGGER.info("CRL validation date: " + validationDate + " is after CRL next update date: " + crl.getNextUpdate());
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        }

        Date thisUpdate = crl.getThisUpdate();
        LOGGER.info("This update " + thisUpdate);

        /*
         * The PKI the nextUpdate CRL extension indicates 7 days. The
         * actual CRL refresh rate is every 3 hours. So it's a bit dangerous to
         * only base the CRL cache refresh strategy on the nextUpdate field as
         * indicated by the CRL.
         */

        DateTime cacheMaturityDateTime = new DateTime(thisUpdate)
                .plusHours(DEFAULT_CACHE_AGING_HOURS);
        LOGGER.info("Cache maturity Date Time " + cacheMaturityDateTime);
        if (validationDate.after(cacheMaturityDateTime.toDate())) {
            LOGGER.info("Validation date: "  + validationDate + " is after cache maturity date: " + cacheMaturityDateTime.toDate());
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        }
        LOGGER.info("using cached CRL: " + crlUri);
        return crl;
    }

    public static int getDEFAULT_CACHE_AGING_HOURS() {
        return DEFAULT_CACHE_AGING_HOURS;
    }

    public static void setDEFAULT_CACHE_AGING_HOURS(int dEFAULT_CACHE_AGING_HOURS) {
        DEFAULT_CACHE_AGING_HOURS = dEFAULT_CACHE_AGING_HOURS;
    }

    private X509CRL refreshCrl(URI crlUri, X509Certificate issuerCertificate,
            Date validationDate) throws DigitalValdiationException,
            CertificateException, CRLException, IOException, NamingException {
        X509CRL crl = crlRepository.downloadCRL(crlUri.toString());
        this.crlCache.put(crlUri, new SoftReference<X509CRL>(crl));
        return crl;
    }




}

我有一个名为CachedCrlrepository的类,用于存储特定提供商的CRL列表。我想知道我的实现是否是线程安全的,或者我是否遗漏了什么。该缓存用于Web服务,因此是多线程的。
我对这个特定方法存在疑问。
private X509CRL refreshCrl(URI crlUri, X509Certificate issuerCertificate,
                Date validationDate) throws DigitalValdiationException,
                CertificateException, CRLException, IOException, NamingException {
            X509CRL crl = crlRepository.downloadCRL(crlUri.toString());
            this.crlCache.put(crlUri, new SoftReference<X509CRL>(crl));
            return crl;
        }

我认为这一行需要同步。
this.crlCache.put(crlUri, new SoftReference<X509CRL>(crl));

 synchronized(this)
{
this.crlCache.put(crlUri, new SoftReference<X509CRL>(crl));

}

我看到的另一个问题是,在运行GC后,缓存仍然在内存中保留该条目。它从未执行这些代码行。

if (null == crl) {
            LOGGER.info("CRL Entry garbage collected: " + crlUri);
            return refreshCrl(crlUri, issuerCertificate, validationDate);
        } 

我其实有点困惑,你是希望它是线程安全的还是不安全的? - Saurabh Jhunjhunwala
考虑使用像Caffeine这样的库。 - Ben Manes
关于 put() 方法:不必担心,Collections.synchronizedMap() 方法已经帮你实现了同步。虽然可以使用SoftReference,但这并不是一个好的编程习惯。 - markspace
1
我不相信有一种权威和正确的方式来回答这个问题。对于“这是线程安全的吗”的问题,必须取决于已经为一个或多个类建立的线程安全策略以及是否使用这些类的实例违反了该策略。由于您没有明确说明策略,因此无法知道。归根结底,线程安全只是关于保护可变状态的问题...有许多实现这个目标的方法(也有许多犯错的方法)。 - scottb
@Saurabh Jhunjhunwala -- 希望它是线程安全的 - Guest
@scottb-- 这个类在Spring bean配置中被声明为单例。然而,使用这个bean的类是原型。由于这是一个缓存,我希望有一个单一的实例,可以在多个类之间共享。 - Guest
1个回答

4
一般情况下,在期望对象有大量访问且并发性高的情况下,不应该使用同步Map。因为每个读写线程都会在另一个线程后面等待,在高负荷情况下,你的线程数将会增加,并最终导致服务器崩溃。你可以查看ConcurrentHashMap,它是专门设计用于这种情况下的高效工作方式。
第二点:
synchronized(this)
{
this.crlCache.put(crlUri, new SoftReference<X509CRL>(crl));

}

由于put方法已经实现了同步,因此当前代码不需要同步。

进行最小化的更改即可替换。

private final Map<URI, SoftReference<X509CRL>> crlCache = Collections
            .synchronizedMap(new HashMap<URI, SoftReference<X509CRL>>());;

使用

private final ConcurrentHashMap<URI, SoftReference<X509CRL>> crlCache = new ConcurrentHashMap<URI, SoftReference<X509CRL>>();

最后,虽然使用SoftReference是好的选择,但有更好的选项。来自谷歌的Guava是一个非常强大和高效的缓存构建器。


好吧,“必须”不是说一定要,但如果存在高需求/线程的可能性,我同意ConcurrentHashMap可能是更好的选择。 - markspace
1
首先,在多线程环境中不要使用同步Map,因为它们会导致严重的性能问题。当然,这是错误的建议。同步集合是完全可接受的解决方案,适用于任何争用较低的情况。只有在并发访问高、产生高争用的情况下,对象状态访问的序列化才会影响性能。此外,在没有并发访问的情况下需要原子性的要求,使得同步成为一个简单而且通常是干净的第一步解决方案。 - scottb
@scottb:感谢您的建议,已经进行了编辑以更加谦虚的陈述。 - ares

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接