読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

Rails4で悲観的ロックを実装する

Rails中級 Rails Model

ActiveRecordでは、lockとやlock!メソッドを使うことで悲観的ロックを実現することができます。
ロック自体は、基本的にはSELECT … FOR UPDATEが発行されてロックがされますが、DBに依存するので実際に発行されるSQLコードを確認することが大切です。

動作確認

  • Rails 4.1
  • ActiveRecord 4.1
  • mysql2 0.3.7
  • MySQL Server 5.6.21
  • Mac OS X 10.10

目次

  1. 悲観的ロックとは
  2. MySQLの設定とモデルの準備
  3. 悲観的ロックの使い方
  4. (WIP)悲観的ロックのテスト


悲観的ロックとは

悲観的ロックとは、競合が発生する可能性が十分あるという状況に向いたロック手法です。
DB側でレコードをロックし、ロックされたレコードを更新できないようにします。
あまりに競合が発生する場合には、ロックにより処理が待たされる時間が多くなり利便性が悪くなるため、DB自体の設計を考え直す必要があるかもしれません。




MySQLの設定とモデルの準備

DBは"MySQl"、モデルはAccountモデル、テーブルはaccountsテーブルを前提にします。そのための準備をします。

MySQLをインストールする
※インストールできない場合は、Googleで検索してください

brew install mysql

MySQL Serverを起動する

mysql.server start

mysql2を追加。

# Gemfile

# gem 'sqlite3' # コメントアウト
gem 'mysql2'    # 追加

bundle installを実施する。

bundle install

database.ymlに接続情報を記載する。

# config/database.yml

# development:
#   <<: *default
#   database: db/development.sqlite3

development:
  adapter: mysql2
  encoding: utf8
  database: model_test_development
  pool: 5
  username: root
  password:
  host: localhost

Accountモデルを作成する。

rails g model Account name:string balance:integer

balanceカラムに制約を追加する。

# db/migrate/20141124063626_create_accounts.rb
class CreateAccounts < ActiveRecord::Migration
  def change
    create_table :accounts do |t|
      t.string :name
      # 初期値 0, NOT NULL制約をつける
      t.integer :balance, default: 0, null: false

      t.timestamps
    end
  end
end

DBを作成し、マイグレートを実施する。

rake db:create db:migrate


悲観的ロックの使い方

悲観的ロックはDBに依存するので、上記のMySQLの設定とAccountモデルとaccountsテーブルを前提として、使い方を説明します。

レコードを取得する際にlockメソッドを使うことで、悲観的ロックをかけることができる。

Account.lock.find(1)
# => SELECT  `accounts`.* FROM `accounts`  WHERE `accounts`.`id` = 1 LIMIT 1 FOR UPDATE

また、lock!メソッドを使って取得したレコードに対して悲観的ロックをかけることができる。

Account.transaction do
  accounts = Account.where(...)
  # => SELECT `accounts`.* FROM `accounts` WHERE ...

  account1 = accounts.detect { |account| ... }
  account2 = accounts.detect { |account| ... }

  account1.lock!
  # => SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` = ? FOR UPDATE
  account2.lock!
  # => SELECT `accounts`.* FROM `accounts` WHERE `accounts`.`id` = ? FOR UPDATE

  account1.balance -= 100
  account1.save!
  account2.balance += 100
  account2.save!
end


(WIP)悲観的ロックのテスト

参考文献