コンテンツにスキップ

Craft Key-Value Store にデータを保存する

Craft Key-Value Store (Craft KVS) はCraft Functions内で利用できるKey Value Storeです。

Craft Functionsの MODULES.kvs から利用します。

KVSの構造

Craft KVSのレコードはkeyvalue からなります。

  • key
    • KVSのユニークなキーです
      • KARTEプロジェクト単位でユニークなキーとなります。
    • MODULES.kvs 上はstring型の値として扱います
    • マルチバイト文字は利用できません
  • value
    • KVSのキーに紐づく値です
    • MODULES.kvs では一般的なオブジェクトとして扱います。

また、Craft KVSのレコードは管理用の情報として created_at , expired_at フィールドを持ちます。

  • created_at
    • レコードの作成/更新時刻です。
    • 書き込み系のメソッドを実行したタイミングで自動的に付与されます。
      • レコードを更新(上書き)した場合、 created_at フィールドも更新されます。
  • expired_at
    • レコードの有効期限です。
    • 書き込み系のメソッドで指定します。
    • expired_at の時刻を超えたレコードは、通常72時間以内にKVSが自動で削除します。

サンプルコード

export default async function (data, {MODULES}) {
const {kvs} = MODULES;
const key = "key1";
// KVSにレコードを書き込む
await kvs.write({ key, value: { [key]: 123 }});
// レコードを取得する
await kvs.get({ key });
// unixtimeMsよりも前に更新されていればレコードを更新する
const unixtimeMs = new Date().getTime();
await kvs.checkAndWrite({ key, value:{ [key]: 1234 }, operator: "<", unixtimeMs})
// レコードを削除する
await kvs.delete({ key });
}

メソッド

kvs モジュールで利用できるメソッドについて説明します。

write

/**
* @param {Object} param
* @param {string} param.key キー
* @param {Object} param.value
* @param {number} [param.minutesToExpire] 有効期限(分)
* @returns {Promise<Object>}
*/
kvs.write({ key, value, minutesToExpire })

storeにデータを書き込みます。

1 valueあたり 1KB の制限があります。

minutesToExpireで何分後にexpireするか指定することが出来ます。minutesToExpireを指定しない場合はTTLはデフォルトで1 dayです。

minutesToExpireの上限は、通常のプランでは現在時刻から1 month(44,640min)までです。これを超える値を指定したい場合は、プレイドの営業・カスタマーサクセスにお問い合わせください。

get

/**
* @param {Object} param
* @param {string} param.key キー
* @returns {Promise<Object>}
*/
kvs.get({ key })

storeからデータを読み取ります。

なお、TTLによるレコードの自動削除は expired_at で指定した時刻から72時間後までの間に行われるため 、expired_atに過去の値を持つデータを取得することがあります。

delete

/**
* @param {Object} param
* @param {string} param.key キー
* @returns {Promise<Object>} 空オブジェクト
*/
kvs.delete({ key })

storeからデータを削除します。

checkAndWrite

/**
* 条件付きでデータを書き込みます。
*
* @param {Object} param
* @param {string} param.key キー
* @param {Object} param.value
* @param {string} param.operator 比較演算子(`<`, `>`, `=`, `<=`, `>=` のいずれか)
* @param {number} param.unixtimeMs 比較対象のunixtime(ms)(ミリ秒)
* @param {number} [param.minutesToExpire] 値の有効期限(分)
* @returns {Promise<Object>} 空オブジェクト
*/
kvs.checkAndWrite({ key, value, operator, unixtimeMs, minutesToExpire })

created_at fieldに対する条件を照らしてから、その結果に応じて条件付き書き込みができます。

operator は、 =, <, <=, > , >= から指定できます。

unixtimeMsconst unixtimeMs = new Date().getTime() のように millisecond で指定ください。

条件判定では、created_at, operator, unixtimeMs の関係が以下となる場合に書き込みを行います。

created_at (operator) unixtimeMs

例えばoperatiorに ‘<’ を指定した場合、 created_atunixtimeMs より小さい場合に値を書き込みます。

created_at < unixtimeMs

レコードが現在時刻の5分前よりも前に作成・更新された場合にのみ上書きするには、次の条件式になるように引数を設定します。

created_at < (unixtimeMs - 300000)

この時のメソッド呼び出しは次のようになります。

const currentTime = new Date().getTime()
await kvs.checkAndWrite({
key: 'key'
value: { id: 'hoge', name: 'fuga' }
operator: '<'
unixtimeMs: currentTime - 300000
});

なお、TTLの挙動はwrite methodと同じです。

checkAndWriteのエラーハンドリング

throwされるerrorオブジェクトのstatus というフィールドに次のステータスコードが代入されます。要件に応じてretryの処理を入れてください。

  • status: 404
    • writeメソッド、checkAndWriteメソッドにおいて、条件に合致するデータが存在せず、値が更新されなかった場合に返却されます。
    • データ自体が存在しない場合、このerrorは発生せずwriteが成功します。
  • status: 409
    • 同時に同じキーが更新されたので更新されなかった
  • status: 400
    • その他のバリデーションエラーなど
  • status: 500
    • Internal Error

サンプルデータ

  • 自動で[key名].valueというパスが間に入ります。
await kvs.write({ key:'current_coupon', value: { index: 0, user_id: null }});
{
"current_coupon": {
"created_at": "2022-12-28T02:50:57.167Z",
"expired_at": "2023-01-27T02:50:57.167Z",
"value": { "index": 0, "user_id": null }
}
}
const v = await kvs.get({ 'current_coupon' });
const current_index = v.current_coupon.value.index;

制限

1 valueあたり 1KB の制限があります。

API リファレンス

リファレンス

注意点

ホットスポットの発生によるパフォーマンス低下

keyの辞書順が近いレコード群に対して頻繁に読み取りや書き込みを行うと、ホットスポットの発生により一時的にパフォーマンス低下することがあります。

この問題は、keyに対して辞書順をばらけさせるためのprefixを付与することで軽減できる可能性があります。

// keyにホットスポット回避用のprefixを付与する実装の例
import crypto from 'crypto';
function generateHashPrefix(id) {
const hashBase64 = crypto.createHash('sha256').update(id).digest('base64');
// 辞書順を分散させるためハッシュ値の5〜12文字目を使用
const prefix = hashBase64.substring(4, 12);
return prefix;
}
function kvsKey(id) {
const hash = generateHashPrefix(id);
return `${hash}-${id}`;
}

FAQ

Q. KVSはどの単位で共有されますか?

  • プロジェクト単位で1つのKVSが共有されます。
  • 複数のFunctionで同じデータが参照できてしまいます。
    • Function単位でnamespaceを厳密に管理してください。

Q. すでに存在するkeyに対してwriteをした場合はどうなりますか?

  • 既存のレコードが上書きされます
  • 内部的には新しくレコードが作り直される形になり、created_atも最新の時刻で更新されます

Q. レコードの有効期限(minutesToExpire)を指定せずにレコードを永続化できますか?

  • minutesToExpireを設定せずにレコードを書き込んだ場合は、永続化されるのではなく上述した「デフォルトの有効期限」が設定されます
  • minutesToExpireに設定できる値の上限は契約プランによって異なりますが、仮に設定値の上限が無いプロジェクトであっても、有効期限を長く設定したい場合はminutesToExpireを明示的に指定する必要があります
    • その場合、minutesToExpireを1000年後 (約525,600,000分後) など遠い未来の値に指定することで、実質的にレコードの永続化が可能です