とりあえず、DBへオブジェクト(というか文字列だけ……)をキャッシュする仕組みの叩き台。
DB は MySQL で MyISAM エンジン使用。

それにしても、テストコードがやけに長い。


import java.sql.*;
import java.util.*;
 
/**
 * 文字列をDBにキャッシュする。
 * 信頼性は低め。キャッシュがあったら「ラッキー♪」ぐらいの信頼性。
 * 
 * 期限切れと判断されたデータはその場で削除される。
 * あとで期限切れデータの詳細をチェックされることは考慮しない。
 * 
 * MemoryCache と同じインターフェースにすると、
 * MemoryCache に無ければ DBCache を見に行くような
 * ChainCache クラス作っておくとか
 * 好きなように組み合わせて使えるから interface 統一しようかと思ったけど、
 * MySQLとかDBMSはメモリキャッシュしてくれてるんだから、
 * DBCache な仕組みさえ作っておいて、あとはDBMSにまかせたほうがラクそう。
 * というわけで、DBCache クラス。
 * 
 * 文字列よりバイナリキャッシュの方が汎用性高い。
 * けど、コンソールで確認するときは、テキストのほうが良いかな
 * ということで、文字列をキャッシュすることに。
 * それにバイナリデータをキャッシュしたい局面が自分には今のところ無い。
 * 
 * 日時は、Javaではエポックな long を使いたい。Date クラス周辺はなんか変だし。
 * DBでは、コンソールで確認できるように、DateTime 系カラムがよさそう。
 */
public final class DBCache {
 
  //////////////////////////////////////////////////////////////////////
  // test
  
