Rails中如何配置仅针对单用户的Mutex(互斥锁)?
在Rails中实现针对单一用户的互斥锁方案
当然可以实现,下面提供几种适配不同部署场景的方案,既能保证同一用户的操作互斥,又不会影响其他用户的并发执行:
一、内存级用户专属互斥锁(单进程场景适用)
如果你的Rails应用是单进程部署(比如Puma单进程模式、单Worker的Unicorn),可以维护一个线程安全的哈希表,为每个用户分配独立的Mutex实例,只有同一用户的并发请求会被阻塞,不同用户互不干扰。
代码示例:
# 在Entry模型中定义线程安全的锁哈希 USER_MUTEXES = Concurrent::Hash.new { |hash, user_id| hash[user_id] = Mutex.new } def self.update(user, day, segment, category_id) # 获取当前用户对应的锁 mutex = USER_MUTEXES[user.id] mutex.synchronize do entry = find(user, day, segment) entry ||= Entry.new(user: user, date: day, segment: segment) entry[key_for_id(category_id)] = category_id entry.save! end end
注意事项:
- 依赖
concurrent-rubygem(Rails 5+默认已包含),确保哈希表的线程安全。 - 仅在单进程环境有效,多进程部署时每个进程有独立内存空间,锁无法跨进程生效。
二、数据库层面原子操作(全场景通用)
多进程或分布式部署下,内存锁失效,必须依赖数据库的原子性来保证同一用户的操作互斥,同时不影响其他用户。
方案1:唯一索引 + 异常捕获
首先给entries表添加联合唯一索引,确保同一用户、日期、segment的记录唯一:
# 生成数据库迁移文件并添加索引 def change add_index :entries, [:user_id, :date, :segment], unique: true end
然后修改方法,利用数据库的唯一约束处理并发创建,捕获冲突异常后重新查询:
def self.update(user, day, segment, category_id) begin entry = Entry.find_or_create_by!(user: user, date: day, segment: segment) do |e| e[key_for_id(category_id)] = category_id end rescue ActiveRecord::RecordNotUnique # 并发创建冲突时,直接查询已存在的记录 entry = Entry.find_by!(user: user, date: day, segment: segment) end entry[key_for_id(category_id)] = category_id entry.save! end
方案2:使用Rails的upsert方法(推荐)
Rails 6+支持upsert方法,对应数据库的INSERT ... ON CONFLICT DO UPDATE(PostgreSQL)或INSERT ... ON DUPLICATE KEY UPDATE(MySQL),完全原子化处理插入/更新,无需手动捕获异常:
def self.update(user, day, segment, category_id) Entry.upsert( { user_id: user.id, date: day, segment: segment, key_for_id(category_id) => category_id }, unique_by: [:user_id, :date, :segment] # 指定唯一约束的字段 ) end
优势:
- 完全原子性,数据库层面保证并发安全。
- 适配所有部署场景,性能优于内存锁(无需进程内同步)。
三、数据库悲观锁(适用于复杂更新逻辑)
如果你的更新逻辑更复杂,需要在获取记录后执行多步操作,可以结合with_lock实现悲观锁,确保同一用户的并发请求串行执行:
def self.update(user, day, segment, category_id) # 先原子化获取或创建记录 entry = Entry.find_or_create_by(user: user, date: day, segment: segment) # 对记录加排他锁,同一用户的并发请求会等待锁释放 entry.with_lock do # 这里执行复杂的更新逻辑 entry[key_for_id(category_id)] = category_id entry.save! end end
注意:同样需要先添加联合唯一索引,避免并发创建重复记录。
内容的提问来源于stack exchange,提问作者Zack
相关产品推荐
相关产品推荐

