对使用队列解决缓存一致性的思考(二)

对使用队列解决缓存一致性的思考(二)

思考

在上篇博客中,有具体的给出了使用队列来解决缓存一致性的方案,就是下面的这个流程:

在这个方案中,我们在请求过来时,首先绝大部分的查询请求直接通过缓存查到了,就直接返回了,不会进入到队列,只有少部分的查询请求以及少量的跟新请求可以进入到队列里来。然后,在队列的另外一端,都会有一个对应的线程来消费它,并且消费完后通知被阻塞线程处理成功。

这是一个不错的方案,比如有每秒有3000的查询进来,大概有10%没有查到,进入到队列中,还有每秒50的更新请求,那么总共就是每秒350的请求进来,如果再算上查询去重,假如是300请求每秒的量进入到队列中,我们还是可以去消费的,这里主要一个设置的点就是队列的数量,队列的数量多的话,比如每个服务给个50个队列,那就是平均每个队列每秒消费6个请求。

这里对于队列数量的考虑因素:主要是队列的增加会涉及到对应的线程数的增加,而线程资源又是服务器的一个很重要的资源,所以必须要慎重的考虑:
那么,对于一个服务器的线程资源来说,我个人觉考虑因素得如下:

  1. 每一个线程的创建和销毁都是会需要一定的开销的,所以一般来说,所以尽量用线程池来尽量的优化线程的创建和销毁。
  2. 每个线程的创建需要大概1M左右的内存,创建的线程数过多也会消耗大量的内存。
  3. 线程数一多,更加频繁的上下文切换,会消耗CPU性能,使得CPU的真正的工作时间的利用率减少,白白花非时间在等待上下文切换完成上。
  4. 上面是线程数过多的一些不利因素,那么我们就应该尽量的在不影响性能的前提下多提升线程数,获得一个好的平衡。
  5. 一般来说,如果所做的任务是一些CPU密集型的工作,比如大量的算法计算等,那我们就不能创建太多的线程。因为即使线程多,可是每个线程花费的时间都比较长,可能会经常处于一个CPU 100%的状态,这样的话会有很多的线程得不到快速的响应。
  6. 那么,如果是一些IO密集型的任务,我们确实可以适当增多线程的数量,因为这样的话,每个线程执行到IO操作就被阻塞了,CPU不会是瓶颈所在。

根据上面的几点,差不多可以看到,我们这里基本也都是查数据库、查redis的IO操作,所以说线程数设置个50应该没啥问题。

一些想法

这里的队列多少,就关系到程序的性能,相当于像是hashmap一样把锁给细粒度化了,但是这里却一直受到线程数量的限制,那也没见hashmap底层用线程池来干啥呀。
在这里插入图片描述
(图逻辑写的不是很清楚)
我这里主要是每个队列都配了一个线程来消费它,这点是直接原因。

那么能不能让原来的线程自己去消费自己的请求呢?首先这个线程的请求先进队列,如果说自己前面没有请求,那么就直接执行请求,如果前面有请求,那就把自己的线程注册到前一个请求里然后阻塞自己,当前一个线程执行完以后就会通知它记录的下一个线程去执行,一直这样循环往复,这样就不需要有专门的线程来消费它了。
在这里插入图片描述

可是这怎么看着这么眼熟呢?越看越像AQS,越看越像!
其实这就是AQS啊,那我们我们其实就可以使用利用了AQS的ReentrantLock来这样做了。

新的方案:ReentrantLock

利用ReentrantLock的公平锁来实现:

  1. 前面的Hash路由的操作都是一样的,然后存个reentrantLock的List表,通过哈希取模后的索引获取list中对应的锁
  2. 每一个原来要入队列的请求,在那里直接使用 lock.tryLock() 来实现,这样的话,原来是整个请求入队列,现在是线程入AQS队列,按照上面说的逻辑,公平锁的方式,一个一个的互相通知的来。

原来,使用队列的优化,到最后竟然是用锁来实现呀!

流程图如下:
在这里插入图片描述
文件图:
在这里插入图片描述

源代码如下:

  • 请求基类:
public abstract class Request<T> {
    String key;
    T result;

    /**
     * 执行请求
     */
    abstract void execute();

    public String getKey() {
        return key;
    }

    public T getResult() {
        return result;
    }
}
  • 查询请求类:
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 {
    private static final int LOCKER_NUM = 200;
    private List<ReentrantLock> lockList;
    private static LockerManager manager = new LockerManager();//静态内部类?

    public static LockerManager getInstance() {
        return manager;
    }

    private LockerManager() {
        initial();
    }

    private void initial() {
        lockList = new ArrayList<>(LOCKER_NUM);
        for (int i = 0; i < LOCKER_NUM; i++) {
            lockList.add(new ReentrantLock(true));
        }
    }

    /**
     * 请求路由,发送队列
     *
     * @param request 请求
     * @return
     */
    public ReentrantLock getLocker(Request request) {
        int h = request.getKey().hashCode();
        int hash = request.getKey() == null ? 0 : h ^ (h >>> 16);
        int index = hash & (LOCKER_NUM - 1);
        return lockList.get(index);
    }
}
  • 供业务调用的接口类
@Service
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);
        Lock 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);
        Lock lock = LockerManager.getInstance().getLocker(request);
        try {
            if (lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
                request.execute();
            } else {
                System.out.println("超时");
                throw new Throwable("更新超时");
            }
        } finally {
            lock.unlock();
        }
    }
}

可以看到,相对于上一个方案,少了一个RedisQueue的类,之前在那里是有查询去重的功能的,现在这里队列交由ReentrantLock的AQS来管理了,这里有一个缺点就是没有查询去重了。不过,这也影响不大,查询请求操作会有一个二次检查,在redis里查询到就返回了,依然不会使得查询数据库的操作增多。

同时原本的更新操作的事务的代码,现在不会自动加上去了,现在可以直接使用事务注解来实现,原来是因为交给线程池线程去做了,需要采用编程式事务,但是现在请求的执行是由原来的这一个线程来执行的,没有涉及到线程池,因此可以放心的使用@transactional注解。

最最重要的一点,原来我考虑加几个队列都得考虑这考虑那的,上个方案50个队列还行,让我上200个队列就感觉不行,现在用这个方案,200把锁,200个AQS队列还是随意的,这样才能算得上是和hashmap一样将锁的粒度细化了。

(小声说一句,这就好比以前在看hystrix的时候,线程的资源隔离也是通过线程池隔离来实现的,所以网上总有把这个新增了线程当做缺点来和其他的方案来对比)

此方案的项目源码放到了我的github地址中:github地址

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值