  public static void main(String[] args) throws Exception {
    
    // データベースの設定
    Properties dbconfig = new Properties();
    dbconfig.setProperty(DBCache.MySQL.FQCN + ".db_driver", "com.mysql.jdbc.Driver");
    dbconfig.setProperty(DBCache.MySQL.FQCN + ".db_url", "jdbc:mysql://localhost/hogedb");
    dbconfig.setProperty(DBCache.MySQL.FQCN + ".db_user", "hogeuser");
    dbconfig.setProperty(DBCache.MySQL.FQCN + ".db_password", "hogepassword");
    dbconfig.setProperty(DBCache.MySQL.FQCN + ".db_table", "caches");
 
    DBCache dbcache = new DBCache(new DBCache.MySQL(dbconfig));
    
    // TEST1: データの追加と取り出し
    {
      System.out.println("TEST1 ----------------------------------------");
      
      // データをキャッシュする
      DBCache.Data data1 = new DBCache.Data();
      data1.key = "aaa";
      data1.value = "aaaaaa";
      dbcache.set(data1);
      
      // データを取り出す
      DBCache.Query query1 = new DBCache.Query();
      query1.key = "aaa";
      DBCache.Data data2 = dbcache.get(query1);
      
      // 中身をチェック
      System.out.println(data2.value);
 
      System.out.println(query1);
      System.out.println(data1);
      System.out.println(data2);
    }
    
    // TEST2: データの追加&期限切れで取り出せない
    {
      System.out.println("TEST2 ----------------------------------------");
      
      DBCache.Data data1 = new DBCache.Data();
      data1.key = "bbb";
      data1.value = "びー";
      data1.expirytime = str2long("2006-01-01 10:11:12.000"); // すっかり昔
      dbcache.set(data1);
      
      // 期限切れで取り出せない
      // DBからも削除される
      DBCache.Query query1 = new DBCache.Query();
      query1.key = "bbb";
      DBCache.Data data2 = dbcache.get(query1);
      
      System.out.println(query1);
      System.out.println(data1);
      System.out.println(data2);
    }
    
    // TEST3: データの追加&貯蔵日時の捏造
    {
      System.out.println("TEST3 ----------------------------------------");
      
      DBCache.Data data1 = new DBCache.Data();
      data1.key = "ccc";
      data1.value = "しシ詩";
      data1.cachedtime = str2long("2010-10-10 09:08:07.000"); // けっこう先の未来
      dbcache.set(data1);
 
      DBCache.Query query1 = new DBCache.Query();
      query1.key = "ccc";
      DBCache.Data data2 = dbcache.get(query1);
      
      System.out.println(query1);
      System.out.println(data1);
      System.out.println(data2);
    }
    
    // TEST4: データの追加&5秒後と10秒後の状況
    {
      System.out.println("TEST4 ----------------------------------------");
      
      DBCache.Data data1 = new DBCache.Data();
      data1.key = "ddd";
      data1.value = "で?";
      data1.expirytime = System.currentTimeMillis() + 8000; // 8秒後が有効期限
      dbcache.set(data1);
 
      DBCache.Query query1 = new DBCache.Query();
      query1.key = "ddd";
      
      try{Thread.sleep(3000);}catch(Exception e){e.printStackTrace();}; // 3秒間スリープ
      DBCache.Data data2 = dbcache.get(query1);
      
      try{Thread.sleep(5000);}catch(Exception e){e.printStackTrace();}; // 5秒間スリープ
      DBCache.Data data3 = dbcache.get(query1);
      
      System.out.println(query1);
      System.out.println(data1);
      System.out.println(data2); // data1 が取得できているはず
      System.out.println(data3); // data1 は期限切れのはず
    }
    
    // TEST5: 期限切れを無視してデータを取得
    {
      System.out.println("TEST5 ----------------------------------------");
      
      DBCache.Data data1 = new DBCache.Data();
      data1.key = "http://eee/hoeeeege.html";
      data1.value = "<html><body>イーッ</body></html>";
      data1.expirytime = System.currentTimeMillis() + 1000; // 1秒後が有効期限
      dbcache.set(data1);
 
      try{Thread.sleep(2000);}catch(Exception e){e.printStackTrace();}; // 2秒間スリープ
      DBCache.Query query1 = new DBCache.Query();
      query1.key = "http://eee/eeee.html";
      query1.lifetime = 10000; // 有効期間を10秒として取得を試みる
      DBCache.Data data2 = dbcache.get(query1);
      
      try{Thread.sleep(2000);}catch(Exception e){e.printStackTrace();}; // 2秒間スリープ
      DBCache.Query query2 = new DBCache.Query();
      query2.key = "http://eee/eeee.html";
      DBCache.Data data3 = dbcache.get(query2);
      
      System.out.println(query1);
      System.out.println(query2);
      System.out.println(data1);
      System.out.println(data2); // data1 が取得できているはず
      System.out.println(data3); // data1 は期限切れのはず
    }
    
    // TEST6: 期限切れじゃなくても無理矢理期限切れに
    {
      System.out.println("TEST6 ----------------------------------------");
      
      DBCache.Data data1 = new DBCache.Data();
      data1.key = "http://fff/ffff.html";
      data1.value = "<html><body>絵符</body></html>";
      dbcache.set(data1); // 有効期限なし
 
      DBCache.Query query1 = new DBCache.Query();
      query1.key = "http://fff/ffff.html";
      DBCache.Data data2 = dbcache.get(query1); // ふつうに取得
      
      try{Thread.sleep(2000);}catch(Exception e){e.printStackTrace();}; // 2秒間スリープ
      DBCache.Query query2 = new DBCache.Query();
      query2.key = "http://fff/ffff.html";
      query2.lifetime = 1000; // 有効期間を1秒として取得を試みる
      DBCache.Data data3 = dbcache.get(query2);
      
      DBCache.Query query3 = new DBCache.Query();
      query3.key = "http://fff/ffff.html";
      query3.lifetime = 200000; // 有効期間を20秒として取得を試みる
      DBCache.Data data4 = dbcache.get(query3);
      
      System.out.println(query1);
      System.out.println(query2);
      System.out.println(query3);
      System.out.println(data1);
      System.out.println(data2); // data1 は取得できているはず
      System.out.println(data2); // data1 は無理矢理に期限切れのはず
      System.out.println(data3); // data1 は期限内なのに削除されているはず
    }
    
    // TEST7: どんどん書き換わる値
    {
      System.out.println("TEST7 ----------------------------------------");
      
      for(int i=0; i<5; i++){
        // セット
        DBCache.Data data1 = new DBCache.Data();
        data1.key = "http://gggggg/ggg.cgi?あいうえお";
        data1.value = "<html><body>Gメン" + (2006 + i) + "</body></html>";
        dbcache.set(data1);
        // ゲット
        DBCache.Query query1 = new DBCache.Query();
        query1.key = "http://gggggg/ggg.cgi?あいうえお";
        DBCache.Data data2 = dbcache.get(query1);
        // 出力
        System.out.println(query1);
        System.out.println(data1);
        System.out.println(data2);
      }
    }
  }
  
