You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Rails中实现模型计算的线程安全?附代码优化验证

问题描述

我在Rails中定义了如下Storage模型初始代码:

class Storage < ApplicationRecord
  def free_spaces
    total - occupied
  end

  def decrement!(attribute, by = 1, **kwargs)
    if attribute.to_sym == :occupied_spaces && self.occupied_spaces - by < 0
      raise StandardError, "No more spaces to free up."
    end

    super
  end

  def increment!(attribute, by = 1, **kwargs)
    if attribute.to_sym == :occupied_spaces && self.occupied_spaces + by > self.total_spaces
      raise StandardError, "Occupied spaces completed."
    end

    super
  end
end

随后将其优化为:

class Storage < ApplicationRecord
  def free_spaces
    total, occupied = self.class.where(id: id).pluck(:total_spaces, :occupied_spaces).first
    total - occupied
  end

  def occupy_spot!
    change_occupied_spaces!(by: 1)
  end

  def free_spot!
    change_occupied_spaces!(by: -1)
  end

  private

  def change_occupied_spaces!(by:)
    retries ||= 0

    self.class.transaction do
      reload
      new_value = occupied_spaces + by

      if new_value < 0
        raise StandardError, "No more spaces to free up."
      elsif new_value > total_spaces
        raise StandardError, "Occupied spaces completed."
      end

      update!(occupied_spaces: new_value)
    end
  rescue ActiveRecord::StaleObjectError
    retry if (retries += 1) < 3
    raise
  end
end

场景背景:外部类可能并发调用增减方法。

我的理解:

  • free_spaces的修改可避免脏数据,保证线程安全;
  • change_occupied_spaces!通过事务包裹逻辑并在竞争时重试,确保occupied_spaces的原子、线程安全更新,防止超订或负数计数,保证并发访问下的数据完整性。

请问我的理解是否正确?有没有更优的纯Rails实现方式?

补充说明:storages表结构如下:

create_table "storages", force: :cascade do |t|
  t.integer "total_spaces"
  t.integer "occupied_spaces"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.integer "lock_version"
end

解答

你的理解完全正确

  1. free_spaces的优化:原代码直接使用实例缓存的字段值,并发场景下可能读取到未刷新的脏数据。优化后通过pluck直接从数据库拉取最新值,确保每次计算都基于当前真实数据,彻底避免脏读问题。
  2. change_occupied_spaces!的并发处理:通过事务包裹reload-校验-更新流程,结合表中已有的lock_version(乐观锁)触发冲突检测,竞争时自动重试,保证只有基于最新数据的更新能成功,有效防止超订或负数计数,保障并发下的数据完整性。

更优的纯Rails实现方式

结合你已有的lock_version字段和Rails内置特性,有几种更简洁高效的实现方案:

方案1:数据库原子更新(性能最优)

利用update_all生成原子SQL语句,直接在数据库层面完成操作,无需手动事务和重试:

class Storage < ApplicationRecord
  def free_spaces
    total_spaces - occupied_spaces
  end

  def occupy_spot!
    updated_rows = self.class.where(id: id, occupied_spaces: ...total_spaces)
                            .update_all("occupied_spaces = occupied_spaces + 1, lock_version = lock_version + 1")
    raise StandardError, "Occupied spaces completed." if updated_rows.zero?
    reload
  end

  def free_spot!
    updated_rows = self.class.where(id: id, occupied_spaces: 1..)
                            .update_all("occupied_spaces = occupied_spaces - 1, lock_version = lock_version + 1")
    raise StandardError, "No more spaces to free up." if updated_rows.zero?
    reload
  end

优势:操作完全在数据库层面原子完成,无应用层锁或重试逻辑,性能最高。

方案2:乐观锁+内置增量方法(代码最简洁)

复用Rails内置的increment!/decrement!,结合乐观锁自动处理冲突:

class Storage < ApplicationRecord
  def free_spaces
    total_spaces - occupied_spaces
  end

  def occupy_spot!
    retry_count = 0
    begin
      reload
      raise StandardError, "Occupied spaces completed." if occupied_spaces >= total_spaces
      increment!(:occupied_spaces)
    rescue ActiveRecord::StaleObjectError
      retry_count += 1
      retry if retry_count < 3
      raise
    end
  end

  def free_spot!
    retry_count = 0
    begin
      reload
      raise StandardError, "No more spaces to free up." if occupied_spaces <= 0
      decrement!(:occupied_spaces)
    rescue ActiveRecord::StaleObjectError
      retry_count += 1
      retry if retry_count < 3
      raise
    end
  end

优势:代码简洁直观,复用Rails原生方法,减少自定义逻辑。

方案3:数据库层面添加约束(终极保障)

为了从根源避免数据不一致,建议在数据库层面添加检查约束:

# 迁移文件
class AddConstraintsToStorages < ActiveRecord::Migration[7.0]
  def change
    add_check_constraint :storages, "occupied_spaces >= 0", name: "check_occupied_non_negative"
    add_check_constraint :storages, "occupied_spaces <= total_spaces", name: "check_occupied_not_exceed_total"
  end
end

优势:即使应用层出现逻辑漏洞,数据库约束也能强制保证数据合法性,提供最后一道防线。

内容的提问来源于stack exchange,提问作者James Ostevik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 02:21:17