以下是对正在发生的事情的简要描述 -
执行方案:
搜索请求-->
检查缓存中是否存在此请求-->
的数据(如果存在于缓存中),从缓存中获取,否则从数据库获取并将数据放入缓存
这项工作适用于以下场景
以下是顺序接收
的请求 第一次请求请求-->
检查缓存(不存在,因为这是第一次请求) - >从DB获取并放入缓存
第二次请求请求-->
检查缓存(数据存在,因为之前的请求已使数据在缓存中可用)
第三次请求请求-->
检查缓存(数据仍然存在)
第四次请求请求-->
检查缓存(数据仍然存在)
但是如果多个线程同时请求数据,则失败。
以下是并行收到请求的情况(同时)
第一次请求请求-->
检查缓存(不存在,因为这是第一次请求) - >从DB获取并放入缓存
第二次请求请求-->
检查缓存(不存在,因为这是第一次请求) - >从DB获取并放入缓存
第三次请求请求-->
检查缓存(不存在,因为这是第一次请求) - >从DB获取并放入缓存
第四次请求请求-->
检查缓存(不存在,因为这是第一次请求) - >从DB获取并放入缓存
我没有使用任何同步块,因为这会使它成为顺序执行,对吗?
我如何避免这个问题,以便只有一个线程命中数据库而另一个线程从缓存中选择该数据(特别是在并行执行的情况下)?他们已经存在解决这些问题的任何模式吗?
我知道我将请求混合在线程中,但它们本质上是相同的。
如果看起来很糟糕,可以随意修改这个问题的标题。
答案 0 :(得分:1)
第一个看到该对象的线程在缓存中不存在,创建特殊类的临时对象(一种Future)并将其放在哈希表中。然后启动数据库查询。
后续线程会查看临时对象,但不会查询数据库,但会一直等待结果显示。
第一个线程从DB获取结果并通知其他线程。
临时对象的类可以从头开始创建,也可以基于番石榴的SettableFuture,java8 CompletableFuture或java5 FutureTask创建。
附录
为了确保只有一个并发线程启动数据库提取,当线程测试缓存并插入临时对象时,应锁定整个缓存。结果,访问不同密钥的线程相互竞争。如果访问率很高并且这会导致性能下降,this solution可能会有所帮助。它解释了缓存可以并行测试。对于您的任务,应该更新解决方案,以便将获取的数据放入缓存中。
答案 1 :(得分:0)
您可以使用双重检查锁定(http://en.wikipedia.org/wiki/Double-checked_locking)来确保只有一个线程写入缓存,而其他线程仅使用缓存进行读取。
答案 2 :(得分:0)
这个问题让我想起了懒惰初始化的单例模式。如果没有完成同步,则第一个并行线程可以创建多个实例,从而打破了模式的目标。
你可以从解决单身人士问题的Lazy Initialization holder class idiom中获得灵感。
由于缓存可能很棘手并且充满了陷阱,我建议使用第三方缓存实现。例如,Guava cache已经处理了并发问题,应该对您有用。
答案 3 :(得分:0)
我真的不确定您的问题与延迟初始化有任何相似之处。我认为你的问题可以通过非常简单的解决方案来解决。要使其平行,您可以使用ReadWriteLocks,如:
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock cacheReadLock = readWriteLock.readLock();
private final Lock dbWriteLock = readWriteLock.writeLock();
当您使用writelock(dbWriteLock)从DB更新缓存时,只需从缓存中读取时使用简单的readlock(cacheReadLock)。希望能帮助到你。 如果您希望我添加更多代码示例,请随意发表评论。
答案 4 :(得分:0)
使用原子整数 [比较计数器](初始化为零)并将其传递给所有工作者(线程)。 无论线程在缓存检查之后到达代码点,在进入db之前,都会调用类似这样的内容:
/* some code for read from cache */ /* if not found in cache , then only */ boolean goAhead = counter.compareAndSet(0,1); if(goAhead) { /* some code to read from db */ /* some code to put into cache */ synchronized(counter) { counter.notifyAll(); } } else { synchronized(counter) { counter.wait(); } /* read from cache */ }
请确保仅在缓存为空时调用此(等待和通知)代码。
答案 5 :(得分:0)
另一个简单的解决方案是执行像双重检查锁定之类的操作,因为我们用它来创建单例设计模式。我知道您担心要避免同步,但使用double checked null lock之类的东西只允许一个线程进行同步,其他线程将等待任务完成。
if (fetchFromCache() == null) {
synchronized(this) { // or any monitor lock of choice
if(fetchFromCache == null) {
// code to call db and fill cache data
}
}
}
return fetchFromCache(); // this call will never be null