  // Epoch Time からのミリ秒を日時文字列へ変換
  private static String long2str(long time){
    // yyyy-mm-dd hh:mm:ss.fffffffff ナノ秒まで
    return new Timestamp(time).toString();
  }
  
  // 日時文字列を Epoch Time からのミリ秒へ変換
  private static long str2long(String time) throws Exception {
    // yyyy-MM-dd HH:mm:ss.SSS ミリ秒まで
    java.text.SimpleDateFormat sdf =
      new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    return sdf.parse(time).getTime();
  }
  
  //////////////////////////////////////////////////////////////////////
  // Query クラス
  
  /**
   * キャッシュされたデータを取り出す際の問い合わせパラメータ。
   */
  public static class Query{
 
    // キャッシュのキー
    public String key = null;
    
    // setされたときの有効期限を無視して、
    // データがsetされた日時からの有効期間(ミリ秒数)を設定。
    // 期限を短く設定してしまったデータを長生きさせたいときに使ったり、
    // 無駄に長い期限を設定してしまったデータを早めに消すために使う。
    // 例: 24時間以内にsetされたデータを使う場合: old = 24 * 60 * 60 * 1000
    public long lifetime = -1;
    
    public String toString(){
      StringBuffer sb = new StringBuffer();
      sb.append("key=[");
      sb.append(key);
      sb.append("],");
      sb.append("lifetime=[");
      sb.append(lifetime);
      sb.append("]");
      return sb.toString();
    }
  }
 
  //////////////////////////////////////////////////////////////////////
  // Data クラス
  
  /**
   * キャッシュされる/されたデータ。
   * set時にもget時にも使う。
   */
  public static class Data{
 
    // データのキー
    // set時: 設定必須
    // get時: データのキーが設定される
    public String key = null;
    
    // データの値
    // set時: 設定必須
    // get時: データの値が設定される
    public String value = null;
    
    // データをsetした日時(Epoch Time からのミリ秒数。精度は秒単位まで)
    // set時: データを捏造したい場合はこれを指定
    // get時: データの日時が設定される
    public long cachedtime = -1;
 
    // データの有効期限(Epoch Time からのミリ秒数を指定。精度は秒単位まで)
    // set時: 指定しない場合は有効期限なし(ずっと保存)
    // get時: (setされたときの)データの有効期限が設定される
    public long expirytime = -1;
    
    public String toString(){
      StringBuffer sb = new StringBuffer();
      sb.append("key=[");
      sb.append(key);
      sb.append("],");
      sb.append("value=[");
      sb.append(value);
      sb.append("],");
      sb.append("cachedtime=[");
      sb.append(cachedtime);
      sb.append("],");
      sb.append("expirytime=[");
      sb.append(expirytime);
      sb.append("]");
      return sb.toString();
    }
  }
 
  //////////////////////////////////////////////////////////////////////
  // DBCache クラス
  
  private final MySQL db;
  
  /**
   * @param db キャシュとなるデータベース
   */
  public DBCache(MySQL db){
    this.db = db;
  }
  
  /**
   * データをキャッシュから取り出します。
   */
  public Data get(Query query){
    
    Data data = db.get(query); // DBからデータ取得
 
    if(data != null){
      if(data.value != null){
        if(query.lifetime < 0){
          // 有効期限内かチェック
          if(data.expirytime < 0 || System.currentTimeMillis() <= data.expirytime){
            // 切れてない
            return data;
          }else{
            // 期限切れ
            db.remove(query); // DBのデータ削除
            data.value = null;
            return data;
          }
        }else{
          // setされたときの有効期限を無視して、
          // 指定された有効期限内かチェック
          if(System.currentTimeMillis() <= data.cachedtime + query.lifetime){
            // 切れてない
            return data;
          }else{
            // 期限切れ
            // setされたときの有効期限を無視して、DBのデータ削除
            db.remove(query);
            data.value = null;
            return data;
          }
        }
      }else{
        // データ無し
        return data;
      }
    }else{
      data = new Data();
      data.key = query.key;
      data.value = null;
      return data;
    }
  }
  
  /**
   * データをキャッシュへ保存します。
   */
  public void set(Data data){
    db.set(data);
  }
  
  //////////////////////////////////////////////////////////////////////
  // MySQL クラス
 
  /**
   * MySQLへアクセスする実装クラス。
   */
  public static class MySQL{
    
    private static final String FQCN = DBCache.MySQL.class.getName();
    
    public boolean debug;
  
    private final String db_driver;
    private final String db_url;
    private final String db_user;
    private final String db_password;
    private final String db_table;
 
