Tech Hotoke Blog

IT観音とは私のことです。

【MySQL】デッドロックについて調べてみた

これは何?

業務でデッドロックが発生。そもそもデッドロックはどのような場合に発生して、どのような対応が適切なのか?について理解が曖昧だと感じたので改めて調べてまとめた。

前提

  • MySQL: 8系
    • InnoDB 以下における説明はすべてInnoDBを前提とします。

Tips

デッドロックとはなにか?

A deadlock is a situation where different transactions are unable to proceed because each holds a lock that the other needs. Because both transactions are waiting for a resource to become available, neither ever release the locks it holds.

MySQLのリファレンスには上記のように記載されています。 「トランザクションごとにロックを専有し、トランザクション同士が互いにロックの開放を待つ状態になってしまうことでトランザクションの処理が続行できなくなること」がInnoDBにおけるトランザクションの定義でしょうか。 余談ですが、トランザクションの挙動はRDBMSによって仕様が変わるので注意してみていきたいところ。

デッドロックはどうやって発生するのか?

MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.5.1 InnoDB デッドロックの例

デッドロックの対処方法は?

  • 公式リファレンスに記載がありました(親切)
  • その中でもひとまずここは見ておく・改善すべきかなという点を個人の独断と偏見でピックアップしました。

  • SHOW ENGINE INNODB STATUS コマンドを実行して原因を特定する

  • データを挿入または更新するトランザクションのサイズを小さくする。
  • トランザクションを再試行する。
  • トランザクションを一貫性のある順序で実行する。

その他の対処方法や詳細はこちら MySQL :: MySQL 8.0 リファレンスマニュアル :: 15.7.5.3 デッドロックを最小化および処理する方法

今回はどんな事象が発生した?

後ほど更新

事象に対する仮説

InnoDB では自動行レベルロックが使用されます。 単一の行を挿入または削除するだけのトランザクションの場合でも、デッドロックが発生する可能性があります。 その原因は、これらの操作が実際には「原子的」でないためです。これらの操作では自動的に、挿入または削除される行のインデックスレコード (複数の可能性あり) にロックが設定されます。

  • InnoDBでは、自動行レベルロック が使用されている点。
  • 自分の理解では行ロックとは、複数のトランザクション処理が同一のレコードに対して行われる際に発生すると考えていた。
  • 実際には範囲ロックとネクスキーロックが絡んでいると思われる

感想

  • デッドロックが分離レベルとは関係がないという点が自分の理解が曖昧で、当初の調査で際にそのあたりも関係があるのではないか?と調べてしまっていました。普通に考えれば「それはそう」という感じだったんですが、理解があやふやなままいると、いざ不具合に直面した際にあらぬ方向へ進んでしまい時間を無駄にしてしまうので、知識のインプットや知識の整理は仕事の効率を上げるうえでも非常に重要だなと改めて感じました。 また、参考記事にあげているようなわかりやすい記事を書いていただいている皆様には感謝の念しかありません。

参考

MySQLの行ロックを図解 #MySQL - Qiita

InnoDB の行レベルロックについて解説してみる - あらびき日記

InnoDBのロックの範囲とネクストキーロックの話 - かみぽわーる

システムタイムゾーンを考慮したDB保存にハマっているRails開発者へ

これは何?

Railsアプリ→ MySQLのTIME STAMPの保存でハマったのでメモ

前提

Ruby on Rails: 7.0.2 MySQL: 8系

Tips

  • 事象
    • 以下のようにscheduled_atをTime.zoneで更新をかけたところ、保存されている値が scheduled_at= '2024-04-03 03:00:00UTC時刻で保存されてしまっていた。
    • scheduled_at は DATETIME型
    scheduled_time = if Time.zone.now >= Time.zone.now.beginning_of_day + 12.hours + 1.minute
                         Time.zone.tomorrow.beginning_of_day + 12.hours
                       else
                         Time.zone.today.beginning_of_day + 12.hours
                       end
      hoge.update!(scheduled_at: scheduled_time)

