オタクタイム-デザインパターンの美しさプロトタイプパターン:HashMapハッシュテーブルを最速で複製する方法は?

JavaScript言語に精通しているフロントエンドプログラマーにとって、プロトタイプモードは一般的な開発モードです。これは、Java、C ++、およびその他のクラスベースのオブジェクト指向プログラミング言語とは異なり、JavaScriptはプロトタイプベースのオブジェクト指向プログラミング言語であるためです。JavaScriptは現在クラスの概念を導入していますが、それはプロトタイプに基づく構文上の砂糖にすぎません。ただし、JavaやC ++などのプログラミング言語に精通している場合、実際の開発でプロトタイプモードが使用されることはめったにありません。

今日の説明は、特定の言語の文法メカニズムとは関係ありませんが、クローンハッシュテーブルの例を通じて、プロトタイプモードのアプリケーションシナリオと、ディープコピーとシャローコピーの2つの実装方法を理解するのに役立ちます。プロトタイプパターンの原理とコードの実装は非常に単純ですが、今日の例はまだ少し複雑です。私の考えに従い、もっと考える必要があります。

プロトタイプモードの原理と応用

オブジェクトの作成コストが比較的大きく、同じクラスの異なるオブジェクト間の差がそれほど大きくない場合(ほとんどのフィールドが同じである場合)、この場合、既存のオブジェクト(プロトタイプ)を使用してコピー(またはコピー)できます。 )作成時間を節約するために新しいオブジェクトを作成します。プロトタイプに基づいてオブジェクトを作成するこの方法は、プロトタイプデザインパターン、または略してプロトタイプモードと呼ばれます

では、「オブジェクトの作成コストは比較的高い」とは何でしょうか。

実際、メモリの適用やメンバー変数への値の割り当てなど、オブジェクトを作成するプロセスはそれほど時間はかかりません。ほとんどのビジネスシステムでは、この時間は完全に無視できます。複雑なモデルを適用しても、パフォーマンスが少し向上するだけです。これはいわゆるオーバーデザインであり、利益は損失の価値がありません。

ただし、オブジェクト内のデータを複雑な計算(並べ替えやハッシュ計算など)で取得する必要がある場合、またはRPC、ネットワーク、データベース、ファイルシステムなどの非常に遅いIOから読み取る必要がある場合は、この場合、新しいオブジェクトが作成されるたびにこれらの時間のかかる操作を繰り返す代わりに、プロトタイプモードを使用して、他の既存のオブジェクトから直接コピーすることができます。

これはまだ比較的理論的です。次に、例を通して今の箇所を説明しましょう。

データベースに約10万個の「検索キーワード」情報が格納されていると仮定すると、各情報には、キーワード、キーワードが検索された回数、および情報が最後に更新された時刻が含まれます。システムAは、他のビジネス要件の処理を開始すると、このデータをメモリにロードします。特定のキーワードに対応する情報をすばやく便利に見つけるために、キーワードのハッシュテーブルインデックスを作成します。

Java言語に精通している場合は、その言語で提供されているHashMapコンテナーを直接使用して実装できます。その中で、HashMapのキーは検索キーワードであり、値は詳細なキーワード情報(検索数など)です。データベースからデータを読み取り、それをHashMapに配置するだけです。

ただし、検索ログの分析、データベース内のデータの定期的な間隔(10分など)でのバッチ更新、および新しいデータバージョンとしてのマーク付けに特化した別のシステムBがあります。たとえば、次の図の例では、v2バージョンのデータを更新してv3バージョンのデータを取得します。ここでは、更新と新しく追加されたキーワードのみがあり、キーワードを削除するアクションはないと想定しています。

ここに写真の説明を挿入
システムAのデータのリアルタイムパフォーマンスを確保するために(必ずしもリアルタイムである必要はありませんが、データが古すぎないようにする必要があります)、システムAは、データベースのデータに基づいて、メモリ内のインデックスとデータを定期的に更新する必要があります。

