kickflow Tech Blog

株式会社kickflowの開発チームによるブログ

Rails / PostgreSQL環境での連番の実装方法

こんにちは。株式会社kickflow・プロダクト開発本部の小本です。

今回は連番の実装方法についてです。

kickflowとは?どこに連番が必要?

kickflowはSaaS型の企業向けワークフロー製品です。ワークフロー=複数人の承認や作業が発生する事務処理、例えば経費申請や支払い依頼(もっと複雑な処理も扱えます)を扱えます。

kickflow.com

なお、kickflowでは用語を以下のように使っています。

  • テナント:kickflowを契約した企業
  • ワークフロー:ユーザーの起票から始まる一連の承認や作業の流れを表したもの。他社では「チケット種別」や「申請フォーム」とも言う。
  • チケット:個別の申請

そして、連番処理が特に必要なのはチケットです。経費申請#1、経費申請#2、経費申請#3、・・・と番号が必要で、番号はテナント・ワークフローごとに別々でなければなりません(A社の経費申請#2とB社の経費申請#2は別物、経費申請#2と支払い依頼#2も別物)。

なお、技術スタックは、DBはPostgreSQL、サーバーサイドはRuby on Railsです。詳しくはkickflowの技術スタック - what we use(技術スタックデータベース) を参照のこと。

sequencedによる連番

創業当初は人的リソースが足りなかったので、できるだけ簡便に実装する必要があり、それにマッチするのが sequenced というRubyライブラリでした。

github.com

モデルに1行追加するだけ!簡単!

class Ticket < ApplicationRecord
  acts_as_sequenced column: :ticket_number, scope: [:tenant_id, :workflow_id]

  ...
end

これで万事は上手くいっていました。初めのうちは……。

sequencedの問題点

ユーザーが増えるにつれロック待ちによるタイムアウトが起きるようになりました。

sequencedは大雑把に言うと

  • テーブル全体をロックする
  • SELECT .. FROM ... ORDER BY ... DESC LIMIT 1 で現在の最大値を得る
  • チケットを作成

という実装なので、同時に2つのレコードを作ったり更新することができません。しかも、kickflow のマルチテナントはプールモデル(全テナントが1テーブルを共有する方式)なので、同時アクセスは必須要件です。sequencedから別の方法に切り替える時です。

連番の要件を再確認

連番を再実装する前に、kickflowのチケット番号の要件を再確認しました。

エンジニアとしては例えば「UUIDにすればロックなしで実装できる!」などと考えなくもないのですが、顧客による運用実態を考えると、連番が必要です。一方で「あるチケットを削除したら、削除したことがバレないように、他チケットの番号をずらしたい(うちの独自システムではできるぞ!)」といった顧客要望が来ることもありますが、実装が面倒な割にデメリットが大きいため、却下せざるをえません。

結局、以下のような要件になりました:

  • チケット番号は連番でなければならない(UUIDや乱数はダメ)
  • プールモデルのマルチテナントである
  • テナント毎に別々に発行したい(A社の『経費申請#2』とB社の『経費申請#2』がある)
  • ワークフロー毎に別々に発行したい(『経費申請#2』と『購買申請#2』がある)
  • 番号が重複してはならない(『経費申請#2』が2件できてはならない)
  • 欠番は障害やチケット削除以外では生じない
  • チケット番号を後から変えたり、番号を巻き戻すことは無い

kickflow の現在の連番実装

以下のような実装に変えました。

連番をワークフロー毎に管理する next_ticket_numbers テーブルを作りました。

CREATE TABLEnext_ticket_numbers (
    id UUID DEFAULT gen_random_uuid() NOT NULL,
    tenant_id UUID NOT NULL,
    workflow_id UUID NOT NULL,
    value INTEGER DEFAULT 1 NOT NULL,
)l

CREATE UNIQUE INDEX idx ON next_ticket_numbers (tenant_id, workflow_id);
class NextTicketNumber < ApplicationRecord
  belongs_to :workflow
end

class Workflow < ApplicationRecord
  has_one :next_ticket_number
end

class Ticket < ApplicationRecord
  belongs_to :workflow
end

チケット作成時にnext_ticket_numberをロック(行レベルロック)し、チケット保存と同時に連番もインクリメントします。

ApplicationRecord.transaction
  n = workflow.next_ticket_number.lock!

  ticket = Ticket.create!(
    ticket_number: n.value,
    ...
  )

  n.increment(:value).save!
end

なお、この実装でも同じ種類のチケットを複数個同時に作成することはできませんが、そこは許容しています。

また、連番を実装する際に、DB側の機能(generate_seriesなど)を使う方法も検討しましたが、要件を満たすには実装が複雑になりそうだったので、Rails側で実装しました。

We are hiring!

kickflow(キックフロー)は、運用・メンテナンスの課題を解決する「圧倒的に使いやすい」クラウドワークフローです。

kickflow.com

サービスを開発・運用する仲間を募集しています。株式会社kickflowはソフトウェアエンジニアリングの力で社会の課題をどんどん解決していく会社です。こうした仕事に楽しさとやりがいを感じるという方は、カジュアル面談、ご応募お待ちしています!

careers.kickflow.co.jp