OSキャッシュの便利な利用方法 | サイバーエージェント 公式エンジニアブログ
アメーバ事業本部スマートフォンゲームDiv兼ソーシャルゲームDivの岩本と申します。
アメーバのソーシャルゲーム開発を担当しているエンジニアです。

今回は、アメーバのソーシャルゲームを開発する際によく使われる、WEBサーバ上でのOSキャッシュを、Java、Springを利用して実装する便利な方法をご紹介いたします。
またここでのOSキャッシュとは、JavaのJVM上でのヒープメモリにsingletonで情報を格納することとします。


■まず何をキャッシュしたいのか?
下記のデータを対象にします。
  ・DB上のマスター情報で参照のみのデータ

よくあるマスタデータです。毎回DB上にSQLを投げてもいいのですが、結果は同じになります。
システム側での情報の更新も行いません。キャッシュしたいですよね!!

よく使うキャッシュは、下記の3点です。
  ・MySQLのクエリーキャッシュ
  ・Memcached
  ・OSキャッシュ

私は「速度が速い」「複数のWEBサーバ間での同期が必要ない」「WEBサーバのメモリが結構ある」という理由からOSキャッシュを採用することが多いです。


■動作説明
今回フレームワークは「struts2系」「Spring2系」「ibatis2系」を使用します。Strutsとibatisに関しては他のフレームワークでも問題ありません。一部Springの機能に依存しているので、seasar等のDIコンテナを使用する場合は、同等の機能があるかどうか確認してください。

ではまず、簡単な概要の説明です。
  1.Tomcat起動時にSpringのbean登録で「OSキャッシュを実装したクラス」を読み込み、データ取得。
  2.設定時間毎にキャッシュしたデータを最新に更新

1は、「OSキャッシュを実装したクラス」をSpringのbean登録します。Tomcat起動時、bean登録の初期化のタイミングで、マスタデータの取得を行います。beanのスコープは、デフォルトのスコープとして登録しているのでsingletonとなります。singltonなのでスレッドをまたいで情報をやりとりすることができ、またJavaのGCの対象とならないためキャッシュとしての利用ができます。

2は、マスタデータなのでシステム側からの変更はないのですが、運用中に変更が必要になる場合(イベント、パラメータの見直し等)が多々あります。マスタデータを変更してTomcatの再起動を行えばいいのですが、サーバが数十台あるとかなり大変ですよね。ここでは、Springのスケジュール機能を利用しています。設定時間毎に最新の情報をDBより取得し、更新する機能があると運用が楽ですよね。


■Springの設定
ここからが実際の設定、実装方法に移っていきます。
まずは、Springの設定からです。「applicationContext.xml」に以下を追加します。

<!-- (1) master cache -->
<context:component-scan base-package="jp.ameba.game.cache" />
<!-- (2) reload bean -->
<bean id="masterReloadService" class="jp.ameba.game.service.cache.impl.MasterReloadServiceImpl" init-method="reload" />

<!-- (3) reload timer -->
<bean id="masterReloadServiceJob"
class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="masterReloadService" />
<property name="targetMethod" value="reload" />
<property name="concurrent" value="false" />
</bean>
<bean id="reloadBatchTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail" ref="masterReloadServiceJob" />
<!-- 10分に1回リロードする -->
<property name="cronExpression" value="0 10 * * * ?" />
</bean>
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="reloadBatchTrigger" />
</list>
</property>
</bean>

大きく分けて3つの処理をしています。
 (1) master cache:マスタデータの取得方法の実装と情報の保持を行うbeanの登録。
 (2) reload bean: (1) master cacheで既に生成されているインスタンスを使用して、実際にデータの取得を行うbeanの登録。
 (3) reload timer:設定時間毎にキャッシュしたデータを最新に更新するbeanの登録。

(1) master cacheでは、テーブル毎に情報取得の実装クラスを作成し、まとめてbean登録しています。context:component-scanタグによって、指定したパッケージ配下のクラスを検索し、条件に合致するClassを自動的に登録する便利な機能を使っています。個々にbean定義を記述する方法では、テーブル毎のクラスが数十個あるので定義を登録するのが面倒、さらにテーブルの追加時に定義を更新しなければいけなくなるので漏れそうですよね。

(2) reload beanでは、Tomcat起動時に各マスタデータを取得するクラスをbean登録しています。後述しますが、取得方法に少し工夫をしています。また、「init-method="reload"」の設定により、起動時MasterReloadServiceImpl#reload()の実行を行い、メソッド内にてマスタデータを取得します。

(3) reload timerでは、Springの機能を利用して設定時間毎に指定の処理を繰り返させています。ここは、詳しくは説明しませんが、設定のみです。


■各クラスの説明
●はじめに(2) reload beanでデータ取得を行う、MasterReloadServiceImplクラスです。


