Rails应用用户重复创建问题:成因排查与解决方案咨询
问题成因与解决方案
核心成因
- 应用层验证的竞态条件:Rails的
validates :uniqueness是先查询数据库再验证的非原子操作。当两个相同请求并发到达时,会同时通过验证,随后各自插入数据库,导致重复记录。 - 数据库索引不匹配验证规则:现有索引仅针对
email和cpf单个字段,没有结合white_label_origin创建复合唯一索引,数据库层面缺乏强制约束,无法阻止重复插入。 - 重复请求触发:用户重复点击提交按钮、前端未做防重复处理,导致同一会话发送多条创建请求。
解决方案
1. 添加数据库复合唯一索引(最关键)
修改迁移文件,创建包含验证scope字段的复合唯一索引,让数据库层面强制约束唯一性:
class AddUniqueCompositeIndexesToCustomers < ActiveRecord::Migration[5.2] def change # 针对email + white_label_origin的唯一索引 add_index :customers, [:email, :white_label_origin], unique: true, name: 'index_customers_on_email_and_white_label' # 针对cpf + white_label_origin的唯一索引(匹配模型中cpf的验证规则) add_index :customers, [:cpf, :white_label_origin], unique: true, name: 'index_customers_on_cpf_and_white_label' # 可删除原单个字段索引(若无需单独查询) remove_index :customers, :email remove_index :customers, :cpf end end
执行迁移:rails db:migrate
2. 捕获数据库唯一性异常
在控制器中捕获ActiveRecord::RecordNotUnique异常,处理并发插入失败的情况,返回友好提示:
def create payload = { created_at: DateTime.now, name: 'Customer Controller', type: 'log', event: 'create - Customer', session_id: session.id.to_s, general_text: 'Create customer' } log_elastic(payload) @customer = Customer.new(customer_params.merge(require_full_signup: true) .merge(white_label_data: white_label_session)) begin if @customer.save payload = { created_at: DateTime.now, name: 'Customer Controller', type: 'log', event: 'saved - Customer', customer_id: @customer.id, session_id: session.id.to_s, general_text: 'customer saved' } log_elastic(payload) # 补充成功响应逻辑(如redirect或render json) end rescue ActiveRecord::RecordNotUnique => e # 匹配错误信息返回对应提示 if e.message.include?('index_customers_on_email_and_white_label') @customer.errors.add(:email, 'Email already registered') elsif e.message.include?('index_customers_on_cpf_and_white_label') @customer.errors.add(:cpf, 'Document already registered') end # 补充错误响应逻辑(如render :new或返回错误json) log_elastic({ created_at: DateTime.now, name: 'Customer Controller', type: 'error', event: 'duplicate - Customer', session_id: session.id.to_s, general_text: "Duplicate customer creation blocked: #{e.message}" }) end end
3. 前端防重复提交
- 提交按钮点击后立即禁用,直到请求完成
- 用JS实现示例:
const submitBtn = document.getElementById('customer-submit-btn'); submitBtn.addEventListener('click', async function(e) { e.preventDefault(); if (this.disabled) return; this.disabled = true; try { await submitForm(); // 执行表单提交逻辑 } catch (err) { // 错误处理 } finally { this.disabled = false; } });
4. 优化请求日志逻辑(可选)
当前日志在Customer.new前就记录了create - Customer,即使保存失败也会留存。可调整为仅在保存成功后记录创建日志,或区分“尝试创建”与“创建成功”的日志,便于问题排查。
内容的提问来源于stack exchange,提问作者Philipe
相关产品推荐
相关产品推荐