どうすればこの需要を達成できますか?

実際、それは難しいことではありません。システムAの現在のデータのバージョンVaに対応する更新時間Taを記録し、更新時間がTaより大きいすべての検索キーワードをデータベースから取得するだけです。つまり、Vaバージョンと最新バージョンデータの「違い」を見つけます。 、次に、差分セット内の各キーワードを処理します。ハッシュテーブルに既に存在する場合は、対応する検索時間、更新時間、その他の情報を更新します。ハッシュテーブルに存在しない場合は、ハッシュテーブルに挿入します。

この設計アイデアによると、私が提供したサンプルコードは次のとおりです。


public class Demo {
    
    
  private ConcurrentHashMap<String, SearchWord> currentKeywords = new ConcurrentHashMap<>();
  private long lastUpdateTime = -1;

  public void refresh() {
    
    
    // 从数据库中取出更新时间>lastUpdateTime的数据,放入到currentKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
    
    
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
    
    
        maxNewUpdatedTime = searchWord.getLastUpdateTime();
      }
      if (currentKeywords.containsKey(searchWord.getKeyword())) {
    
    
        currentKeywords.replace(searchWord.getKeyword(), searchWord);
      } else {
    
    
        currentKeywords.put(searchWord.getKeyword(), searchWord);
      }
    }

    lastUpdateTime = maxNewUpdatedTime;
  }

  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    
    
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}

ただし、現在、特別な要件があります。システムAのすべてのデータは、常に同じバージョンである必要があります。バージョンaのすべてまたはバージョンbのすべてのいずれかです。バージョンaは使用できず、一部はバージョンbです。現在の更新方法では、この要件を満たすことができません。さらに、メモリデータを更新するときに、システムAを使用不可状態にすることはできません。つまり、データを更新するためにシステムAを停止することはできません。

どうすればこの需要を今すぐ達成できますか?

実際、それは難しいことではありません。使用するデータのバージョンを「サービスバージョン」として定義します。メモリ内のデータを更新する場合、サービスバージョンを直接更新するのではなく(バージョンをデータと想定)、別のバージョンを再作成します。データ(バージョンbデータと想定)、新しいバージョンデータが作成された後、サービスバージョンをバージョンaからバージョンbにもう一度切り替えます。これにより、データが常に利用可能になるだけでなく、中間状態の存在も回避されます。

この設計アイデアによると、私が提供したサンプルコードは次のとおりです。


public class Demo {
    
    
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();

  public void refresh() {
    
    
    HashMap<String, SearchWord> newKeywords = new LinkedHashMap<>();

    // 从数据库中取出所有的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords();
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
    
    
      newKeywords.put(searchWord.getKeyword(), searchWord);
    }

    currentKeywords = newKeywords;
  }

  private List<SearchWord> getSearchWords() {
    
    
    // TODO: 从数据库中取出所有的数据
    return null;
  }
}

ただし、上記のコード実装では、newKeywords構築のコストは比較的高くなります。これらの100,000個のデータをデータベースから読み取り、ハッシュ値を計算してnewKeywordsを作成する必要があります。このプロセスは明らかに時間がかかります。効率を上げるには、プロトタイプモードが便利です。

currentKeywordsデータをnewKeywordsにコピーしてから、データベースから新しいキーワードまたは更新されたキーワードのみを取得して、それらをnewKeywordsに更新します。10万個のデータと比較して、毎回追加または更新されるキーワードの数は比較的少ないため、この戦略によりデータ更新の効率が大幅に向上します。

この設計アイデアによると、私が提供したサンプルコードは次のとおりです。


public class Demo {
    
    
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;

