带定时刷新功能的本地缓存简单实现

5 篇文章 0 订阅

写在前面

到有些业务场景中,需要在启动的时候取一些比较少改动(但是有可能会改动)而且量也比较小的数据,主要是一些基础配置类的数据。这些数据实时性要求很低,目前的解决方式是放在 redis 做缓存,并设置过期时间,相当于定期刷新。

但是,其实还可以更进一步优化。毕竟 redis 跟 web 服务器不在同一台服务器上,也是需要远程IO的(redis很快,一般不用考虑这个问题),因此其实还可以做一级本地缓存。
两级缓存
实现本地缓存的方法有很多,我这里只做一个简单的实现。思路是:构造器要求提供一个获取新数据的Supplier,启动一个定时线程进行定时更新。刷新缓存时生成新的 map 对象替换原来的,不对 map 本身进行写操作,避免线程安全问题。这时这个 cache 就是一个线程写,多个线程读的场景,此时只需要使用 volatile 来保证它的可见性即可,否则极端高并发情况下,可能出现线程间的可见性问题(参考《Java并发编程实战》第三章)。

实现

完整代码如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

/**
 * 本地缓存通用类,支持定时刷新缓存
 * <p>
 * 我们采用刷新缓存时生成新的 map 对象的方式,不对 map 进行写操作,避免线程安全问题
 *
 * @author dadiyang
 * @date 2019/3/12
 */
public class RefreshableLocalCache<K, V> {
    private static final Logger log = LoggerFactory.getLogger(RefreshableLocalCache.class);
    /**
     * 添加 volatile 关键字保证并发场景下的可见性
     */
    private volatile Map<K, V> cache;
    private final Supplier<Map<K, V>> supplier;
    private final int refreshInterval;
    private final TimeUnit timeUnit;
    private final String threadName;

    private ScheduledThreadPoolExecutor executor;
    private Future<?> refreshFuture;

    /**
     * @param supplier        缓存提供器,通过这个接口获取缓存 map
     * @param refreshInterval 刷新频率,小于或等于0则不刷新缓存
     * @param timeUnit        刷新频率时间单位
     * @param threadName      名称,用于指定刷新的线程名,便于调试
     */
    public RefreshableLocalCache(Supplier<Map<K, V>> supplier, int refreshInterval, TimeUnit timeUnit, String threadName) {
        if (supplier == null) {
            throw new IllegalArgumentException("缓存提供器不能为空");
        }
        this.supplier = supplier;
        this.refreshInterval = refreshInterval;
        this.timeUnit = timeUnit;
        this.threadName = threadName;
    }

    /**
     * @param supplier        缓存提供者,通过这个接口获取缓存 map
     * @param refreshInterval 刷新频率(秒),小于或等于0则不刷新缓存
     * @param threadName      名称,用于指定刷新的线程名,便于调试
     */
    public RefreshableLocalCache(Supplier<Map<K, V>> supplier, int refreshInterval, String threadName) {
        this(supplier, refreshInterval, TimeUnit.SECONDS, threadName);
    }

    /**
     * @param supplier        缓存提供者,通过这个接口获取缓存 map
     * @param refreshInterval 刷新频率(秒),小于或等于0则不刷新缓存
     */
    public RefreshableLocalCache(Supplier<Map<K, V>> supplier, int refreshInterval) {
        this(supplier, refreshInterval, TimeUnit.SECONDS, "localCacheRefresher");
    }

    /**
     * @param supplier        缓存提供者,通过这个接口获取缓存 map
     * @param refreshInterval 刷新频率(秒),小于或等于0则不刷新缓存
     */
    public RefreshableLocalCache(Supplier<Map<K, V>> supplier, int refreshInterval, TimeUnit timeUnit) {
        this(supplier, refreshInterval, timeUnit, "localCacheRefresher");
    }

    /**
     * 启动定时刷新任务,懒加载
     */
    private void runRefresher() {
        if (executor != null) {
            return;
        }
        synchronized (this) {
            if (executor != null) {
                return;
            }
            log.debug(threadName + ": 缓存初始化,刷新间隔: " + refreshInterval);
            // 立即初始化缓存
            cache = supplier.get();
            // 若刷新时间间隔小于 0,则只获取一次,永不刷新缓存
            if (refreshInterval <= 0) {
                log.warn("刷新时间小于等于 0,不自动刷新缓存: " + refreshInterval);
                return;
            }
            // 使用定时线程池
            executor = new ScheduledThreadPoolExecutor(1, r -> new Thread(r, threadName));
            // 定时刷新缓存
            refreshFuture = executor.scheduleAtFixedRate(() -> {
                log.debug("缓存刷新");
                cache = supplier.get();
            }, refreshInterval, refreshInterval, timeUnit);
        }
    }

    /**
     * 获取缓存,注意处理 null 值
     *
     * @param key 键
     * @return 缓存的值,注意处理 null 值
     */
    public V get(K key) {
        runRefresher();
        return cache.get(key);
    }

    /**
     * 获取缓存,若缓存不存在则返回默认值
     *
     * @param key 键
     * @return 缓存的值
     */
    public V getOrDefault(K key, V defaultValue) {
        runRefresher();
        return cache.getOrDefault(key, defaultValue);
    }

    /**
     * 判断缓存是否存在
     *
     * @param key 键
     * @return 是否存在
     */
    public boolean containsKey(K key) {
        runRefresher();
        return cache.containsKey(key);
    }

    /**
     * 优雅地退出,主要是清理定时刷新的任务和线程池
     */
    public void shutdownGracefully() {
        if (executor != null && !executor.isShutdown()) {
            executor.shutdown();
        }
        if (refreshFuture != null) {
            refreshFuture.cancel(true);
        }
    }
}

使用示例

public class RefreshableLocalCacheTest {
    private RefreshableLocalCache<String, Integer> refreshableLocalCache;
	
	public RefreshableLocalCacheTest(){
	    // 初始化
		refreshableLocalCache = new RefreshableLocalCache<>(() -> {
		      // 模拟从数据库或者调用服务获取最新的数据
		       Map<String, Integer> map = new HashMap<>(1024);
		       for (int i = 0; i < 760; i++) {
		           map.put("key:" + i, ThreadLocalRandom.current().nextInt(10000));
		       }
		       return map;
		       // 每分钟刷新一次
		   }, 1, TimeUnit.MINUTES);
    }
    
	public void testCache(){
	    // 调用方法获取缓存
		int value = refreshableLocalCache.getOrDefault("key:111", 0);
		System.out.println(value);
	}
	
	/**
	 * 如果是在Spring中使用,可以使用这个方法在 Bean 在移出容器时关闭定时线程
	 */
	 @PreDestroy
	 public void shutdown(){
	     refreshableLocalCache.shutdownGracefully();
	 }
 }
  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值