SiNBLOG

140文字に入らないことを、極稀に書くBlog

Slim3 シャーディングカウンタ作ってみた。

Slim3でシャーディングカウンタを作ってみました。

Slim3 シャーディングカウンタを改良した! - SinDiary
改良版が上記になります!


シャーディングカウンタをご存じない方のために簡単に説明します。
GAEでアクセスカウンタなどを作ろうとした時に、1つのEntityでカウントしていくと更新頻度が高すぎて競合を起こす可能性が出てきます。
そのため、カウントするためのEntityをいくつかに分けて作成するというのがシャーディングカウンタです。
最終的なアクセスカウントは、分けて作成したEntityのカウントを合計するということですね。

もっと詳しい内容は、公式でご参照下さい。
また、今回作ったソースも、公式を参考にしています。
http://code.google.com/intl/ja/appengine/articles/sharding_counters.html

まずは、Modelです。
作成したフィールドは、以下の2つだけです。

  1. CounterShardType name
  2. Integer count

countはまんま、カウントする値を入れるフィールドです。
CounterShardTypeはenumです。
公式のサンプルでは、ConfigModelを作成して、識別するための名前とカウンタを分割する個数を入れていたのですが、管理画面とか作るの面倒だし、コード上に書いておけば良いや!と横着しています。
Keyは、CounterShardTypeのnameとランダムな数値を繋げたものにしてます。
このランダムな数値が、分割に利用しているところですね。


import java.util.Arrays;
import java.util.List;

/**
* シャードカウンタのNameType
*
* @author Sinmetal
*
*/
public enum CounterShardType {
/** 掲示板 */
BBS(1, "bbs", 3);

/** ID */
private Integer id;

/** Name */
private String name;

/** シャードカウンタ個数 */
private int numShards;

/**
* コンストラク
*
* @param id
* @param name
*/
private CounterShardType(Integer id, String name, int numShards) {
this.id = id;
this.name = name;
this.numShards = numShards;
}

/**
* IDを返す
*
* @return
*/
public Integer getValue() {
return id;
}

/**
* 名前を返す
*
* @return
*/
public String getName() {
return name;
}

/**
* @return numShards
*/
public int getNumShards() {
return numShards;
}

/**
* 指定したIDの列挙体を返す
*
* @param id
* @return
*/
public static CounterShardType parse(Integer id) {
for (CounterShardType value : values()) {
if (value.getValue().equals(id)) {
return value;
}
}
throw new IllegalArgumentException(id + "は有効なタイプを表すIDとして認識されません");
}

/**
* 一覧を返す
* @return
*/
public static List getAll() {
return Arrays.asList(values());
}
}


import java.io.Serializable;

import com.google.appengine.api.datastore.Key;

import org.slim3.datastore.Attribute;
import org.slim3.datastore.Datastore;
import org.slim3.datastore.Model;
import org.smallreunion.model.constract.CounterShardType;

/**
* CounterShard
*
* @author Sinmetal
*
*/
@Model(schemaVersion = 1)
public class CounterShard implements Serializable {

private static final long serialVersionUID = 1L;

@Attribute(primaryKey = true)
private Key key;

@Attribute(version = true)
private Long version;

private CounterShardType name;

@Attribute(unindexed = true)
private Integer count;

/**
* Returns the key.
*
* @return the key
*/
public Key getKey() {
return key;
}

/**
* Sets the key.
*
* @param key
* the key
*/
public void setKey(Key key) {
this.key = key;
}

/**
* Returns the version.
*
* @return the version
*/
public Long getVersion() {
return version;
}

/**
* Sets the version.
*
* @param version
* the version
*/
public void setVersion(Long version) {
this.version = version;
}

/**
* @return name
*/
public CounterShardType getName() {
return name;
}

/**
* @param name セットする name
*/
public void setName(CounterShardType name) {
this.name = name;
}

/**
* @return count
*/
public Integer getCount() {
return count;
}

/**
* @param count セットする count
*/
public void setCount(Integer count) {
this.count = count;
}

/**
* カウンタをインクリメント
*/
public void increment() {
this.count++;
}

/**
* Key生成
*
* @param name
* @param numShard
* @return
*/
public static Key createKey(CounterShardType name, int numShard) {
String id =String.format("%s%d", name, numShard);
return Datastore.createKey(CounterShard.class, id);
}

/**
* インスタンス生成
*
* @return
*/
public static CounterShard getInstance(CounterShardType name) {
CounterShard counterShard = new CounterShard();
counterShard.count = 0;
counterShard.name = name;
return counterShard;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((key == null) ? 0 : key.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
CounterShard other = (CounterShard) obj;
if (key == null) {
if (other.key != null) {
return false;
}
} else if (!key.equals(other.key)) {
return false;
}
return true;
}
}


次はServiceです。
public method として以下の2つがあります。

  1. int getCount(CounterShardType name)
  2. void increment(CounterShardType name)

getCountは分割したカウンタを集計して、合計値を返します。
incrementは分割されたカウンタをランダムで選んで、インクリメントします。
ただ、Read Operationを減らすために、memcacheを利用しているので、正確さは欠けると思います。
正確さが重要な場合は、memcacheを利用している部分をいじる必要があると思います。


package org.smallreunion.service;

import java.util.List;
import java.util.Random;

import org.slim3.datastore.Datastore;
import org.slim3.memcache.Memcache;
import org.smallreunion.meta.CounterShardMeta;
import org.smallreunion.model.CounterShard;
import org.smallreunion.model.constract.CounterShardType;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Transaction;

/**
* シャードカウンタサービス
*
* @author Sinmetal
*
*/
public class CounterShardService {

CounterShardMeta meta = new CounterShardMeta();

public int getCount(CounterShardType name) {
if (Memcache.contains(name)) {
return Memcache.get(name);
}
int total = 0;
List counterShards =
Datastore.query(meta).filter(meta.name.equal(name)).asList();
for (CounterShard counterShard : counterShards) {
total += counterShard.getCount();
}
return total;
}

public void increment(CounterShardType name) {
Random random = new Random();
int index = random.nextInt(name.getNumShards());
Key key = CounterShard.createKey(name, index);

Transaction tx = Datastore.beginTransaction();
CounterShard counter = Datastore.getOrNull(meta, key);
if (counter == null) {
counter = CounterShard.getInstance(name);
counter.setKey(key);
}
counter.increment();
Datastore.put(counter);
tx.commit();
incrementMemcache(name);
}

protected void incrementMemcache(CounterShardType name) {
if (!Memcache.contains(name)) {
return;
}
int total = Memcache.get(name);
total++;
Memcache.put(name, total);
}
}

以上で、全部です。
公式のサンプルを見ながら作ったのですが、python版を主に見てます。
java版もあったのですが、JDOを使っているからなのか妙に長い?ように感じたので、python版にしました。
変なところがあったら、コメント欄やtwitterで指摘していただけるとありがたいです!