  public void refresh() {
    
    
    // 原型模式就这么简单,拷贝已有对象的数据,更新少量差值
    HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();

    // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
    
    
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
    
    
        maxNewUpdatedTime = searchWord.getLastUpdateTime();
      }
      if (newKeywords.containsKey(searchWord.getKeyword())) {
    
    
        SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword());
        oldSearchWord.setCount(searchWord.getCount());
        oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
      } else {
    
    
        newKeywords.put(searchWord.getKeyword(), searchWord);
      }
    }

    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }

  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    
    
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}

ここでは、Javaのclone()構文を使用してオブジェクトをコピーします。使い慣れた言語にこの構文がない場合は、currentKeywordsからデータを1つずつ取り出してから、ハッシュ値を再計算してnewKeywordsに入れることができます。結局のところ、最も時間のかかる操作は、データベースからデータをフェッチすることです。データベースIO操作と比較して、時間のかかるメモリ操作とCPU計算は無視できます。

ただし、実際、コードの実装に問題があることに気付いたかどうかはわかりません。問題が何であるかを理解するには、最初に他の2つの概念を理解する必要があります。ディープコピーとシャローコピーです。

プロトタイプモードの実現:ディープコピーとシャローコピー

ハッシュテーブルに整理された検索キーワード情報がどのようにメモリに格納されるかを見てみましょう。概略図を描き、一般的な構造を以下に示します。この図から、ハッシュテーブルインデックスでは、各ノードに格納されているキーが検索キーワードであり、値がSearchWordオブジェクトのメモリアドレスであることがわかります。SearchWordオブジェクト自体は、ハッシュテーブルの外側のメモリスペースに格納されます。

ここに写真の説明を挿入
シャローコピーとディープコピーの違いは、シャローコピーは図のインデックス(ハッシュテーブル)のみをコピーし、データ(SearchWordオブジェクト)自体はコピーしないことです。逆に、ディープコピーはインデックスだけでなくデータ自体もコピーします。シャローコピーで取得したオブジェクト(newKeywords)は、元のオブジェクト(currentKeywords)とデータ(SearchWordオブジェクト)を共有しますが、ディープコピーで取得したオブジェクトは完全に独立したオブジェクトです。具体的な比較を次の図に示します。

ここに写真の説明を挿入
ここに写真の説明を挿入
Java言語では、Objectクラスのclone()メソッドは、先ほど説明した浅いコピーを実行します。オブジェクト内の基本データタイプ(int、longなど)のデータと参照オブジェクトのメモリアドレス(SearchWord)のみをコピーし、参照オブジェクト自体は再帰的にコピーしません。

上記のコードでは、HashMapでclone()シャローコピーメソッドを呼び出して、プロトタイプモードを実装しています。newKeywordsを使用してSearchWordオブジェクトを更新する場合(たとえば、検索キーワード「デザインモード」の訪問数を更新する場合)、newKeywordsとcurrentKeywordsは同じSearchWordオブジェクトのセットを指します。これにより、currentKeywordsでポイントされるSearchWordが発生し、一部は古いバージョンです。はい。一部は新しいバージョンであり、以前のニーズを満たすことができません。currentKeywordsのデータは常に同じバージョンであり、古いバージョンと新しいバージョンの間に中間状態はありません。

では、どうすればこの問題を解決できますか?

浅いコピーを深いコピーに置き換えることができます。newKeywordsは、currentKeywordsインデックスをコピーするだけでなく、SearchWordオブジェクトもコピーするため、newKeywordsとcurrentKeywordsは異なるSearchWordオブジェクトを指し、newKeywordsデータを更新するとcurrentKeywordsデータが更新されるという問題はありません。

ディープコピーを実現する方法は?要約すると、以下の2つの方法があります。

最初の方法:オブジェクト、オブジェクトの参照オブジェクト、および参照オブジェクトの参照オブジェクトを再帰的にコピーします...コピーするオブジェクトに基本データタイプデータのみが含まれ、参照オブジェクトがなくなるまで。この考えに従って、前のコードをリファクタリングします。リファクタリング後のコードは次のとおりです。


