没事就喜欢研究这些东西,于是对之前做的使用队列解决缓存一致性有了新的思考。下面只是记录我的思考思路,欢迎大家一起来讨论。
思考
在上篇博客中,有具体的给出了使用ReentrantLock来解决缓存一致性的方案:
- 链接: 对使用队列解决缓存一致性的思考(二).
在上个方案中,已经可以利用ReentrantLock来代替线程+阻塞队列实现的消费模式。这样做的有点就是不需要开启多余的线程进行消费,可以节省线程资源,并且可以放心大胆的设置更多的锁,让锁的粒度更小一点。那么有没有一种方式可以让锁的粒度更小呢?或者说不受制于数量限制呢?
最开始想的是每一个请求过来时,通过hash去路由获取到指定的锁,每一个请求,只要key不同都能获取到不同的锁,那么在进行同步操作的时候锁的粒度是最小的。但是为了让对于相同key的请求能够获取到同一个锁,我们需要用一个HashMap来存储hash到锁的映射关系,每次请求都会过来先判断系统里有没有这个key的锁,有的话则取这个锁,没有的话则新建一个锁并存入到这个HashMap中去。
那么现在就会有一个问题,随着访问量不断增多,存储在HashMap中的数据也会越来越大,我们必须想办法让没有在使用或者不需要使用的锁被GC回收掉,但是大家都知道,在HashMap中存入的锁是不会被GC回收的。
因此,接下来的一些想法就是着重于解决怎么去让JVM正确回收掉这些数据的问题。
一些想法
既然是由于HashMap是强引用,不能解决问题,那么第一时间想到的便是WeakHashMap,这个使用虚引用的WeakHashMap有一个特点,就是里面的Key都是虚引用,当这个Map中的Key不再使用时,即这个Key没有其他地方再对它有强引用时,发生GC时便会将他回收掉。
很显然,我们要的就是这种效果,然而在这个例子中,我们需要的并不是Key不再使用时将其回收掉,而是WeakHashMap中的值,因为Key基本是一直没有强引用的,只要一发生GC就会将这个键值对从Map中移除,而这个时候Key没有用了,值可能还是在使用的,这与我们的预期不太一样。
我们的预期是,Map中的值ReentrantLock不再使用时(即不再有强引用时),将其回收掉,所以目前的问题就在于如何解决WeakHashMap中的值没有引用时就将他回收的问题。
方案
(1)、JVM的根可达性算法,将Value挂上对Key的强引用。
当Value存在强引用时,会使得Key也不会被当做垃圾。
因为 KEY——>Value——>某个强引用——>root。
而当Value的某个强引用消失时,即Value不再被使用时,在发生垃圾回收时,Key在这一条链路到达不了root,所以也就变成垃圾可以回收了
(2)、取消WeakHashMap对Value的强引用。
因为WeakHashMap是对Value有强引用的,如果不取消的话就会一直有强引用存在,那么这个键值对是不能被回收的。因此这个Value还要包装一层WeakRenference。
更改后相关的代码如下:
- 查询请求类:
public class QueryRequest<T> extends Request<T> {
private QueryFunction<T> queryFunction;
public QueryRequest(String key, QueryFunction<T> queryFunction) {
this.key = key;
this.queryFunction = queryFunction;
}
@Override
public void execute() {
if (queryFunction != null) {
result = queryFunction.queryExecution(key);
}
}
}
- 更新请求类
public class UpdateRequest<T> extends Request<T> {
private T value;
private UpdateFunction<T> updateFunction;
public UpdateRequest(String key, T value, UpdateFunction<T> updateFunction) {
this.key = key;
this.value = value;
this.updateFunction = updateFunction;
}
@Override
public void execute() {
if (updateFunction != null) {
updateFunction.updateExecution(key, value);
}
}
}
- 查找请求方法的类:
public interface QueryFunction<T> {
/**
* 需要执行查询的动作,一般是查数据库和写入redis的操作
*
* @param key 要查询的key
* @return 查询得到的值
*/
public T queryExecution(String key);
}
- 更新请求方法的类
public interface UpdateFunction<T> {
/**
* 需要执行查询的动作,一般是查数据库和写入redis的操作
*
* @param key 要更新的key
* @param value 要更新的value
*/
public void updateExecution(String key, T value);
}
- 锁管理类
public class LockerManager {
public final Map<Integer, WeakReference<WeakLocker>> lockMap = new WeakHashMap<>();
private static final LockerManager manager = new LockerManager();
public static LockerManager getInstance() {
return manager;
}
/**
* 获取该请求对应的锁
*
* @param request 请求
* @return
*/
public WeakLocker getLocker(Request request) {
int h = request.getKey().hashCode();
int index = request.getKey() == null ? 0 : h ^ (h >>> 16);
WeakReference<WeakLocker> reference;
synchronized (this) {
reference = lockMap.get(index);
if (Objects.isNull(reference)) {
ReentrantLock lock = new ReentrantLock();
Integer key = new Integer(index);
WeakLocker locker = new WeakLocker(key, lock);
reference = new WeakReference<>(locker);
lockMap.put(key, reference);
}
}
return reference.get();
}
}
- 虚引用锁
public class WeakLocker {
/** 仅仅只是为了给这个key添加一个强引用 */
private final Integer key;
private final ReentrantLock lock;
public WeakLocker(Integer key,ReentrantLock lock){
this.key=key;
this.lock=lock;
}
public ReentrantLock getLock(){
return lock;
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException{
return lock.tryLock(timeout,unit);
}
public void unlock(){
lock.unlock();
}
}
@Service("redisQueueService2")
public class RedisQueueService {
/**
* 先从redis缓存获取,获取不到,再执行该方法
*
* @param key 要查询的key
* @param timeout 超时时间,以毫秒为单位
* @param queryFunction 定义查询操作
* @return
*/
public <T> T query(String key, long timeout, QueryFunction<T> queryFunction) throws Throwable {
Request<T> request = new QueryRequest<>(key, queryFunction);
WeakLocker lock = LockerManager.getInstance().getLocker(request);
try {
if (lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
request.execute();
} else {
System.out.println("超时");
throw new Throwable("查询超时");
}
} finally {
lock.unlock();
}
return request.getResult();
}
/**
* 执行更新操作
*
* @param key 要更新的key
* @param value 要更新的值
* @param timeout 超时时间,以毫秒为单位
* @param updateFunction 定义更新操作
*/
public <T> void update(String key, T value, long timeout, UpdateFunction<T> updateFunction) throws Throwable {
Request<T> request = new UpdateRequest<>(key, value, updateFunction);
WeakLocker lock = LockerManager.getInstance().getLocker(request);
try {
if (lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
request.execute();
} else {
System.out.println("超时");
throw new Throwable("更新超时");
}
} finally {
lock.unlock();
}
}
}
结束
以上,就能够实现每一个相同的key一定会获取到同一个锁,不同的key除非hash相同,不然都获取到的是不同的锁,并且有JVM帮助自动回收多余的、不需要的锁。