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

ActiveRecord touch多Worker并发安全吗?Sidekiq重复Job场景问询

Answers to Your ActiveRecord & Sidekiq Concurrency Questions

1. Is ActiveRecord's touch method concurrency-safe in a multi-worker environment?

Short answer: Yes, it is concurrency-safe.

Here's why: Under the hood, touch(:read_at) translates to an atomic SQL UPDATE statement like this:

UPDATE "cars" SET "read_at" = CURRENT_TIMESTAMP WHERE "cars"."id" = $1

Most production databases (like PostgreSQL, MySQL with InnoDB) use row-level locking for UPDATE operations. When multiple workers try to touch the same record simultaneously, the database will queue these updates and execute them one at a time. You won't get corrupted data or lost updates—each touch will successfully update the read_at timestamp, with the last executed operation overwriting the timestamp from earlier ones. The key point is that each touch operation is atomic and isolated, so there's no risk of race conditions breaking data integrity.

2. What happens when two Sidekiq workers run this job in parallel, and how to fix it?

The Problem: Duplicate Emails

In a concurrent scenario, the race condition here can lead to duplicate emails being sent to the user. Let's walk through a realistic execution flow:

  • 19:00:01: W1 runs car.touch(:read_at) → sets read_at to T1
  • 19:00:01: At the exact same time, W2 runs car.touch(:read_at) → sets read_at to T2 (slightly later than T1)
  • 19:00:02: W1 queries for the earliest read_at non-null car → gets this car (either it's the only one with a non-null read_at, or the earliest if others exist)
  • 19:00:02: W2 runs the same query → also gets this car
  • Both workers then execute send_user_email, resulting in two identical emails hitting the user's inbox.

The root issue is that the touch and subsequent query/check aren't atomic—there's a window between the touch and the query where another worker can modify the record and trigger the same email condition.

The Fix: Atomic Operations with Row Locking

To prevent this, wrap the entire logic in a block that uses row-level locking to ensure only one worker can execute the sequence at a time. ActiveRecord's with_lock method does exactly this:

car.with_lock do
  car.touch(:read_at)
  cars = Car.where.not(read_at: nil).order(read_at: :asc).limit(1)
  send_user_email if cars.include?(car)
end

Here's how this works:

  • with_lock starts a transaction and acquires an exclusive row lock on the car record.
  • Any other worker trying to run with_lock on the same car will block until the first worker's transaction completes.
  • This ensures the entire sequence (touch → query → email check) runs as an atomic unit. By the time the second worker gets the lock, the read_at timestamp has already been updated; depending on your business logic, you might want to add an extra check (like a email_sent flag on the car) to avoid redundant emails entirely.

If you want to avoid holding the lock during email delivery (which could be slow), split the logic:

  1. Use with_lock to atomically update the car and check if it's the earliest eligible car.
  2. If it is, mark the car as "email pending" and enqueue a separate job to send the email. This way, the lock is only held for fast database operations, not the potentially slow email send.

内容的提问来源于stack exchange,提问作者Rafael Oliveira

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 08:26:26