public class MasterReloadServiceImpl implements MasterReloadService {
private Log log = LogFactory.getLog(this.getClass());
@Autowired
private WebApplicationContext applicationContext;
@SuppressWarnings("rawtypes")
@Override
public void reload() {
String[] beanNames = applicationContext.getBeanNamesForType(AbstractCache.class);  // (A)
for (String beanName : beanNames) {
AbstractCache cache = (AbstractCache) applicationContext.getBean(beanName); // (B)
cache.reload(); // (C)
}
log.info("master relaod");
}
}


Strategy パターンの考え方がベースにあります。
(A)の部分では、ApplicationContext#getBeanNamesForType()を実行し、AbstractCacheを継承しているクラスのBean-IDを取得しています。AbstractCacheは、(1) master cacheで登録しているクラスの親クラスとなります。
(B)の部分では、取得したBean-IDを元にしてApplicationContext#getBean()を実行し、インスタンスの取得をしています。
(C)の部分では、各インスタンスのreload()を実行し、マスタデータの取得をしています。


●次は、AbstractCacheクラスと、例としてアイテム情報のマスタデータをキャッシュするMItemCacheクラスを紹介します。

public abstract class AbstractCache<KEY, DTO> {
protected Map<KEY, DTO> keyMap;
protected List<DTO> list;
/**
* {@link MasterReloadService#reload()}より呼び出され、キャッシュの初期化、再取得を行う
*/
public void reload() {
try {
Map<KEY, DTO> tempKeyMap = new HashMap<KEY, DTO>();
List<DTO> tempList = this.getAll();
this.beforeLoop(tempList);
for (DTO dto : tempList) {
tempKeyMap.put(this.getKey(dto), dto);
}
this.afterLoop(tempList);
list = Collections.unmodifiableList(tempList);
keyMap = Collections.unmodifiableMap(tempKeyMap);
} catch (SQLException e) {
throw new ServiceException(e);
}
}
/**
* 主キーをもとにマスタデータを取得する
*/
public DTO get(KEY key) {
if (keyMap == null) { reload(); }
return keyMap.get(key);
}

/**
* マスタデータの全データをリスト形式で取得する
*/
public List<DTO> getList() {
if (list == null) { reload(); }
return list;
}

/**
* キャッシュの初期化、再取得時にマスタデータを取得するロジックを実装する
*/
protected abstract List<DTO> getAll() throws SQLException;

/**
* キャッシュの初期化、再取得時にマスタデータから主キーを取得するロジックを実装する
*/
protected abstract KEY getKey(DTO dto);

/**
* デフォルトは何もしない
* キャッシュの初期化、再取得時に前処理を行うような場合にオーバーライドする
* @param tempList {@link AbstractCache#getAll()}で取得したリスト
*/
protected void beforeLoop(List<DTO> tempList) { }

/**
* デフォルトは何もしない
* キャッシュの初期化、再取得時に後処理を行うような場合にオーバーライドする
* @param tempList {@link AbstractCache#getAll()}で取得したリスト
*/
protected void afterLoop(List<DTO> tempList) { }
}


@Service
public class MItemCache extends AbstractCache<String, MItem> {
@Autowired
private MItemDao dao;

@Override
protected List<MItem> getAll() throws SQLException {
MItemExample example = new MItemExample();
return dao.selectByExample(example);
}

@Override
protected String getKey(MItem dto) {
return dto.getItemId();
}
}


ここでのポイントは、AbstractCache#getAll()とAbstractCache#getKey(DTO dto)です。抽象メソッドとして定義し、MItemCacheクラスで実装を行います。これにより、MasterReloadService#reload()内でMItemCacheクラスのインスタンスを取得し、MItemCache#reload()メソッドを実行することでマスタデータの取得が行えます。それを、全マスタデータについて同じ処理を行い、データを取得しています。


キャッシュのデータ構造は、List<DTO>とMap<KEY, DTO>の2種類を持っています。Listはテーブルの情報をリスト形式で持ち、Mapはテーブルの主キーをMapのキーとして情報を持たせた形式です。主キーが複数ある場合のデータ構造は、別になります。また任意のデータ構造でも問題ありません。


使い方は、以下の感じです。サンプルでアイテム情報を取得するクラスを実装してみます。

@Service
public class HogeServiceImpl implements HogeService {

@Autowired
private MItemCache mItemCache;

@Override
public List<MItem> getMItemList() { return mItemCache.getList (); }

@Override
public MItem getMItemList(String key) { return mItemCache.get (key); }
}



■最後に
今回紹介した実装方法は、1つの例なので他の方法も沢山あります。
また、Springに依存している箇所もあるので、フレームワークが変われば実装方法も変わります。

結構便利なので色々な情報を保存したくなるのですが、JavaではOutOfMemoryErrorが発生するのでご利用はご計画的に!!