write throughキャッシュは読み出しの高速化に効果がありますが、書き込みに関しては何もしません。しかし一般的には読み出しより書き込みの方が時間が掛かりますし、書き込みが読み出しより圧倒的に頻度が少ない、とも言えない事が多い場合は、書き込みに関してもキャッシュの機能を使いたくなります。
一番簡単な仕掛けとしては、書き込み要求があったときには、すぐにはsourceには書きに行かず、そのキャッシュデータが無効化される時(あるいはフラッシュされた時)に初めて書きに行く、という方法があります。
この方法ですと、そのデータへのアクセス(読み出しでも書き込みでも)が続いてあるうちは常にcache hitしますので、高速な動作が実現できます。ただmiss hitが増えて無効化される頻度が高くなってくると、sourceへの書き込みが頻発し、急激にパフォーマンスが低下してしまいます。
このためここではwrite bufferを使ったsourceへの書き込みの機能を作成してみます。write bufferというはハードウェアではおなじみの仕組みです。ここではWriteThroughCacheとCacheSourceの間にwrite bufferを挿入することにします。この様に異なった機能は別のクラスにしておけば、機能の追加などの扱いが後々楽になります。write bufferでは、あるデータへの書き込みが発生した時には、書き込み処理はオブジェクト内で保持して、すぐに戻ります。そしておもむろに書き込み専用の仕組みを用いて、cacheの情報をsourceに書き込んでゆきます。souceへの書き込みはオブジェクト内(つまりメモリ)へのアクセスに比べ圧倒的に時間が掛かる、という仮定の基では、sourceへの書き込みは非同期に、じっくり行おうというものです。
import java.util.LinkedList; import java.util.List; /** * @author PoisonSoft * * Write Bufferクラス */ public class WriteBuffer implements CacheSource, Runnable { private CacheSource origin = null; private List queue = new LinkedList(); public WriteBuffer(CacheSource source) { origin = source; Thread writeThread = new Thread(this); writeThread.start(); }
書き込み処理を行うためのThreadを作成して起動しておきます。このためWriteBufferはRunnableもimplementsしています。 write bufferへの書き込み、削除はコマンドとして内部のqueueに持つだけになります。
public void write(Object key, Object value) throws CacheSourceException { Command command = new Command(Command.WRITE, key, value); addCommand(command); } public void delete(Object key) throws CacheSourceException { Command command = new Command(Command.DELETE, key); addCommand(command); } void addCommand(Command command) { synchronized (queue) { queue.add(command); queue.notify(); } } class Command { static final int UNKNOWN = 0; static final int WRITE = 1; static final int DELETE = 2; private int command = UNKNOWN; private Object key = null; private Object value = null; Command(int command, Object key) { this.command = command; this.key = key; this.value = null; } Command(int command, Object key, Object value) { this.command = command; this.key = key; this.value = value; } }
実際の書き込み処理を行う処は、キューからコマンドを1つずつ取り出して実行するという感じになります。
public void run() { Command command = null; while (true) { while ((command = getCommand()) != null) { doCommand(command); } try { synchronized (queue) { queue.notify(); queue.wait(); } } catch (InterruptedException e) { throw new Error("write error", e); } } } Command getCommand() { synchronized (queue) { if (queue.size() > 0) { Command command = (Command) queue.remove(0); return command; } else { return null; } } } void doCommand(Command command) { if (command.command == Command.WRITE) { try { this.origin.write(command.key, command.value); } catch (CacheSourceException e) { throw new Error(e); } } else if (command.command == Command.DELETE) { try { this.origin.delete(command.key); } catch (CacheSourceException e) { throw new Error(e); } } }
読み出しについては注意が必要です。キューにコマンドが残っている状態でCacheSourceから読み出しを行うと、書いたものが読めない、という状況が発生してしまいます。このためキューが一旦空になるのを確認した上でCacheSourceから読み出しを行うようにします。
public Object read(Object key) throws CacheSourceException { flushCommand(); return origin.read(key); } void flushCommand() { synchronized (queue) { while (queue.size() > 0) { try { queue.notify(); queue.wait(); } catch (InterruptedException e) {} } } }
このwrite bufferを使うには、Cacheを作成する前にCacheSourceを基にWriteBufferを作成し、CacheにはWriteBufferを渡して作成するようにするだけです。この程度のwrite bufferでも書き込み性能は格段に良くなります。ただし非同期に書き込みを行うため、万が一書き込み失敗が発生した場合の処理(ここではサボっていますが)が複雑になります。