Rails側の確認

irb(main):001:0> Time.now
=> 2024-04-04 02:56:00.07041709 +0000
irb(main):002:0> Time.current
=> Thu, 04 Apr 2024 11:56:11.926433860 JST +09:00
irb(main):003:0> Time.zone.now
=> Thu, 04 Apr 2024 11:56:26.882822349 JST +09:00

上記のことから、RailsタイムゾーンJSTになっていることがわかる。 Time.nowだけUTC時刻になっているのはシステムのゾーンがUTCのママだからだとわかる。

補足
` config/application.rb に config.time_zone = 'Tokyo' ` を記載していても、Time.nowには反映されません。
なぜなら、TimeはRubyの組み込みのクラスなので、Railsの設定の影響を受けないためです。
Rubyのタイムゾーンは以下の2つによって決まります。

■ システムのタイムゾーン
■ 環境変数 ENV['TZ'] の値

環境変数が設定されていればそちらが優先されます。
環境変数が設定されていなければシステムのタイムゾーンが使われます。

例
Asia/Tokyo
US/Central

⚠️ 無効なタイムゾーン('Tokyo'や'Hoge'など)が設定されている場合は ** 特にエラーにならず、世界標準時(UTC)がデフォルトになる ** そうなので注意が必要(システムのタイムゾーン設定にもなりません)

MySQLタイムゾーンの設定を確認

  • 以下のコマンドを実行すると、system_time_zoneがUTCになっていた。

SHOW VARIABLES LIKE '%time_zone%';

補足

■システムタイムゾーン
ホストマシンのタイムゾーンです。
サーバ起動時にホストマシンのタイムゾーンを特定して、system_time_zone システム変数に設定されます。

■サーバタイムゾーン
MySQLサーバの現在のタイムゾーンです。
time_zone システム変数に設定され、現在動作しているタイムゾーンを示します。time_zone の初期値は 'SYSTEM' となっており、サーバーのタイムゾーンがシステムタイムゾーンと同じであることを示します。

まとめ

これらを踏まえると、Rails側の設定の問題でDBの保存処理実行時にUTC時間に変換されてしまっている様子。 MySQLの設定は関係ない。。。?

config.active_record.default_timezoneの設定はDBを読み書きする際に、DBに記録されている時間をTime.utcで読むかTime.localで読むかを設定する。 :utcの場合DBに記録されている時間はUTC扱いで、この時DBサーバのタイムゾーン設定は考慮しない。

ActiveRecordインスタンスが持っているTimeWithZoneの値をUTCに変換し、その時刻をDBに書き込む。 :localの場合は、DBに記録されている時間はシステムのタイムゾーンとして扱う。

ActiveRecordインスタンスが持っているTimeWithZoneの値をシステムのタイムゾーンに変換し、その時刻をDBに書き込む。

参考

RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い #Ruby - Qiita

Rails アプリケーションの設定項目 - Railsガイド

Railsと周辺のTimeZone設定を整理する (active_record.default_timezoneの罠) #Ruby - Qiita

https://ruby-doc.org/3.0.6/Time.html

CodeBuildを使用したビルドエラー解決方法

これは何?

AWS CodeBuildを使用して、Reactのソースコードをビルドしていた。エラーが発生したため、その対処方法のメモ。

前提

ランタイム Node.js: 18

Tips

  • error
