AppEngineでCounterを実装する #appengine #slim3
※本エントリーのソースには誤りがありました。修正版はこちら
Counterの実装に興味がある人がいるようですのでアップしておきます。
- 仕様は
- increment()でカウントアップ
- カウントは日毎に持つ
- clear()でカウントを0に戻す
単純なカウンター
import java.io.Serializable; import java.util.ConcurrentModificationException; import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; import org.slim3.datastore.Attribute; import org.slim3.datastore.Datastore; import org.slim3.datastore.EntityNotFoundRuntimeException; import org.slim3.datastore.Model; import org.slim3.util.DateUtil; import org.slim3.util.ThrowableUtil; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.Transaction; @Model public class Counter implements Serializable { private static final long serialVersionUID = 1L; @Attribute(primaryKey = true) private Key key; private Long seq; public Key getKey() { return key; } public void setKey(Key key) { this.key = key; } public Long getSeq() { return seq; } public void setSeq(Long seq) { this.seq = seq; } private static Logger logger = Logger.getLogger(Counter.class.getName()); private static final int RETRY_MAX = 10; private static Key createKey(Date date) { String keyName = DateUtil.toString(date, DateUtil.ISO_DATE_PATTERN); return Datastore.createKey(Counter.class, keyName); } public static Long increment() { return increment(new Date()); } public static Long increment(Date date) { Key key = createKey(date); Transaction tx = Datastore.beginTransaction(); try { return createOrUpdate(tx, key); } catch (ConcurrentModificationException e) { for (int i = 0; i < RETRY_MAX; i++) { try { return createOrUpdate(tx, key); } catch (ConcurrentModificationException e2) { logger.log(Level.WARNING, "Retry(" + i + "): " + e2.getMessage(), e2); } } throw e; } } private static Long createOrUpdate(Transaction tx, Key key) { Counter counter = null; try { counter = Datastore.get(tx, Counter.class, key); counter.setSeq(counter.getSeq() + 1); Datastore.put(tx, counter); } catch (EntityNotFoundRuntimeException e) { counter = new Counter(); counter.setKey(key); counter.setSeq(new Long(1)); Datastore.put(tx, counter); } Datastore.commit(tx); return counter.getSeq(); } public static void clear() { clear(new Date()); } public static void clear(Date date) { Key key = createKey(date); Counter counter = Datastore.get(Counter.class, key); counter.setSeq(new Long(0)); Datastore.put(counter); } }
ShardingCounter
import java.io.Serializable; import java.util.ConcurrentModificationException; import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; import org.slim3.datastore.Attribute; import org.slim3.datastore.Datastore; import org.slim3.datastore.EntityNotFoundRuntimeException; import org.slim3.datastore.Model; import org.slim3.util.DateUtil; import org.slim3.util.ThrowableUtil; import com.blank.meta.ShardingCounterMeta; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.Transaction; @Model public class ShardingCounter implements Serializable { private static final long serialVersionUID = 1L; private static final int KIND_COUNT = 10; private static final ShardingCounterMeta meta = ShardingCounterMeta.get(); @Attribute(primaryKey = true) private Key key; private Long seq; public Key getKey() { return key; } public void setKey(Key key) { this.key = key; } public Long getSeq() { return seq; } public void setSeq(Long seq) { this.seq = seq; } private static Logger logger = Logger.getLogger(ShardingCounter.class.getName()); private static final int RETRY_MAX = 10; private static Key createKey(String kind, Date date) { String keyName = DateUtil.toString(date, DateUtil.ISO_DATE_PATTERN); return Datastore.createKey(kind, keyName); } private static Key createKey(Date date) { String kind = meta.getKind() + "$" + (((int) (Math.random() * (KIND_COUNT))) + 1); return createKey(kind, date); } public static Long increment() { return increment(new Date()); } public static Long increment(Date date) { Key key = createKey(date); Transaction tx = Datastore.beginTransaction(); try { createOrUpdate(tx, key); } catch (ConcurrentModificationException e) { for (int i = 0; i < RETRY_MAX; i++) { try { createOrUpdate(tx, key); } catch (ConcurrentModificationException e2) { logger.log(Level.WARNING, "Retry(" + i + "): " + e2.getMessage(), e2); } } throw e; } return getTotalCount(date); } private static void createOrUpdate(Transaction tx, Key key) { try { Entity entity = Datastore.get(tx, key); ShardingCounter counter = meta.entityToModel(entity); counter.setSeq(counter.getSeq() + 1); Datastore.put(tx, meta.modelToEntity(counter)); } catch (EntityNotFoundRuntimeException e) { ShardingCounter counter = new ShardingCounter(); counter.setKey(key); counter.setSeq(new Long(1)); Datastore.put(tx, meta.modelToEntity(counter)); } Datastore.commit(tx); } private static Long getTotalCount(Date date) { Long result = new Long(0); for (int i = 0; i < KIND_COUNT; i++) { Key key = createKey(meta.getKind() + "$" + (i + 1), date); ShardingCounter counter = null; try { Entity e = Datastore.get(key); counter = meta.entityToModel(e); } catch (EntityNotFoundRuntimeException ignore) { continue; } result += counter.getSeq(); } return result; } public static void clear() { clear(new Date()); } public static void clear(Date date) { for (int i = 0; i < KIND_COUNT; i++) { Key key = createKey(meta.getKind() + "$" + (i + 1), date); try { ShardingCounter counter = meta.entityToModel(Datastore.get(key)); counter.setSeq(new Long(0)); Datastore.put(meta.modelToEntity(counter)); } catch (EntityNotFoundRuntimeException ignore) { continue; } } } }
Counter.classはgetした値は必ずユニークですが、ShardingCounterはgetした値はユニークにならない可能性があります。
ShardingCounterが遅い
そもそも同時アクセスが少ないアプリを今作成していたのでShardingCounterはいらなかったのですが、どうせなら作ってしまおうと実装してみた。ところが、負荷テストをしてみたところShardingCounterが結構遅いんですよね。まだ厳密に調査できていなのですが、おそらくgetTotalCountが遅いんじゃないかなと思われます。
追記:getTotalCountをparallel getにすると早くなりました。