【個人開発】HottyDBのLock-freeなメトリクステーブルを紹介します！

※HottyDBは、検索エンジンとレコメンドエンジンの機能を搭載したRDBMSです！

# この記事の概要
筆者が個人開発しているHottyDBの機能の1つ 「**Lock-free** なメトリクステーブル」 について紹介したいと思います。

以前の記事でHottyDBの概要を解説しているので、そちらも是非参照してみてください！

https://qiita.com/toru1055/items/0fe400989373823e9c78


# メトリクステーブル
## メトリクステーブルとは
メトリクステーブルとは、例えば商品別のアクセス数などをカウントする際に役立つ機能です。
通常のテーブルでアクセス数などをカウントする場合、書き込み処理になるため排他ロックを取得する必要があります。排他ロックはその名の通り、その他のトランザクションの書き込みや読み込みを全て禁止します。そのためアクセス数のように、頻繁に更新が発生する処理があると全体の処理性能を大きく悪化してしまいます。

メトリクステーブルは、READ用テーブルとWRITE用テーブルを2つずつ用意し、多少の値の反映遅延を許容することで、ロックフリーな読み込みと書き込みを実現した専用テーブルです。

## メトリクステーブルの作成
例として、`page_view`という名前のメトリクステーブルを作成します。
`page_view`は`PRIMARY KEY`に`id`というフィールドを持ち、メトリクスフィールドとして`num`というフィールドを持ちます。
### CREATE METRICS TABLEの例文
```SQL
CREATE METRICS TABLE page_view 
(id INT PRIMARY KEY, num INT) 
FLUSH_FREQ = 25
```

`CREATE METRICS TABLE`の構文は`CREATE TABLE`の構文と似ていますが、 以下の違いがあります。
- `PRIMARY KEY`
  - メトリクステーブルは必ずPrimary Keyを持つ必要があります
  - 裏側のシステムでは、Primary Keyで指定したフィールドでGroup byし、それ以外のフィールド（メトリクスフィールド）をSUM集計しています
- `FLUSH_FREQ`
  - 更新を読み込みテーブルに反映するまでの更新回数を指定します
  - オプションのコマンドで、指定しなければデフォルト値の100回となります

## メトリクステーブルへの加算
先ほど作成した`page_view`テーブルの`id=4`のレコードに、`num += 1`の加算をしたいと思います。

#### ADD METRICSの例文
```SQL
ADD METRICS INTO page_view (id, num) VALUES (4, 1)
```

このように、`ADD METRICS`命令は `INSERT`命令とよく似た構文で記述することが可能です。


#### ADD METRICSの例文2
下記のように、マイナスの値を加算することで、減算も可能です。
```SQL
ADD METRICS INTO page_view (id, num) VALUES (4, -3)
```

`ADD METRICS`命令を`FLUSH_FREQ`で指定した回数実行すると、自動的に集計処理が走り、読み込みテーブルへの反映が行われます。
読み込みテーブルも書き込みテーブルも裏側では2つずつ用意してあるので、利用者はこのスイッチ処理の間も遅延なく書き込みと読み込みを行うことができます。

## メトリクステーブルの参照
メトリクステーブルは通常のテーブルと同じように扱うことが可能で、下記のようなSELECT文で参照可能です。
```SQL
SELECT id, num FROM page_view
```

また、検索エンジン機能の方で解説した機械学習ランキングの特徴量としても利用可能です。
メトリクステーブルを機械学習ランキングの特徴量として利用したサンプルコードがありますので、そちらをぜひ参照してみてください。

[機械学習ランキング+メトリクステーブルのサンプルコード](https://gitlab.com/hottydb/hottydb-sample/-/blob/main/src/main/java/org/example/SearchEngine.java#L146-148)
（後半部分のコード）

# ロックフリーとなる仕組みの解説
メトリクステーブルがロックフリーとなる仕組みを解説したいと思います。

`m1`というメトリクステーブルを作成した場合、HottyDBの裏側では、下記のように2つのREADテーブルと2つのWRITEテーブルを作成します。

![メトリクステーブル説明.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30697/9b230634-6519-4113-3f53-2080617fc75c.png)


2つのうち、どちらか一方がアクティブなテーブルとなりますが、この図の場合 READもWRITEもどちらも `A` がActiveなテーブルとなります。

## 通常期
図のように、SELECT文ではアクティブなREADテーブルを参照します。
そのため、READテーブルには更新処理が発生しないので排他ロックがかからずロックフリーとなります。

ADD METRICS命令ではWRITEテーブルを更新しますが、WRITEテーブルには参照処理が発生しないので、書き込み処理のみで競合が発生します。
ただし、ADD METRICS命令は追記（INSERT）のみの更新となるため、ほとんどがメモリ上の操作となりほぼロックがかかりません。
また、通常のテーブルであればトランザクションログ書き込みがDISK書き込みになりますが、メトリクステーブルではトランザクションログ書き込みもしていませんので、ロックの競合を最小限に抑えることができています。
※その代わり、ロールバックなどはできません。

## スイッチ処理
`ADD METRICS`命令を`FLUSH_FREQ`で指定した回数実行すると、スイッチ処理が走ります。
図（READもWRITEもAがアクティブ）の状態からスイッチ処理を行う過程を説明します。

### 1. WRITEアクティブの切替
まずWRITEのアクティブを `B` に切り替えます。
これで以降のADD METRICS命令は、`B` に追記していきます。

![メトリクステーブル_スイッチ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30697/ca08a5ce-7dfd-e52f-1658-12cf3b7b856a.png)


### 2. 集計処理
次に `m1.READ.A` と `m1.WRITE.A` を GROUP BY集計で集計した結果を `m1.READ.B` に書き出します。

![メトリクステーブル_スイッチ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30697/cbb96865-4776-12bd-52e6-c35aac744a44.png)


### 3. READアクティブの切替
最後に READのアクティブを `B` に切り替えます。

![メトリクステーブル_スイッチ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30697/52fce0b8-3a6b-0bab-6673-e56336fc3818.png)

# フィードバックをお待ちいています！
HottyDBはまだ生まれたばかりのソフトウェアです。 

使い方に関すること、分かりづらい点、バグ報告や改善提案、機能の要望など、なんでもOKなので下記のいずれかの方法でフィードバックをいただけると助かります。

- このQiitaのコメント欄
- [お問い合わせフォーム](https://docs.google.com/forms/d/e/1FAIpQLSfyokt68QP4yq4774xFyDEO2DyvVKvnvpnKh5vhOfbRSA1wnA/viewform?usp=sf_link)
- [Twitter](https://twitter.com/hottydb)

https://www.hottydb.com/other/contact/