【问题标题】:Design AppServer Interview Discussion设计 AppServer 面试讨论
【发布时间】:2017-12-23 04:59:10
【问题描述】:

我在最近的一次系统设计面试中遇到了以下问题:

设计一个与缓存和数据库接口的 AppServer。

我想出了这个:

public class AppServer{
    public Database DB;
    public Cache cache;

    public Value get(Key k){
        Value res = cache.get(k);
        if(res == null){
            res = DB.get(k);
            cache.set(k, res);
        }
    }

    public void set(Key k, Value v){
        cache.set(k, v);
        DB.set(k, v);
    }
}

此代码很好并且可以正常工作,但是对问题的跟进是:

  1. 如果有多个线程怎么办?
  2. 如果有多个 AppServer 实例怎么办?
  3. AppServer 性能突然下降很多,我们发现这是因为我们的缓存一直丢失。缓存大小是固定的(已经是最大的了)。我们如何防止这种情况发生?

回复:

  1. 我回答说我们可以使用锁或条件变量。在 Java 中,我们可以在每个方法中添加 Synchronized 以实现互斥,但面试官提到这样做效率不高,只希望同步关键部分。

我以为我们只需要同步void set(Key k, Value v)中的2个set行和Value get(Key k)中的1个set方法,但是面试官要求也同步res = DB.get(k);。我最后同意了他的观点,但并不完全理解。线程没有独立的栈和共享的堆吗?所以当一个线程执行get时,它会将res存储在栈帧上的局部变量中,即使另一个线程顺序执行get,前一个线程也会保留它的get值。然后每个线程设置各自获取的值。

  1. 我们如何处理 AppServer 的多个实例?

我想出了一个像 Kafka 这样的分布式队列解决方案,每次我们执行 set / get 命令时,我们都会将该命令排队,但他也提到 set 是可以的,因为该操作在缓存 / db 中设置了一个值,但是如何你会返回正确的值吗?有人可以解释一下吗?

还有版本控制系统和事件系统的可能解决方案吗?

  1. 可能的解决方案:
    • L1、L2、L3 缓存 - 层和更多缓存
    • 区域/分段缓存 - 为用户组使用不同的缓存。
    • 还有其他想法吗?

将支持所有有见地的回复:)

【问题讨论】:

  • @ElliottFrisch ?
  • 来自链接:“它做什么?”,答案 3“并发”:并行执行。并发感知请求缓存。通过请求折叠自动批处理。
  • @ElliottFrisch 我不完全理解,您能否详细说明并解决问题的其他部分? :)
  • 好奇是否有人知道如何解决这个问题?

标签: java multithreading caching architecture distributed-system


【解决方案1】:

1

虽然 JDBC “假定”是线程安全的,但某些驱动程序不是,我将假设 Cache 也不是线程安全的(尽管大多数缓存应该是线程安全的),所以在这种情况下,你会需要对您的代码进行以下更改:

  1. 将两个字段设为最终字段
  2. 同步整个get(...)方法
  3. 同步整个set(...)方法

假设没有其他方法可以访问上述字段,get(...) 方法的行为取决于两件事:首先,可以看到来自 set(...) 方法的更新,其次,缓存未命中是然后仅由单个线程存储。您需要同步,因为这个想法是只有一个线程在缓存未命中的情况下执行昂贵的数据库查询。如果您不同步整个get(...) 方法,或者您拆分了同步语句,则另一个线程也可能在查找和插入之间看到缓存未命中。

老实说,我回答这个问题的方式就是把整个事情都扔掉。我会看看JCIP 是如何编写缓存的,并以此为基础回答。


2

我认为您的队列解决方案很好。

我相信你的面试官的意思是,如果 AppServer 的另一个实例没有缓存 set(...) 的另一个实例 set(...) 的内容,那么它会在数据库中查找并找到正确的值。如果您使用多个线程,此解决方案将不正确,因为 2 个线程可能是 set(...)ing 冲突值,然后缓存将有 2 个不同的值,而取决于您的数据库的线程安全,它甚至可能没有完全没有价值。

理想情况下,您永远不会创建多个 AppServer 实例。


3

我没有足够的经验来专门评估这个问题,但也许 LRU 缓存会在一定程度上提高性能,或者使用哈希环缓冲区。这可能有点费力,但如果您想放弃,甚至使用 ML 来确定预加载以在一天中的某些时间保留的最佳值,例如,也可以工作。

如果您的缓存中总是缺少值,则无法改进您的代码。性能取决于您的数据库。

【讨论】:

  • 感谢您的回复。
  • 对于第一部分:我建议要么将整个方法标记为同步,要么按照你提到的同步整个块,但面试官很快强调这不是太有效,我们应该只锁定关键部分。问题是关键部分是什么。我认为不同的线程不可能在查找和插入之间遇到缓存未命中,因为局部变量存储在堆栈中,每个线程将检索自己的本地对象并且不会影响彼此的值。
  • 2.实际上面试官提到只有 1 个缓存服务器和 1 个 DB,所以如果 1 个 AppServer 具有缓存的值,那么另一个 :) 我同意我们不应该有超过 1 个 AppServer 实例的事实,但是这个是一次面试,面试官似乎坚持要拥有 2 台服务器。他提到可能的解决方案是 Kafka 和 Zookeeper。他还期望尽可能详细的解决方案,但我没有提供。如果您有任何想法,在这里将不胜感激:)
  • 3.好建议!缓存已经是面试官提到的LRU缓存。 (很抱歉在这个问题中没有提到它)。然而,并不是每个系统都是为 LRU 量身定做的。例如,ATM 缓存可能不会使用 LRU,因为今天取钱的人不太可能在不久的将来立即这样做。如果它总是缺少缓存未命中的值,则可以通过为不同的用户创建不同的缓存来改进它,这样我们就可以一次存储和缓存与不同用户组相关的数据。肯定有其他解决方案,只是不知道。所以我在问问题:)
  • 如果查询数据库的成本足以证明缓存的合理性,您必须确保如果另一个线程已经在执行查询,那么您必须等待该线程完成查找。同样,更有效的解决方案是我链接的带有 Future 的 CHM,但是如果您没有在解决方案中同步整个 get(...) 方法,则其他线程可能会在当前线程查询之前执行多个数据库查找再次访问数据库。
猜你喜欢
  • 2015-04-13
  • 1970-01-01
  • 2016-06-30
  • 1970-01-01
  • 2011-06-04
  • 2012-03-24
  • 1970-01-01
  • 1970-01-01
  • 2019-10-16
相关资源
最近更新 更多