public class Demo {
    
    
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;

  public void refresh() {
    
    
    // Deep copy
    HashMap<String, SearchWord> newKeywords = new HashMap<>();
    for (HashMap.Entry<String, SearchWord> e : currentKeywords.entrySet()) {
    
    
      SearchWord searchWord = e.getValue();
      SearchWord newSearchWord = new SearchWord(
              searchWord.getKeyword(), searchWord.getCount(), searchWord.getLastUpdateTime());
      newKeywords.put(e.getKey(), newSearchWord);
    }

    // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
    
    
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
    
    
        maxNewUpdatedTime = searchWord.getLastUpdateTime();
      }
      if (newKeywords.containsKey(searchWord.getKeyword())) {
    
    
        SearchWord oldSearchWord = newKeywords.get(searchWord.getKeyword());
        oldSearchWord.setCount(searchWord.getCount());
        oldSearchWord.setLastUpdateTime(searchWord.getLastUpdateTime());
      } else {
    
    
        newKeywords.put(searchWord.getKeyword(), searchWord);
      }
    }

    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }

  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    
    
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }

}

2番目の方法:最初にオブジェクトをシリアル化し、次にそれを新しいオブジェクトに逆シリアル化します。具体的なサンプルコードは次のとおりです。


public Object deepCopy(Object object) {
    
    
  ByteArrayOutputStream bo = new ByteArrayOutputStream();
  ObjectOutputStream oo = new ObjectOutputStream(bo);
  oo.writeObject(object);
  
  ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
  ObjectInputStream oi = new ObjectInputStream(bi);
  
  return oi.readObject();
}

上記の2つの実装方法に関係なく、ディープコピーはシャローコピーよりも時間とメモリを消費します。私たちのアプリケーションシナリオでは、より高速でメモリを節約する実装はありますか?

まず、浅いコピーを使用してnewKeywordsを作成できます。次に、更新が必要なSearchWordオブジェクトについて、deep copyメソッドを使用して新しいオブジェクトを作成し、newKeywordsの古いオブジェクトを置き換えます。結局のところ、更新する必要のあるデータはほとんどありません。この方法は、浅いコピーの利点を利用して時間とスペースを節約するだけでなく、currentKeywordsのデータが古いバージョンのデータであることを保証します。具体的なコードの実装を以下に示します。これは、タイトルにも記載されています。アプリケーションシナリオでは、ハッシュテーブルを複製する最速の方法です。


public class Demo {
    
    
  private HashMap<String, SearchWord> currentKeywords=new HashMap<>();
  private long lastUpdateTime = -1;

  public void refresh() {
    
    
    // Shallow copy
    HashMap<String, SearchWord> newKeywords = (HashMap<String, SearchWord>) currentKeywords.clone();

    // 从数据库中取出更新时间>lastUpdateTime的数据,放入到newKeywords中
    List<SearchWord> toBeUpdatedSearchWords = getSearchWords(lastUpdateTime);
    long maxNewUpdatedTime = lastUpdateTime;
    for (SearchWord searchWord : toBeUpdatedSearchWords) {
    
    
      if (searchWord.getLastUpdateTime() > maxNewUpdatedTime) {
    
    
        maxNewUpdatedTime = searchWord.getLastUpdateTime();
      }
      if (newKeywords.containsKey(searchWord.getKeyword())) {
    
    
        newKeywords.remove(searchWord.getKeyword());
      }
      newKeywords.put(searchWord.getKeyword(), searchWord);
    }

    lastUpdateTime = maxNewUpdatedTime;
    currentKeywords = newKeywords;
  }

  private List<SearchWord> getSearchWords(long lastUpdateTime) {
    
    
    // TODO: 从数据库中取出更新时间>lastUpdateTime的数据
    return null;
  }
}

おすすめ

転載: blog.csdn.net/zhujiangtaotaise/article/details/110445333