[Container] 2024/04/17 01:24:29.689519 Running command n 18 && rm -rf /tmp/*
  installing : node-v18.20.2
       mkdir : /usr/local/n/versions/node/18.20.2
       fetch : https://nodejs.org/dist/v18.20.2/node-v18.20.2-linux-x64.tar.xz
     copying : node/18.20.2
node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node)
/usr/local/bin/node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by /usr/local/bin/node)
/usr/local/bin/node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by /usr/local/bin/node)
   installed :  (with npm )

[Container] 2024/04/17 01:24:48.910339 Moving to directory /codebuild/output/src3539160356/src
[Container] 2024/04/17 01:24:48.913081 Unable to initialize cache download: no paths specified to be cached
[Container] 2024/04/17 01:24:49.066400 Configuring ssm agent with target id: codebuild:833d9e9f-3cff-4ada-a5b6-064be6417e92
[Container] 2024/04/17 01:24:49.094250 Successfully updated ssm agent configuration
[Container] 2024/04/17 01:24:49.094647 Registering with agent
[Container] 2024/04/17 01:24:49.140702 Phases found in YAML: 3
[Container] 2024/04/17 01:24:49.140740  INSTALL: 1 commands
[Container] 2024/04/17 01:24:49.140747  PRE_BUILD: 4 commands
[Container] 2024/04/17 01:24:49.140751  BUILD: 5 commands
[Container] 2024/04/17 01:24:49.141037 Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
[Container] 2024/04/17 01:24:49.141056 Phase context status code:  Message: 
[Container] 2024/04/17 01:24:49.225260 Entering phase INSTALL
[Container] 2024/04/17 01:24:49.225864 Running command npm install -g pnpm
node: /lib64/libm.so.6: version `GLIBC_2.27' not found (required by node)
node: /lib64/libc.so.6: version `GLIBC_2.28' not found (required by node)

[Container] 2024/04/17 01:24:49.234030 Command did not exit successfully npm install -g pnpm exit status 1
[Container] 2024/04/17 01:24:49.239405 Phase complete: INSTALL State: FAILED
[Container] 2024/04/17 01:24:49.239422 Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: npm install -g pnpm. Reason: exit status 1

GLIBC_2.27, GLIBC_2.28が見つからないとのこと。 Linux イメージのランタイムを確認すると、Amazon Linux 2 x86_64 standard:4.0 が設定されていたため、Amazon Linux 2023 x86_64 standard:5.0 に変更してエラーが解消されました。

参考: 使用可能なランタイム - AWS CodeBuild

なぜこのエラーが発生したのか?

  • もともと、ローカルではNode18を使用していたが、buildspec.ymlのruntimeがnode16になっていた。
  • どこかのタイミングでライブラリがアップデートされてバージョンの互換性がなくなってエラーが発生したと思われる。
  • 今回はバージョンを開発環境に合わせる形で対応したが、同じような現象が今後も発生するだろうし、ライブラリのバージョンアップデートを行い開発環境のバージョンが上がった場合にこちらのバージョンを上げ忘れるみたいなことは出てくる気がするのでどのように管理していくのがいいんだろう🤔

で、結局preload, eager_load, includesのどれを使うのがよろしいの? #Ruby on Rails #ActiveRecord

これは何?

preload, eager_load, includesを見かけるたびに、あれ、これってどういう挙動をするんだっけ?と調べているのでメモ。

前提

Tips

結論

  • eager_load
    • 1対1あるいはN対1のアソシエーションをJOINする場合に使う
  • preload
    • 多対多のアソシエーションの場合に使う
  • joins
    • メモリの使用量を必要最低限に抑えたい場合に使う
    • JOINした先のデータを参照せず、絞り込み結果だけが必要な場合に使う
  • includes
    • 使わない。私は断固として使わない。使ってるコードを見つけたら絶対に駆逐する。

Eager loading と Lazy loading

  • Eager loading

    • 予めメモリ上にActive Recordで情報を保持する方法。
    • ActiveRecordのメソッドで言えば、preload, eager_load, includesなど。
    • pros : 素早いレンダリングが可能になる。
    • cons : アソシエーションしているテーブルにある情報が膨大な場合、大量のメモリを消費することになる。
  • Lazy loading

    • JOINしたテーブルの情報が必要になった時に SQLを発行する方法。
    • ActiveRecordのメソッドで言えば、joinsなど。
    • pros : メモリを確保する量はEagerLoadingにくらべて少なくなる。
    • cons : JOINするテーブルを参照するたびSQLを発行するためWebサイト表示パフォーマンスを悪くする場合があります(N+1問題)

それぞれのメソッドの比較

メソッド クエリ アソシエーション先の参照 デメリット
eager_load LEFT JOIN できる JOIN先のデータ多いほど速度が低下する
preload SELECT できない データ量が多いと、IN句が肥大化してメモリを圧迫する
joins INNER JOIN できる N+1問題が発生するかも
incleds SELECT または LEFT JOIN できる 理解していないと予想外の挙動をするかも
  • どんな時に使うべきか??
    • eager_load
      • 1対1あるいはN対1のアソシエーションをJOINする場合。1回のSQLでまとめて取得した方が効率的な場合が多いと思うから。
    • preload
      • 多対多のアソシエーションの場合。データ量が多くなる場合が多いと思うので、使用するとパフォーマンスの向上につながるケースが多そうだから。
    • joins
      • メモリの使用量を必要最低限に抑えたい場合
      • JOINした先のデータを参照せず、絞り込み結果だけが必要な場合
    • includes
      • 使わない。eager_loadとpreloadを明示的に使い分けたほうが、思わぬボトルネックを埋め込まなくなると思うから。

【おまけ】includesの挙動について

  • includesしたテーブルでwhereによる絞り込みを行っている
  • includesしたassociationに対してjoinsかreferencesも呼んでいる
  • 任意のassociationに対してeager_loadも呼んでいる のうちいずれかに該当すると、eager_loadが呼ばれる

github.com

  • 例 下記の場合は、eager_loadの挙動をします。
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.includes(:reviews, :users).where(reviews: { article_id: 1})
  end
end

下記の場合は、preloadの挙動をします。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def index
    @articles = Article.includes(:reviews, :users)
  end
end

RFC 2397に定義されたデータURLスキームを読んでみた。

これは何?

AntD Reactの実装中にImageコンポーネントを使った。 その際に、srcのフォーマットについて調べてみた。

Tips

  • base64に変換されたPDFファイルの場合、Imageコンポーネントのsrcプロパティには、以下のような値を渡す必要がある。 data:application/pdf ; base64, <base64にエンコードされた文字列>

このフォーマットについてはRFC 2397 - The "data" URL scheme に定義されている。

[https://tex2e.github.io/rfc-translater/html/rfc2397.html:title]

[https://datatracker.ietf.org/doc/html/rfc1866:title]

#ReactQuery フェッチタイミングの制御Tips #invalidateQueries #resetQueries

これは何?

ReactQueryのフェッチタイミングの制御とかを触った。毎回調べている感じがするので、メモ。

前提

  • React: 18系
  • TypeScript: 4系
  • TanStack Query: 4系

Tips

  const { data } = usePlaylistAutoProgram(playlist?.playlistUid)

 useEffect(() => {
    if (data) {
      setValue('hoge', data.hoge)
    }
  }, [data])

  const onSubmitForm: SubmitHandler<FormInputs> = async (
    values
  ) => {
    await updatePlaylistAutoProgram({
      data: values
    })
    // 更新したデータでfieldの値を更新したい。 
  }
 const queryClient = useQueryClient()
  const { data } = usePlaylistAutoProgram(playlist?.playlistUid)

 useEffect(() => {
    if (data) {
      setValue('hoge', data.hoge)
    }
  }, [data])

  const onSubmitForm: SubmitHandler<FormInputs> = async (
    values
  ) => {
    await updatePlaylistAutoProgram({
      data: values
    })
    // 
    queryClient.resetQueries([
      '<任意のキー>',
      [任意のバリュー]
    ])
  }
  • ReactQueryはdefaultで5分間キャッシュされる。
  • キャシュしないか明示的にリフェッチするかは要件次第だと思いますが、今回は特定の用途でしかリフェッチする必要がないため、キャッシュ設定はそのままに明示的にリフェッチするようにしました。

useQuery | TanStack Query Docs

補足

  • キャッシュ時間を0にするには?
    • chacheTimeを0にする + retryフラグをfalseにする。(記憶ベースで恐縮なんですが、chacheTimeを0にするだけではキャッシュが無効にならず、GitHubリポジトリに同様の投稿があり、retryフラグをfalseにすれば治りました)
 cacheTime: 0,
 retry: false
  • resetQueriesinvalidateQueries の 使い分け
    • 今回実装するにあたって、どちらが適切か判断ができず、違いがよくわかっていなかったので改めてドキュメントを読んでみました。

    • resetQueriesは、指定したクエリをキャッシュから完全に削除し、初期状態に戻す。アクティブなクエリは自動的にリフェッチされる。

    • invalidateQueriesは、指定したクエリをキャッシュから無効化する。完全に削除はしません。アクティブなクエリが自動的に再フェッチされるかどうかは設定次第。

    • 上記を踏まえると、それぞれの使用例はこんな感じなのかなぁ。。。

      • resetQueriesの使用例

        • フォームの入力内容をリセットした際に、関連するクエリも初期状態に戻したい場合
        • アプリの設定を変更した際に、関連するクエリをすべて再フェッチしたい場合
      • invalidateQueriesの使用例

        • リストの一部分を更新した際に、そのリストに関連するクエリだけを無効化してリフェッチしたい場合
        • データを更新した際に、そのデータを使用しているクエリを無効化して、次回アクセス時にリフェッチさせたい場合

QueryClient | TanStack Query Docs

The resetQueries method can be used to reset queries in the cache to their initial state based on their query keys or any other functionally accessible property/state of the query. This will notify subscribers — unlike clear, which removes all subscribers — and reset the query to its pre-loaded state — unlike invalidateQueries. If a query has initialData, the query's data will be reset to that. If a query is active, it will be refetched.

QueryClient | TanStack Query Docs

The invalidateQueries method can be used to invalidate and refetch single or multiple queries in the cache based on their query keys or any other functionally accessible property/state of the query. By default, all matching queries are immediately marked as invalid and active queries are refetched in the background. If you do not want active queries to refetch, and simply be marked as invalid, you can use the refetchType: 'none' option. If you want inactive queries to refetch as well, use the refetchType: 'all' option

チリも積もれば山となる。パフォーマンスを測定してわかったリファクタリングの効果 #Ruby #Rails

これは何?

コードのリファクタリングの際に、パフォーマンスの計測まで行ってみたら結構な差が出てきたので、メモ。

前提

Tips

  • mapを使っていた箇所をpluckを使ってリファクタリングしてみた
    • before

      user_ids =Pls.where(pl_id: pl.id).map { |record| record['user_id'] }

    • after

      user_ids =Pls.where(pl_id: pl.id).pluck(:user_id)

結果

  • 出力されるSQLはSELECT句で全レコードを取得するか、特定のレコードを取得するかの差がある。
  • この差 + map処理が省略される差で処理速度がpluckの方が向上する場合が多い。

  • pluckを使った場合

SELECT `pl`.`user_id` FROM `pls` WHERE `pls`.`pl_id` = 3
  • mapを使った場合
SELECT `pl`.* FROM `pls` WHERE `pls`.`pl_id` = 3
  • パフォーマンスの比較をすると、 78倍 もの差があった。
  • もともと処理の時間が短いものの、こういうのがちりつもになって、パフォーマンスの劣化を招いていくのだろう。。。
  • 対象のテーブルのカラム数は10程度なので、これが増えていくとさらに差が出てくると思われる。
Before Refactoring: 0.93471800000043 seconds
After Refactoring: 0.01190639999913401 seconds