    public MySQL(Properties props) throws Exception {
 
      this.db_driver = props.getProperty(FQCN + ".db_driver"); // com.mysql.jdbc.Driver
      this.db_url = props.getProperty(FQCN + ".db_url"); // jdbc:mysql://hostname/databasename
      this.db_user = props.getProperty(FQCN + ".db_user");
      this.db_password = props.getProperty(FQCN + ".db_password");
      this.db_table = props.getProperty(FQCN + ".db_table");
      
      if(!dbcheck()){
        throw new Exception("DB Error");
      }
    }
  
    public synchronized Data get(Query query){
      
      Connection con = null;
      try{
        con = getConnection();
        Data data = get(con, db_table, query);
        return data;
      }catch(Exception e){
        e.printStackTrace();
        return null;
      }finally{
        try{
          if(con != null){
            con.close();
          }
        }catch(Exception e){
          e.printStackTrace();
          return null;
        }
      }
    }
 
    public synchronized void set(Data data) {
      
      Connection con = null;
      try{
        con = getConnection();
        set(con, db_table, data);
      }catch(Exception e){
        e.printStackTrace();
      }finally{
        try{
          if(con != null){
            con.close();
          }
        }catch(Exception e){
          e.printStackTrace();
        }
      }
    }
    
    public synchronized void remove(Query query){
      
      Connection con = null;
      try{
        con = getConnection();
        remove(con, db_table, query.key);
      }catch(Exception e){
        e.printStackTrace();
      }finally{
        try{
          if(con != null){
            con.close();
          }
        }catch(Exception e){
          e.printStackTrace();
        }
      }
    }
  
    private boolean dbcheck(){
      try{
        if(isExistTable()){
          return true;
        }else{
          return createTable();
        }
      }catch(Exception e){
        e.printStackTrace();
        return false;
      }
    }
    
    private boolean isExistTable() throws Exception{
      boolean result = false;
      Connection con = null;
      try{
        con = getConnection();
        result = isExistTable(con, db_table);
      }finally{
        try{
          if(con != null){
            con.close();
          }
        }catch(Exception e){
          e.printStackTrace();
        }
      }
      return result;
    }
 
    private boolean createTable() throws Exception{
      Connection con = null;
      try{
        con = getConnection();
        Statement st = con.createStatement();
        String query = createTableQuery(db_table);
        st.executeUpdate(query);
        st.close();
        return true;
      }finally{
        try{
          if(con != null){
            con.close();
          }
        }catch(Exception e){
          e.printStackTrace();
          return false;
        }
      }
    }
    
    private Connection getConnection() throws Exception{
      return getConnection(db_driver, db_url, db_user, db_password);
    }
  
    private static Connection getConnection(String driver, String url,
        String user, String password) throws Exception {
      Class.forName(driver);
      Connection con = DriverManager.getConnection(url, user, password);
      return con;
    }
    
    private static Data get(Connection con, String tableName, Query query) throws Exception {
      
      String sql = "select * from " + tableName + " where ckey=?";
      PreparedStatement pstmt = con.prepareStatement(sql);
      pstmt.setString(1, query.key); // キー
      ResultSet rs = pstmt.executeQuery();
      
      Data data = new Data();
      data.key   = query.key; // キー
      if(rs.next()){
        data.value = rs.getString("cvalue"); // 値
        data.cachedtime = rs.getTimestamp("cachedtime").getTime(); // 貯蔵日時
        Timestamp expirytime = rs.getTimestamp("expirytime"); // 有効期限
        if(expirytime != null){
          data.expirytime = expirytime.getTime();
        }else{
          data.expirytime = -1; // 有効期限なし
        }
      }
      
      rs.close();
      pstmt.close();
      
      return data;
    }
  
