2022/09/21

[Rust]TiDBを使ったサンプルアプリケーションの実装

mysqltidbrust

概要

TiDBはMySQL互換のNewSQLです。

個人的に以下の点で気に入って導入を決めました。

  • 大量のDBへのWriteが発生する大規模アプリにて、DBも水平方向でスケールできる。
    • (Readももちろんスケールできますが、すごいパフォーマンスが良いわけではないようなので、超高速なReadを行う場合は、別のDBサーバーをたてたりする方が良さそうでした。)
  • MySQLなどの(標準的な)SQLをサポートできる。
  • MySQLコネクターがあれば、どんな言語からも接続できる。(弊社は主にRustかPython)
  • k8sへのデプロイが(公式サイトで)サポートされている。

導入を決めるにあたって、実際のアプリケーションで想定された使い方をイメージしながらRustでサンプリアプリケーションを実装してみました。

公式のGo言語で実装されたサンプリアプリをRustで書き直しました。

Rustでは主に以下を実装しました。

  1. コマンドラインアプリ 👈 clap = "3.2" を利用しています。
  2. Http Restful APIサーバー 👈 actix-web = "4.2.1" を利用しています。
  • どちらもSQL driver部分は、dieselというライブラリを利用しています。
  • 比較的clean architectureになるように気をつけています。

事前準備

ローカルでTiDBをdocker-composeを使って立ち上げておきます。

% git clone [email protected]:pingcap/tidb-docker-compose.git
% cd tidb-docker-compose
% docker-compose up

これだけでOKでした。

接続するには、以下で接続できました。

% mysql -h 127.0.0.1 -P 4000 -u root

Disesel Setup

まず以下のコマンドを実行します。(事前にdiesel_cliのインストールが必要です。)

# 環境変数を設定しておきます。(direnvなどを使うと便利です。)
% DATABASE_URL="mysql://root:@127.0.0.1:4000/test?charset=utf8mb4"
% export DATABASE_URL

# 初期設定
% diesel setup

# migrationファイルを作成します。
% diesel migration generate init
  • up.sqlに記載します。
CREATE TABLE IF NOT EXISTS player
(
    id VARCHAR(36),
    coins INTEGER,
    goods INTEGER,
    PRIMARY KEY (id)
);
  • down.sqlに記載します。
DROP TABLE IF EXISTS player;
# migrationを実行します。
% diesel migration run

TiDB接続部分

基本的にはMySQLと同じなので、diesel & mysql みたいな感じで検索して実装しました。

最初にDTOを準備します。

use crate::schema::player;

#[derive(Queryable, QueryableByName, Debug)]
#[diesel(table_name = player)]
pub struct Player {
    pub id: String,
    pub coins: Option<i32>,
    pub goods: Option<i32>,
}

#[derive(Insertable, Debug)]
#[diesel(table_name = player)]
pub struct NewPlayer<'a> {
    pub id: &'a str,
    pub coins: Option<i32>,
    pub goods: Option<i32>,
}

次に、Data adapter部分の実装です。

use crate::entities::{NewPlayer, Player};
use crate::schema::player;
use crate::Result;
use diesel::prelude::*;
use diesel::result::Error;
use diesel::{QueryDsl, RunQueryDsl};

pub type StoreConnection = diesel::mysql::MysqlConnection;

pub fn create(conn: &mut StoreConnection, entity: NewPlayer) -> Result<usize> {
    diesel::insert_into(player::table)
        .values(&entity)
        .execute(conn)
        .map_err(Into::into)
}

pub fn bulk_insert(conn: &mut StoreConnection, entities: &Vec<NewPlayer>) -> Result<usize> {
    diesel::insert_into(player::table)
        .values(entities)
        .execute(conn)
        .map_err(Into::into)
}

pub fn update(conn: &mut StoreConnection, coins: i32, goods: i32, id: &str) -> Result<usize> {
    diesel::update(player::dsl::player.find(id))
        .set((player::coins.eq(coins), player::goods.eq(goods)))
        .execute(conn)
        .map_err(Into::into)
}

pub fn get(conn: &mut StoreConnection, id: &str) -> Result<Option<Player>> {
    let result = player::table.find(id).first::<Player>(conn);
    match result {
        Ok(entity) => Ok(Some(entity)),
        Err(err) => match err {
            Error::NotFound => Ok(None),
            _ => Err(err.into()),
        },
    }
}

pub fn count(conn: &mut StoreConnection) -> Result<i64> {
    player::table.count().get_result(conn).map_err(Into::into)
}

pub fn select_for_update(conn: &mut StoreConnection, id: &str) -> Result<Player> {
    player::table
        .find(id)
        .for_update()
        .first::<Player>(conn)
        .map_err(Into::into)
}

pub fn gets_by_limit(conn: &mut StoreConnection, limit: i64) -> Result<Vec<Player>> {
    player::table
        .limit(limit)
        .load::<Player>(conn)
        .map_err(Into::into)
}

実際のアプリ

実際のアプリはgithub.com/kumanote/tidb-example-rsに公開しています。

% git clone [email protected]:kumanote/tidb-example-rs.git
% cd tidb-example-rs

# リリースビルドします。
% cargo build --release

# コマンドラインアプリケーションを実行する場合
% ./target/release/tidb-example-cmd
getPlayer: Some(Player { id: "test", coins: Some(1), goods: Some(1) })
countPlayers: 1920
print 1 player: Player { id: "test", coins: Some(1), goods: Some(1) }
print 2 player: Player { id: "16e8539a-58bf-4259-9197-57b2967ade75", coins: Some(10000), goods: Some(10000) }
print 3 player: Player { id: "ecb60313-e851-4a5d-ad39-bbf899c5f082", coins: Some(10000), goods: Some(10000) }

buyGoods:
    => this trade will fail

buyGoods:
    => this trade will success

[buyGoods]:
    'trade success'

# Restful APIサーバーを起動する場合は以下で実行できます。
# 8080がtidbのdocker-composeと競合するので、8000番を指定して起動しました。
% ./target/release/tidb-example-server -a 0.0.0.0:8000

# 起動後以下を実行すると、レスポンスが返ってきます。
% curl --location --request GET "http://localhost:8000/player/limit/3"
[{"coins":100,"goods":20,"id":"2e86517c-5cc2-4148-ac61-45bac68ad558"},{"coins":100,"goods":20,"id":"8509a96c-fa93-43aa-a0cd-1648bb24fe80"},{"coins":100,"goods":20,"id":"a48dccad-12b8-49c3-b040-18ec3ba237cb"}]

以上です。