Java使用WeakReference、SoftReference、ConcurrentHashMap构建本地缓存,支持高并发和集群环境

前言(个人的理解):缓存 一般用来缓解耗时、耗资源的问题,而本地缓存(jvm缓存),相对于需要通过网络连接来访问的缓存(如Redis),本地缓存主要用来缓解耗时问题,以及本地缓存实现起来比较方便 而远程缓存支持存储的对象不够完善(如需要通过序列化/反序列化来解决,本质来说 还是时间问题)。

关于Reference所引用对象的生存时间:

WeakReference:如果当前不存在强引用指向对象T,则对象T的生存时间是一轮GC内;

SoftReference:如果当前不存在强引用指向对象T,则对象T的生存时间直到系统将要发生内存溢出时才会被清除(一轮Full GC的时间?);

因此 使用哪种类型的缓存 看自己的业务需求了。

注:以下实现 模仿自java.lang.reflect.WeakCache。

首先是基类,封装了数据存取逻辑,子类只需提供具体类型的Reference即可。与直接采用WeakReference/SoftReference的区别,该实现是key-value的形式(其中key是强引用,value才是弱引用/软引用),因此使用场景是 需要根据类别缓存多个同一类别的情况。

import java.lang.ref.ReferenceQueue;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.Supplier;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Cache mapping a {@code key -> value}. values are weakly(or softly) but keys are strongly referenced.
 * Keys are passed directly to {@link #get} method.Values are calculated from keys using the {@code valueFactory}
 * function passed to the constructor. Keys can not be {@code null} and are compared by equals 
 * while values returned by {@code valueFactory} can be null and are compared by identity. 
 * Entries are expunged from cache lazily on invocation to {@link #get} method when the WeakReferences(or SoftReference)
 * to values are cleared. 
 * 

this class is imitate from java.lang.reflect.WeakCache * * @param type of keys * @param type of values */ public abstract class ReferenceCache { private static Log log ; private final ReferenceQueue refQueue = new ReferenceQueue<>(); private final ConcurrentMap> map = new ConcurrentHashMap<>(); private final Function valueFactory; /** * Construct an instance of {@code ReferenceCache} * * @param valueFactory a function mapping a {@code key -> value} * @throws NullPointerException if {@code valueFactory} is null. */ protected ReferenceCache(Function valueFactory){ this.valueFactory = Objects.requireNonNull(valueFactory); } private static Log getLog(){ // lazily init the log if(log==null){ // regardless of the concurrency log = LogFactory.getLog(ReferenceCache.class); } return log; } /** * Look-up the value through the cache. * * @param key * @return the cached value (maybe null) * @throws NullPointerException if {@code key} passed in is null. */ public final V get(K key){ Objects.requireNonNull(key); expungeStaleEntries(); Value cache = map.get(key); Value newCache = null; while(true){ if(cache!=null){ V value = cache.get(); if(value!=null){ return value; } } // lazily construct a new-CacheEntry if(newCache==null){ // create new value V value = valueFactory.apply(key); // if new-value is null then just return it if(value==null){ return null; } // wrap value with CacheValue (WeakReference or SoftReference) newCache = createNewValue(key, value, refQueue); } if(cache==null){ cache = map.putIfAbsent(key, newCache); if(cache==null){ // successfully put new-cache cache = newCache; } }else{ if(map.replace(key, cache, newCache)){ if(cache==newCache){ //newCache is cleared? getLog().error("should not reach here!---->there is a bug in ReferenceCache? newCache.value=" + newCache.get()+" -->"+newCache.getClass()); return valueFactory.apply(key); } // successfully replaced cleared CacheEntry with our new-CacheEntry cache = newCache; }else{ // retry with current cache-value cache = map.get(key); } } } } /** * expunge all stale cache entries */ private void expungeStaleEntries(){ Value cache; while ((cache = (Value)refQueue.poll()) != null){ // removing by key and mapped value is always safe here because after a Value // is cleared and enqueue-ed it is only equal to itself // (see Value.equals method)... map.remove(cache.getKey(), cache); } } /** * Removes all of the mappings from this cache. * The cache will be empty after this call returns. */ public final void clear(){ // to remove from refQueue while (refQueue.poll() != null) ; map.clear(); } /** * Removes the key (and its corresponding value) from this cache. * This method does nothing if the key is not in the cache. * * @param key the key that needs to be removed * @return the previous value associated with {@code key}, or * {@code null} if there was no mapping for {@code key} * ( or already cleared) * @throws NullPointerException if the specified key is null */ public final V remove(Object key){ expungeStaleEntries();// to remove from refQueue Value val = map.remove(key); if(val!=null){ return val.get(); } return null; } /** * Constructs a new {@code Value} (WeakReference or SoftReference) * with specified key, value and ReferenceQueue. * @param key the key * @param value the value * @param refQueue the ReferenceQueue * @return a new Value */ protected abstract Value newValue(K key, V value, ReferenceQueue refQueue); /** * Common type of value suppliers that are holding a referent. * The {@link #equals} and {@link #hashCode} of implementations is defined * to compare the referent by identity and cleared Value is only equal to itself. * * @param type of keys * @param type of values */ protected static interface Value extends Supplier { /** * Gets the key. * * @return key */ K getKey(); } }

软引用SoftReference的实现:

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Objects;
import java.util.function.Function;

/**
 * Cache mapping a {@code key -> value}. values are softly but keys are strongly referenced.
 * Keys are passed directly to {@link #get} method.Values are calculated from keys using the {@code valueFactory}
 * function passed to the constructor. Keys can not be {@code null} and are compared by equals 
 * while values returned by {@code valueFactory} can be null and are compared by identity. 
 * Entries are expunged from cache lazily on invocation to {@link #get} method when the SoftReference to
 * values are cleared. 
 *
 * @param  type of keys
 * @param  type of values
 */
public final class SoftCache extends ReferenceCache{

	/**
     * Construct an instance of {@code SoftCache}
     *
     * @param valueFactory  a function mapping a {@code key -> value}
     * @throws NullPointerException if {@code valueFactory} is null.
     */
	public SoftCache(Function valueFactory) {
		super(valueFactory);
	}

	/**
	 * create a new instance of Value(SoftReference)
	 */
	@Override
	protected Value newValue(
			K key, V value, ReferenceQueue refQueue) {
		return new CacheValue(key, value, refQueue);
	}

	/**
     * CacheValue containing a softly referenced {@code value}. It registers
     * itself with the {@code refQueue} so that it can be used to expunge
     * the entry when the {@link SoftReference} is cleared.
     */
    private static final class CacheValue extends SoftReference implements Value {

        private final int hash;
        private final K key;

        private CacheValue(K key, V value, ReferenceQueue refQueue) {
            super(value, refQueue);
            this.hash = System.identityHashCode(value);  // compare by identity
            this.key = Objects.requireNonNull(key);
        }

        @Override
        public int hashCode() {
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            V value;
            return obj == this ||
                   obj != null &&
                   obj.getClass() == this.getClass() &&
                   // cleared CacheValue is only equal to itself
                   (value = this.get()) != null &&
                   // compare value by identity
                   value == ((CacheValue) obj).get();
        }

		public K getKey() {
			return key;
		}
        
    }
}

弱引用WeakReference的实现:

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.function.Function;


/**
 * Cache mapping a {@code key -> value}. values are weakly but keys are strongly referenced.
 * Keys are passed directly to {@link #get} method.Values are calculated from keys using the {@code valueFactory}
 * function passed to the constructor. Keys can not be {@code null} and are compared by equals 
 * while values returned by {@code valueFactory} can be null and are compared by identity. 
 * Entries are expunged from cache lazily on invocation to {@link #get} method when the WeakReferences to
 * values are cleared. 
 *
 * @param  type of keys
 * @param  type of values
 */
public final class WeakCache extends ReferenceCache{

	/**
     * Construct an instance of {@code WeakCache}
     *
     * @param valueFactory  a function mapping a {@code key -> value}
     * @throws NullPointerException if {@code valueFactory} is null.
     */
	public WeakCache(Function valueFactory){
		super(valueFactory);
	}
	
	/**
	 * create a new instance of Value(WeakReference)
	 */
	@Override
	protected Value newValue(
			K key, V value, ReferenceQueue refQueue) {
		return new CacheValue(key, value, refQueue);
	}
	
	/**
     * CacheValue containing a weakly referenced {@code value}. It registers
     * itself with the {@code refQueue} so that it can be used to expunge
     * the entry when the {@link WeakReference} is cleared.
     */
    private static final class CacheValue extends WeakReference implements Value{

        private final int hash;
        private final K key;

        private CacheValue(K key, V value, ReferenceQueue refQueue) {
            super(value, refQueue);
            this.hash = System.identityHashCode(value);  // compare by identity
            this.key = Objects.requireNonNull(key);
        }

        @Override
        public int hashCode() {
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            V value;
            return obj == this ||
                   obj != null &&
                   obj.getClass() == this.getClass() &&
                   // cleared CacheValue is only equal to itself
                   (value = this.get()) != null &&
                   // compare value by identity
                   value == ((CacheValue) obj).get();
        }

		public K getKey() {
			return key;
		}
        
    }

}

一种使用例子:(比如 根据用户id查找对应的信息,如果当前缓存里不存在对应的信息 则从数据库中获取并放入缓存中。缓存的对象通常应该是不变的或者不经常改变的信息)

public class Test {

	//Object可替换为你需要缓存的Bean实例
	private static final SoftCache cache = 
			new SoftCache<>(id -> Test.getFromDB(id));
	
	/**
	 * 根据主键从数据库获取相关信息
	 * @param id 主键
	 * @return 对应的信息
	 */
	private static Object getFromDB(Long id){
		//从数据库查询出相关信息,并返回你需要缓存的信息
		//TODO 这里作为示例 直接返回null避免编译报错
		return null;
	}
	
	/**
	 * 
	 * @param id 主键id
	 * @return 对应的实体类
	 * @throws NullPointerException if specified id is null
	 */
	//该方法对外提供,封装了取数逻辑,外部不用管到底是从缓存还是从数据库获取的
	public static Object get(Long id){
		//至于 是否支持返回null 依赖于提供的valueFactory(这里是Test.getFromDB(id))的返回值(如果返回null将不会被缓存而是直接return给调用者)
		return cache.get(id);
	}
}

如果并发比较高则可能会导致valueFactory.apply(key)被多次调用,如果valueFactory.apply(key)值的计算过程(当前例子这里是getFromDB(id)的调用过程)比较耗时、耗资源的话,那么需要作限制,限制为 当前只有一个线程在执行计算/调用,其他线程等待计算/调用结果。代码如下:

import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

public class Test {

	private static final ConcurrentMap>
	LOCK = new ConcurrentHashMap<>();
	
	//Object可替换为你需要缓存的Bean实例
	private static final SoftCache cache = 
			new SoftCache<>(id -> Test.getFromDB(id));
	
	/**
	 * 根据主键从数据库获取相关信息
	 * @param id 主键
	 * @return 对应的信息
	 */
	//见《Java并发编程实战》5.6节-构建高效且可伸缩的结果缓存
	private static Object getFromDB(Long id){
		Future f = LOCK.get(id);
		if(f==null){
			Callable eval = new GetFromDBCallable(id);
			FutureTask ft = new FutureTask(eval);
			f = LOCK.putIfAbsent(id, ft);
			if(f==null){//successfully put
				f = ft;
				ft.run();
			}
		}
		try{
			return f.get();
		} catch (InterruptedException | ExecutionException e) {
			// 根据你的业务需求进行处理 throw RuntimeException or return null
			e.printStackTrace();
			//TODO 这里作为示例 直接返回null
			return null;
		} finally{
			//无论这次取值成功与否 都要归还当前占位
			LOCK.remove(id, f);
		}
	}
	
	private static class GetFromDBCallable implements Callable{

		private final long id;
		public GetFromDBCallable(long id) {
			this.id = id;
		}
		
		@Override
		public Object call() throws Exception {
			return GetFromDBCallable.doGetFromDB(id);
		}
		
		private static Object doGetFromDB(long id){
			//从数据库查询出相关信息,并返回你需要缓存的信息
			//TODO 这里作为示例 直接返回null避免编译报错
			return null;
		}
	}
	
	/**
	 * 
	 * @param id 主键id
	 * @return 对应的实体类
	 * @throws NullPointerException if specified id is null
	 */
	//该方法对外提供,封装了取数逻辑,外部不用管到底是从缓存还是从数据库获取的
	public static Object get(Long id){
		//至于 是否支持返回null 依赖于提供的valueFactory(这里是Test.getFromDB(id))的返回值(如果返回null将不会被缓存而是直接return给调用者)
		return cache.get(id);
	}
}

由于ReferenceCache.map存的是最终的值(valueFactory计算后的值),因此ReferenceCache无法通过putIfAbsent()提前占位来避免并发环境下多次调用valueFactory.apply(key)的问题,所以 只能在外部 再套一层(见上面的例子)来解决。

另一个问题是,如果缓存的信息有变化 则需要更新对应的缓存。单节点环境下 这个没什么问题,直接封装并调用cache.remove(id)方法即可。如果是集群(多节点)环境,就要考虑其他节点对应数据缓存是否更新到的问题。一个方法是 轮询版本号:为缓存提供一个中央版本号,每次更新时 中央版本号+1,在调用处 每次调用时本地版本号与中央版本号比对,如果不一致则更新本地缓存并更新本地版本号为中央版本号的值。缺点就是 每次都要获取一次中央版本号。另一个方法就是 采用消息通知的模式 如Redis的订阅/发布功能。代码如下:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.function.Consumer;

import com.test.redis.RedisSubscribeThreadManager;
import com.test.redis.ReidsMessageConsumerManager;
import com.test.redis.SubscribeChannel;

public class Test {

	static{
		//创建并注册Redis订阅事件
		ReidsMessageConsumerManager.register(SubscribeChannel.CACHE_UPDATE,
				new Consumer(){
					/**
					 * 收到需要更新缓存的信息.假设消息格式为 以英文逗号分隔的id
					 */
					@Override
					public void accept(String message) {
						List ids = getLongsBySplitComma(message);
						for(Long id:ids){
							Test.remove(id);
						}
					}

				});
	}
	
	private static final ConcurrentMap>
	LOCK = new ConcurrentHashMap<>();
	
	//Object可替换为你需要缓存的Bean实例
	private static final SoftCache cache = 
			new SoftCache<>(id -> Test.getFromDB(id));
	
	/**
	 * 根据主键从数据库获取相关信息
	 * @param id 主键
	 * @return 对应的信息
	 */
	//见《Java并发编程实战》5.6节-构建高效且可伸缩的结果缓存
	private static Object getFromDB(Long id){
		Future f = LOCK.get(id);
		if(f==null){
			Callable eval = new GetFromDBCallable(id);
			FutureTask ft = new FutureTask(eval);
			f = LOCK.putIfAbsent(id, ft);
			if(f==null){//successfully put
				f = ft;
				ft.run();
			}
		}
		try{
			return f.get();
		} catch (InterruptedException | ExecutionException e) {
			// 根据你的业务需求进行处理 throw RuntimeException or return null
			e.printStackTrace();
			//TODO 这里作为示例 直接返回null
			return null;
		} finally{
			//无论这次取值成功与否 都要归还当前占位
			LOCK.remove(id, f);
		}
	}
	
	private static class GetFromDBCallable implements Callable{

		private final long id;
		public GetFromDBCallable(long id) {
			this.id = id;
		}
		
		@Override
		public Object call() throws Exception {
			return GetFromDBCallable.doGetFromDB(id);
		}
		
		private static Object doGetFromDB(long id){
			//从数据库查询出相关信息,并返回你需要缓存的信息
			//TODO 这里作为示例 直接返回null避免编译报错
			return null;
		}
	}
	
	/**
	 * 以逗号分隔的long字符串转为List
	 * @param strDigits
	 * @return
	 * @throws RuntimeException 如果其中的字符无法转换为long类型
	 */
	private static List getLongsBySplitComma(String strDigits){
		if(strDigits==null||strDigits.isEmpty())
			return Collections.emptyList();
		
		String[] digis = strDigits.split(",");
		List list = new ArrayList<>(digis.length);
		for(String digi:digis){
			try {
				list.add(Long.valueOf(digi));
			} catch (Exception e) {
				throw new RuntimeException("["+digi+"]无法转换成long类型");
			}
		}
		return list;
	}
	
	/**
	 * 
	 * @param id 主键id
	 * @return 对应的实体类
	 * @throws NullPointerException if specified id is null
	 */
	//该方法对外提供,封装了取数逻辑,外部不用管到底是从缓存还是从数据库获取的
	public static Object get(Long id){
		//确保存在活动的订阅线程.如果当前订阅线程已死亡,则新建一个订阅线程
		RedisSubscribeThreadManager.ensureSubscribeThreadAlive();
		//至于 是否支持返回null 依赖于提供的valueFactory(这里是Test.getFromDB(id))的返回值(如果返回null将不会被缓存而是直接return给调用者)
		return cache.get(id);
	}
	
	/**
	 * 清空指定项的缓存
	 * @param id 需要清空缓存的id
	 */
	public static void remove(Long id){
		cache.remove(id);
	}
}

由于redis订阅会一直阻塞到取消订阅,如果有多个业务且每个业务都启一个线程去订阅的话,可能会显得有点浪费(当然这得看具体业务了)。这里封装了多个订阅业务可以共用一个订阅线程,通过调用ReidsMessageConsumerManager.register(channel,consumer)注册订阅消息的消费者会自动触发订阅线程开启(并且能在线程意外退出时自动进行一定次数的重试)。相比每个业务分别启一个订阅线程,这种共用一个订阅线程可能存在的问题是:当某个消息的消费者处理了很长时间,其他待处理的消息可能会等很久。因此使用哪个策略 得看业务需求了。

ReidsMessageConsumerManager.java:

package com.test.redis;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

/**
 * 
 * <频道-频道消息处理器>管理器.
* 统一管理每对<频道-频道消息处理器>(通过{@link #register}方法注册到管理器中), * 以便消息订阅者收到发布的消息时可以通过{@link #get(String) get}方法获取对应的处理器对收到的消息进行处理. *
注意:在{@link SubscribeChannel}定义过的频道名称才会自动订阅 * (通过{@link SubscribeChannel#getChannels()}返回需要自动订阅的频道名称) * */ public class ReidsMessageConsumerManager { private static final Map> channelConsumers = new ConcurrentHashMap<>(); static{ String[] channels = SubscribeChannel.getChannels(); if(channels.length>0){ SubscribeManager.subscribe(new SubscribeRunnable(new ReidsMessageListener(), channels)); } } /** * 将一对<频道-频道消息处理器>注册到管理器中以便消息订阅者收到发布的消息时可以通过get方法获取对应的处理器对收到的消息进行处理 * @param channel 频道名称 * @param consumer 与该频道对应的消息处理器 * @throws NullPointerException if any arg is null */ public static void register(String channel,Consumer consumer){ channelConsumers.put(channel, consumer); } /** * 将指定频道从当前管理器中移除(在SubscribeChannel定义的频道则不允许移除) * @param channel 需要移除的频道名称 * @return true 移除成功,false 移除失败 * @throws NullPointerException if specified channel is null */ public static boolean unregister(String channel){ if(SubscribeChannel.isDefined(channel)){ return false; } return channelConsumers.remove(channel)!=null; } /** * 获取指定频道对应的消息处理器.如果该<频道-频道消息处理器>还没注册过则返回null * @param channel 频道名称 * @return 对应的消息处理器Consumer 如果有的话,否则返回null * @throws NullPointerException if the specified channel is null */ public static Consumer get(String channel){ return channelConsumers.get(channel); } }

SubscribeRunnable.java:

package com.test.redis;



import java.util.Objects;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import redis.clients.jedis.exceptions.JedisException;


public class SubscribeRunnable implements ResubscribedRunnable{
	
	private static final Log log = LogFactory.getLog(SubscribeRunnable.class);

	private final JedisPubSub jedisPubSub;
	
	private final String[] channels;
	
	private boolean needToResubscribe = true;
	
	/**
	 * 构造一个SubscribeRunnable对象
	 * @param jedisPubSub 消息处理器
	 * @param channels 需要订阅的频道名称
	 * @throws NullPointerException if any arg is null
	 * @throws IllegalArgumentException if channels is empty
	 */
	public SubscribeRunnable(JedisPubSub jedisPubSub, String... channels) {
		if(channels.length==0){
			throw new IllegalArgumentException("empty channels!");
		}
		this.jedisPubSub = Objects.requireNonNull(jedisPubSub);
		this.channels = channels;
	}
	
	@Override
	public void run() {
		Jedis jedis = null;
		try{
			jedis = RedisPool.get();
			//subscribe()方法会一直阻塞直到取消订阅(或连接中断)
			jedis.subscribe(jedisPubSub, channels);
			this.needToResubscribe = jedisPubSub.isSubscribed();
		} catch(JedisException e){
			log.error("redis 消息订阅 发生异常:"+e.toString(), e);
		} finally{
			RedisPool.close(jedis);
		}
	}

	/**
	 * {@inheritDoc}
	 * 

Default implementation does nothing. */ public void failToResubscribe(Throwable t){} /** * Returns true if should continue to resubscribe when abort unexpectedly * @return {@code true} if should continue to resubscribe, * else {@code false} */ public boolean isNeedToResubscribe() { return needToResubscribe; } }

ResubscribedRunnable.java:

package com.test.redis;

/**
 * 
 * A class that supports auto resubscribe when abort unexpectedly
 *
 */
public interface ResubscribedRunnable extends Runnable{

	/**
	 * Method invoked when fails to retry to subscribe before quit.
	 * 
	 * @param t the exception that caused termination
	 */
	void failToResubscribe(Throwable t);

	/**
	 * Returns true if should continue to resubscribe when abort unexpectedly
	 * @return {@code true} if should continue to resubscribe,
     *         else {@code false}
	 */
	boolean isNeedToResubscribe();
}

SubscribeChannel.java:

package com.test.redis;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * 
 * Redis订阅/发布 频道名称定义.
 * 
在这里定义的频道名称会自动被订阅(在{@link ReidsMessageConsumerManager ReidsMessageConsumerManager} * 加载时执行订阅),不需要再手动调用Jedis.subscribe() */ // (灵活一点的 可配置在数据库或配置文件里,这里直接定义在代码里了) public final class SubscribeChannel { /**缓存更新频道名称**/ public static final String CACHE_UPDATE = "CACHE_UPDATE"; /** * 判断指定频道名称是否已在该类定义过 * @param channel 需要判断的频道名称 * @return true 已定义,false 未定义 */ public static boolean isDefined(String channel){ return channels.contains(channel); } /** * 获取当前定义的所有的频道名称 * @return 所有的频道名称 */ public static String[] getChannels(){ return channels.toArray(new String[channels.size()]); } /** * 记录当前定义的所有的频道名称 */ private static final Set channels; static{ Set fieldVal = new HashSet<>(); Field[] pubFields = SubscribeChannel.class.getFields(); Field[] decFields = SubscribeChannel.class.getDeclaredFields(); outer: for(Field pub:pubFields){ int mod = pub.getModifiers(); //public static final String if(Modifier.isStatic(mod)&&Modifier.isFinal(mod)&&pub.getType()==String.class){ for(Field dec:decFields){ if(pub.equals(dec)){ Object val = null; try { val = pub.get(SubscribeChannel.class); } catch (IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(); } if(val instanceof String){ fieldVal.add((String)val); } continue outer; } } } } channels = Collections.unmodifiableSet(fieldVal); } }

ReidsMessageListener.java:

package com.test.redis;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;

import redis.clients.jedis.JedisPubSub;

/**
 * 
 * Redis订阅/发布-消息监控器
 *
 */
public class ReidsMessageListener extends JedisPubSub{

	
	/**
	 * 收到发布的消息
	 */
	@Override
	public void onMessage(String channel, String message) {
		Consumer consumer = ReidsMessageConsumerManager.get(channel);
		if(consumer!=null){
			consumer.accept(message);
		}
	}

	/**
	 * 收到发布的消息
	 */
	@Override
	public void onPMessage(String pattern, String channel, String message) {
		onMessage(channel, message);
	}

	/**
	 * 收到指定频道的订阅事件
	 */
	@Override
	public void onSubscribe(String channel, int subscribedChannels) {
		// ignore
		
	}

	/**
	 * 收到指定频道的退订阅事件
	 */
	@Override
	public void onUnsubscribe(String channel, int subscribedChannels) {
		ReidsMessageConsumerManager.unregister(channel);
	}

	@Override
	public void onPUnsubscribe(String pattern, int subscribedChannels) {
		// ignore
	}

	@Override
	public void onPSubscribe(String pattern, int subscribedChannels) {
		// ignore
	}

}

RedisMessagePublisher.java:

package com.test.redis;

import java.util.Objects;

import redis.clients.jedis.Jedis;

/**
 * 
 * Redis消息发布管理器
 *
 */
public class RedisMessagePublisher {

	/**
	 * 向指定频道发布消息
	 * @param channel 需要发布消息的频道
	 * @param message 需要发布的消息
	 * @throws NullPointerException if any arg is null
	 * @throws IllegalArgumentException 
	 * if the channel haven't been defined in the class {@link com.test.redis.SubscribeChannel SubscribeChannel}
	 */
	public static void publish(String channel, String message){
		Objects.requireNonNull(channel);
		Objects.requireNonNull(message);
		Jedis jedis = null;
		try {
			jedis = RedisPool.get();
			jedis.publish(channel, message);
		} finally{
			RedisPool.close(jedis);
		}
	}
}

RedisPool.java:

package com.test.redis;

import redis.clients.jedis.Jedis;

/**
 * 
 * 这里是你的Redis管理器的实现
 *
 */
public final class RedisPool {

	/**
	 * 从资源池里获取一个Jedis连接
	 * @return Jedis
	 */
	public static Jedis get(){
		//TODO 作为示例 直接返回null
		return null;
	}
	
	/**
	 * 将指定的Jedis连接归还给资源池
	 * @param jedis
	 */
	public static void close(Jedis jedis){
		//TODO 你的实现
	}
}

SubscribeManager.java:

package com.test.redis;

import java.util.Objects;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


/**
 * 
 * A class that manages the subscribe of redis for resubscribe automatically
 * as need when abort unexpectedly
 *
 */
public class SubscribeManager {
	
	private SubscribeManager(){}
	
	private static final ThreadPoolExecutor EXECUTOR;

	private static final int MAX_SUBSCRIBE_SIZE = 20; //Adjust the size if needed
	
	static{
		ThreadPoolExecutor executor = new ThreadPoolExecutor(
				MAX_SUBSCRIBE_SIZE, MAX_SUBSCRIBE_SIZE, 1L, TimeUnit.NANOSECONDS,
	            new LinkedBlockingQueue(),ThreadFactorys.newFactory("RedisSubscribe")){
			@Override
			public void afterExecute(Runnable r, Throwable t){
				DelegateResubscribedRunnable delegate;
				if( r instanceof DelegateResubscribedRunnable){
					delegate = (DelegateResubscribedRunnable) r;
				}else{
					ResubscribedRunnable sr = (ResubscribedRunnable) r;
					if(!sr.isNeedToResubscribe()){
						sr.failToResubscribe(t);
						return;
					}
					delegate = new DelegateResubscribedRunnable(sr);
				}
				
				boolean needEnqueue = delegate.trySleepAndReturnIsNeedEnqueue();
				if(!needEnqueue||!getQueue().offer(delegate)){
					delegate.failToResubscribe(t);
				}
			}
			
			@Override
			public int prestartAllCoreThreads() {
				// Not allowed
				return 0;
			}
		};
		executor.allowCoreThreadTimeOut(true);
		EXECUTOR = executor;
	}
	
	/**
	 * Stars a thread to do the subscribe using the specified ResubscribedRunnable
	 * and it will resubscribed automatically as need when abort unexpectedly
	 * @param r the {@code ResubscribedRunnable}
	 * @throws NullPointerException if the specified ResubscribedRunnable is null
	 */
	public static void subscribe(ResubscribedRunnable r){
		EXECUTOR.execute(Objects.requireNonNull(r));
	}
	
	/**
	 * Returns the current number of {@code ResubscribedRunnable} in the pool.
	 * @return the number of {@code ResubscribedRunnable}
	 */
	public static int getCurrentSubscribedSize(){
		return EXECUTOR.getActiveCount();
	}
	
	/**
	 * Returns the maximum allowed number of {@code ResubscribedRunnable}.
	 * @return the maximum allowed number of {@code ResubscribedRunnable}.
	 */
	public static int getMaximumSubscribeSize(){
		return MAX_SUBSCRIBE_SIZE;
	}
	
	private static class DelegateResubscribedRunnable implements ResubscribedRunnable{
		private static final long TIME_LIMIT = 1 * 60 * 1000;
		private static final int MAX_RETRY_COUNT = 10;
		private final ResubscribedRunnable delegate;
		private long lastTime;
		private int continuousRetryCount;
		public DelegateResubscribedRunnable(ResubscribedRunnable r) {
			delegate = r;
		}

		@Override
		public void run() {
			delegate.run();
		}
		
		public boolean trySleepAndReturnIsNeedEnqueue(){
			if(!isNeedToResubscribe()){
				return false;
			}
			if(lastTime>0&&System.currentTimeMillis()0){
					if(continuousRetryCount>MAX_RETRY_COUNT){
						return false;
					}
					sleep(30*continuousRetryCount);
				}
			}else{
				continuousRetryCount = 1;
			}
			lastTime = System.currentTimeMillis();
			return true;
		}
		
		private void sleep(int seconds){
			if(seconds<=0){
				return;
			}
			long remaining = seconds*1000;
			final long last = System.currentTimeMillis()+remaining;
			for(;;){
				try {
					if(remaining<=0){
						return;
					}
					Thread.sleep(remaining);
				} catch (InterruptedException e) {
					//ignored
				}
				remaining = last-System.currentTimeMillis();
			}
			
		}

		@Override
		public void failToResubscribe(Throwable t) {
			delegate.failToResubscribe(t);
			
		}

		@Override
		public boolean isNeedToResubscribe() {
			return delegate.isNeedToResubscribe();
		}

		
	}
}

ThreadFactorys.java

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 
 * A utility class to offer an implementation of {@code ThreadFactory}
 * conveniently which is the same as 
 * {@code java.util.concurrent.Executors$DefaultThreadFactory}
 * but will offer a more specific thread name by the specified
 * parameter {@code threadNamePrefix}
 *
 */
public final class ThreadFactorys {

	private ThreadFactorys(){}
	
	/**
	 * Creates a new {@code ThreadFactory} with specified thread name prefix.
	 * The thread name will be "{@code threadNamePrefix-PoolThread-n}",
	 * where {@code threadNamePrefix} is the parameter passed in, and the 
	 * {@code n} is the number of threads that have been created via this ThreadFactory.
	 * @param threadNamePrefix the thread name prefix
	 * @return a new {@code ThreadFactory}
	 */
	public static ThreadFactory newFactory(String threadNamePrefix){
		//Copy from java.util.concurrent.Executors$DefaultThreadFactory
		//to offer a more specific thread name
		return new ThreadFactory(){

			private final AtomicInteger threadNumber = new AtomicInteger(1);
			private final String namePrefix = threadNamePrefix+"-PoolThread-";
			
			@Override
			public Thread newThread(Runnable r) {
	            Thread t = new Thread(r, 
	            		namePrefix + threadNumber.getAndIncrement());
	            if (t.isDaemon())
	                t.setDaemon(false);
	            if (t.getPriority() != Thread.NORM_PRIORITY)
	                t.setPriority(Thread.NORM_PRIORITY);
	            return t;
			}
			
		};
	}
}

通过调用

RedisMessagePublisher.publish(SubscribeChannel.CACHE_UPDATE, "替换为你的需要更新缓存的信息");

可向集群发送更新缓存的通知,有注册对应Redis订阅事件的 收到更新通知时即可对相应的缓存进行更新。消息通知的可靠性 依赖于redis的订阅/发布机制,据了解 redis发布只发布一次 不管订阅的客户端是否有收到消息(比如刚好连接断开了),此时可能导致相应节点的缓存没更新到。虽然redis的订阅/发布不能保证百分百的准确 但是其可靠性应该还是可以的。如果确实需要强可靠性,可考虑其他的消息机制。

还有一种场景,需要由两个参数生成一个对应的key或者value(即BiFunction):

BiReferenceCache.java:

import java.lang.ref.ReferenceQueue;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiFunction;
import java.util.function.Supplier;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Cache mapping a {@code key -> value}. values are weakly(or softly) but keys are strongly referenced.
 * Keys are calculated from keys {@code key1}, {@code key2} passed to corresponding methods using the
 * {@code keyFactory} function passed to the constructor. Values are calculated from keys {@code key1} or {@code key2} 
 * passed to corresponding methods using the {@code valueFactory} function passed to the constructor. 
 * Keys {@code key1}, {@code key2} and its result {@code key} produced by the {@code keyFactory}
 * can not be {@code null} and are compared by equals while values returned by {@code valueFactory} 
 * can be null and are compared by identity. 
 * Entries are expunged from cache lazily on invocation to {@link #get} method when the WeakReferences(or SoftReferences)
 * to values are cleared. 
 * 
 * 

This is the two-arity specialization of {@link ReferenceCache}. * * @param the type of the first key * @param the type of the second key * @param the type of the result of the key's BiFunction * @param the type of the result of the value's BiFunction */ public abstract class BiReferenceCache { private static Log log ; private final ReferenceQueue refQueue = new ReferenceQueue<>(); private final ConcurrentMap> map = new ConcurrentHashMap<>(); private final BiFunction valueFactory; private final BiFunction keyFactory; /** * Construct an instance of {@code BiReferenceCache} * * @param keyFactory a BiFunction mapping a {@code key1,key2 -> key} * @param valueFactory a BiFunction mapping a * {@code key(produced by key's BiFunction with key1, key2) -> value} * @throws NullPointerException if {@code keyFactory} * or {@code valueFactory} is null. */ protected BiReferenceCache(BiFunction keyFactory, BiFunction valueFactory){ this.keyFactory = Objects.requireNonNull(keyFactory); this.valueFactory = Objects.requireNonNull(valueFactory); } private static Log getLog(){ // lazily init the log if(log==null){ // regardless of the concurrency log = LogFactory.getLog(BiReferenceCache.class); } return log; } /** * Look-up the value through the cache with the key * produced by key's BiFunction with given arguments * {@code key1}, {@code key2}. * @param key1 the first key * @param key2 the second key * @return the cached value (maybe null) * @throws NullPointerException if the specified key * {@code key1} or {@code key2} is null. */ public final VR get(T key1, U key2){ if(key1==null||key2==null){ throw new NullPointerException(); } expungeStaleEntries(); KR key = keyFactory.apply(key1, key2); if(key==null){ return null; } Value cache = map.get(key); Value newCache = null; while(true){ if(cache!=null){ VR value = cache.get(); if(value!=null){ return value; } } // lazily construct a new-CacheEntry if(newCache==null){ // create new value VR value = valueFactory.apply(key1, key2); // if new-value is null then just return it if(value==null){ return null; } // wrap value with CacheValue (WeakReference or SoftReference) newCache = newValue(key, value, refQueue); } if(cache==null){ cache = map.putIfAbsent(key, newCache); if(cache==null){ // successfully put new-cache cache = newCache; } }else{ if(map.replace(key, cache, newCache)){ if(cache==newCache){ //newCache is cleared? getLog().error("should not reach here!---->there is a bug in ReferenceCache? newCache.value=" + newCache.get()+" -->"+newCache.getClass()); return valueFactory.apply(key1, key2); } // successfully replaced cleared CacheEntry with our new-CacheEntry cache = newCache; }else{ // retry with current cache-value cache = map.get(key); } } } } /** * expunge all stale cache entries */ private void expungeStaleEntries(){ Value cache; while ((cache = (Value)refQueue.poll()) != null){ // removing by key and mapped value is always safe here because after a Value // is cleared and enqueue-ed it is only equal to itself // (see Value.equals method)... map.remove(cache.getKey(), cache); } } /** * Removes all of the mappings from this cache. * The cache will be empty after this call returns. */ public final void clear(){ // to remove from refQueue while (refQueue.poll() != null) ; map.clear(); } /** * Removes the key (and its corresponding value) from this cache. * This method does nothing if the key is not in the cache. * * @param key the key that needs to be removed * @return the previous value associated with {@code key}, or * {@code null} if there was no mapping for {@code key} * ( or already cleared) * @throws NullPointerException if the specified key is null */ public final VR remove(Object key){ expungeStaleEntries();// to remove from refQueue Value val = map.remove(key); if(val!=null){ return val.get(); } return null; } /** * Removes the key (which is produced by key's BiFunction * with given arguments {@code key1}, {@code key2}) * and its corresponding value from this cache. * This method does nothing if the key is not in the cache. * * @param key1 the first key * @param key2 the second key * @return the previous value associated with {@code key} * (which is produced by key's BiFunction with given arguments * {@code key1}, {@code key2}), or {@code null} if there was no mapping * for {@code key} ( or already cleared ) * @throws NullPointerException if the specified key * {@code key1} or {@code key2} is null */ public VR remove(T key1, U key2){ KR key = getKey(key1, key2); if(key==null){ return null; } return remove(key); } /** * Applies key's BiFunction to the given arguments * and then returns the function result. * @param key1 the first key * @param key2 the second key * @return the function result * @throws NullPointerException if the specified key * {@code key1} or {@code key2} is null */ public KR getKey(T key1, U key2){ if(key1==null||key2==null){ throw new NullPointerException(); } return keyFactory.apply(key1, key2); } /** * Constructs a new {@code Value} (WeakReference or SoftReference) * with specified key, value and ReferenceQueue. * @param key the key * @param value the value * @param refQueue the ReferenceQueue * @return a new Value */ protected abstract Value newValue(KR key, VR value, ReferenceQueue refQueue); /** * Common type of value suppliers that are holding a referent. * The {@link #equals} and {@link #hashCode} of implementations is defined * to compare the referent by identity and cleared Value is only equal to itself. * * @param type of keys * @param type of values */ protected static interface Value extends Supplier { /** * Gets the key. * * @return key */ KR getKey(); } }

BiSoftCache.java:

import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Objects;
import java.util.function.BiFunction;

/**
 * Cache mapping a {@code key -> value}. values are softly but keys are strongly referenced.
 * Keys are calculated from keys {@code key1}, {@code key2} passed to corresponding methods using the
 * {@code keyFactory} function passed to the constructor. Values are calculated from keys {@code key1} or {@code key2} 
 * passed to corresponding methods using the {@code valueFactory} function passed to the constructor. 
 * Keys {@code key1}, {@code key2} and its result {@code key} produced by the {@code keyFactory}
 * can not be {@code null} and are compared by equals while values returned by {@code valueFactory} 
 * can be null and are compared by identity. 
 * Entries are expunged from cache lazily on invocation to {@link #get} method when the SoftReferences
 * to values are cleared. 
 * 
 * 

This is the two-arity specialization of {@link SoftCache}. * * @param the type of the first key * @param the type of the second key * @param the type of the result of the key's BiFunction * @param the type of the result of the value's BiFunction */ public class BiSoftCache extends BiReferenceCache { /** * Construct an instance of {@code BiSoftCache} * * @param keyFactory a BiFunction mapping a {@code key1,key2 -> key} * @param valueFactory a BiFunction mapping a * {@code key(produced by key's BiFunction with key1, key2) -> value} * @throws NullPointerException if {@code keyFactory} * or {@code valueFactory} is null. */ public BiSoftCache(BiFunction keyFactory, BiFunction valueFactory) { super(keyFactory, valueFactory); } /** * Constructs a new {@code Value} using SoftReference * with the given arguments */ @Override protected Value newValue( KR key, VR value, ReferenceQueue refQueue) { return new CacheValue(key, value, refQueue); } /** * CacheValue containing a softly referenced {@code value}. It registers * itself with the {@code refQueue} so that it can be used to expunge * the entry when the {@link BiSoftCache} is cleared. */ private static final class CacheValue extends SoftReference implements Value { private final int hash; private final KR key; private CacheValue(KR key, VR value, ReferenceQueue refQueue) { super(value, refQueue); this.hash = System.identityHashCode(value); // compare by identity this.key = Objects.requireNonNull(key); } @Override public int hashCode() { return hash; } @Override public boolean equals(Object obj) { VR value; return obj == this || obj != null && obj.getClass() == this.getClass() && // cleared CacheValue is only equal to itself (value = this.get()) != null && // compare value by identity value == ((CacheValue) obj).get(); } public KR getKey() { return key; } } }

BiWeakCache.java:

import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.function.BiFunction;

/**
 * Cache mapping a {@code key -> value}. values are weakly but keys are strongly referenced.
 * Keys are calculated from keys {@code key1}, {@code key2} passed to corresponding methods using the
 * {@code keyFactory} function passed to the constructor. Values are calculated from keys {@code key1} or {@code key2} 
 * passed to corresponding methods using the {@code valueFactory} function passed to the constructor. 
 * Keys {@code key1}, {@code key2} and its result {@code key} produced by the {@code keyFactory}
 * can not be {@code null} and are compared by equals while values returned by {@code valueFactory} 
 * can be null and are compared by identity. 
 * Entries are expunged from cache lazily on invocation to {@link #get} method when the WeakReferences
 * to values are cleared. 
 * 
 * 

This is the two-arity specialization of {@link WeakCache}. * * @param the type of the first key * @param the type of the second key * @param the type of the result of the key's BiFunction * @param the type of the result of the value's BiFunction */ public class BiWeakCache extends BiReferenceCache{ /** * Construct an instance of {@code BiWeakCache} * * @param keyFactory a BiFunction mapping a {@code key1,key2 -> key} * @param valueFactory a BiFunction mapping a * {@code key(produced by key's BiFunction with key1, key2) -> value} * @throws NullPointerException if {@code keyFactory} * or {@code valueFactory} is null. */ public BiWeakCache(BiFunction keyFactory, BiFunction valueFactory){ super(keyFactory, valueFactory); } /** * Constructs a new {@code Value} using WeakReference * with the given arguments */ @Override protected Value newValue( KR key, VR value, ReferenceQueue refQueue) { return new CacheValue(key, value, refQueue); } /** * CacheValue containing a weakly referenced {@code value}. It registers * itself with the {@code refQueue} so that it can be used to expunge * the entry when the {@link WeakReference} is cleared. */ private static final class CacheValue extends WeakReference implements Value{ private final int hash; private final KR key; private CacheValue(KR key, VR value, ReferenceQueue refQueue) { super(value, refQueue); this.hash = System.identityHashCode(value); // compare by identity this.key = Objects.requireNonNull(key); } @Override public int hashCode() { return hash; } @Override public boolean equals(Object obj) { VR value; return obj == this || obj != null && obj.getClass() == this.getClass() && // cleared CacheValue is only equal to itself (value = this.get()) != null && // compare value by identity value == ((CacheValue) obj).get(); } public KR getKey() { return key; } } }

----------------------------------------完----------------------------------------------------------------

(注:该文主要是想记录自己的日常学习并顺便分享一下,对于上述代码并没有进行过任何的并发测试)

 

 

你可能感兴趣的:(原创)