    private static void set(Connection con, String tableName, Data data) throws Exception {
 
      // 先に既存のレコードを削除
      // トランザクション一貫性とか無視
      remove(con, tableName, data.key);
      
      // レコード追加
      String sql =
        "insert into " + tableName +
        "(ckey, cvalue, cachedtime, expirytime) values (?, ?, ?, ?)";
      //String sql =
      //  "update " + tableName + " set ckey=?, cvalue=?, cachedtime=?, expirytime=?";
      
      PreparedStatement pstmt = con.prepareStatement(sql);
      pstmt.setString(1, data.key); // キー
      pstmt.setString(2, data.value); // 値
      if(data.cachedtime < 0){
        // 自前で現在日時を設定
        pstmt.setTimestamp(3, new Timestamp(System.currentTimeMillis())); // 貯蔵日時
        //pstmt.setLong(3, System.currentTimeMillis()); // 貯蔵日時 sql bigint / java long 版
      }else{
        pstmt.setTimestamp(3, new Timestamp(data.cachedtime)); // 貯蔵日時(捏造)
        //pstmt.setLong(3, data.cachedtime); // 貯蔵日時(捏造) sql bigint / java long 版
      }
      if(data.expirytime < 0){
        pstmt.setNull(4, Types.TIMESTAMP); // 有効期限なし
        //pstmt.setTimestamp(4, null); // 有効期限なし
        //pstmt.setLong(4, data.expirytime);
      }else{
        pstmt.setTimestamp(4, new Timestamp(data.expirytime)); // 有効期限
        //pstmt.setLong(4, data.expirytime);
      }
      pstmt.execute();
      pstmt.close();
    }
 
    private static void remove(Connection con, String tableName, String key) throws Exception {
      String sql = "delete from " + tableName + " where ckey=?";
      PreparedStatement pstmt = con.prepareStatement(sql);
      pstmt.setString(1, key); // キー
      pstmt.execute();
      pstmt.close();
    }
  
    private static String createTableQuery(String tableName) {
      
      // - MySQL AB :: MySQL 4.1 リファレンスマニュアル :: 6.2 カラム型
      //   http://dev.mysql.com/doc/refman/4.1/ja/column-types.html
      //
      // - MySQL AB :: MySQL 4.1 リファレンスマニュアル :: 6.5.3 CREATE TABLE 構文
      //   http://dev.mysql.com/doc/refman/4.1/ja/create-table.html
      //
      // - MySQL AB :: MySQL 4.1 リファレンスマニュアル :: 6.5.7 CREATE INDEX 構文
      //   http://dev.mysql.com/doc/refman/4.1/ja/create-index.html
      //
      //   > CHAR 型と VARCHAR  型については、カラムの一部のみを使用する
      //   > インデックスを作成できます。この場合、col_name(length)  構文を
      //   > 使用して、各カラム値の最初から length  に指定した数のバイトの
      //   > インデックスを作成します(BLOB  型と TEXT  型では、プリフィッ
      //   > クスの長さを必ず指定する必要があります。length  には 255 まで
      //   > の数値を指定できます)。
      
      StringBuffer buf = new StringBuffer();
      buf.append("create table " + tableName + " (  ");
      buf.append("  ckey        text      not null, "); // 65535文字まで
      buf.append("  cvalue      text      not null, "); // 65535文字まで
      buf.append("  cachedtime  timestamp not null, "); // 日付と時刻
      buf.append("  expirytime  timestamp     null, "); // 日付と時刻
      buf.append("  key i_key (ckey(255))           "); // 最初の255文字をインデックス(ユニークにしたかったけど……)
      buf.append(") ENGINE=MyISAM;                  ");
      return buf.toString();
    }
 
    private static boolean isExistTable(Connection con, String tableName)
        throws Exception {
  
      DatabaseMetaData dbm = con.getMetaData();
      String types[] = { "TABLE" };
      ResultSet rs = dbm.getTables(null, null, "", types);
  
      boolean result = false;
      while (rs.next()) {
        String sTableName = rs.getString("TABLE_NAME");
        if (tableName.equals(sTableName)) {
          result = true;
          break;
        }
      }
  
      return result;
    }
    
    void debug(Object x){
      if(debug){
        System.out.println(x);
      }
    }
  }
  
}

その後

で、
キャッシュ使いたくなくなったときのために簡単に切り替えられる仕組みが欲しいなと思って、
けっきょく現状はこんなクラス構成に変更した……

・class Cache
・interface Storage
・class MySQLStorage1 implement Storage
・class NullStorage implement Storage
・class MemoryStorage implement Storage

Cache <>----> Storage

Cache クラス内に Storage オブジェクトを持つ形。


Cache c = new Cache(new NullStorage());
Data data = new Data();
data.key = "key";
data.value = "value";
c.set(data);

ま、でも NullStorage は要るでしょ。
Storage の実装オブジェクトを切り替えれば、メモリもDBもblackhole的なNullStorageでもOK.


コメント

きょうNI-Lab.で、文字をテストしたいです。

tags: zlashdot Java Java

Posted by NI-Lab. (@nilab)