以下是【Rails】实现重复处理防止功能的示例
不希望同时运行的方法。
def foo
AggregateTablesService.new.call # 数十万行のレコードの集計を行い、別のテーブルに集計を保存するサービス
@something = CreateGraph.new.call # 集計結果からグラフを生み出しキャッシュに保存するサービス。キャッシュがあればそれを読み込む
end
def exec
AggregateTablesService.new.call
CreateGraph.new.call
end
在处理大量且复杂的记录时,我们首先将它们汇总到聚合表中以便于处理和加快处理速度,然后在此基础上创建图表。
首先,针对方法重复运行多次的情况,可以通过使用”如果存在记录或缓存,则通过return退出处理”这种方法来解决。
class AggregateTablesService
def call
return if SummaryTable.where().present? # もしすでに集計されていたら処理を抜ける
# ~~~具体的な処理〜〜〜
end
end
class CreateGraph
def call
cache = Rails.cache.read(some_key)
return cache if cache.present?
# ~~~具体的な処理〜〜〜
graph # 最後にグラフオブジェクトを返す
end
end
然而,在这种情况下,当同时收到请求时会出现以下问题:1) 第一个聚合会被重复保存,2) 第一个聚合会以不完整的状态生成图表。
因此,这篇文章的重点是为了避免控制器和批处理两者重复执行处理,我们将尝试使用保护措施。
创建一个管理状态的对象
作为一个方针,我们将在处理进行中时采取相应措施,因此我们现在将准备一个对象来管理是否有某个人(包含批处理)正在执行程序。
class ProgressState
def start
redis.write('in_progress', '')
end
def finish
redis.delete('in_progress')
end
def running?
redis.exists?('in_progress')
end
private
def redis
Rails.cache
end
在开始处理时,在redis中保存适当的缓存,并在每次执行处理之前检查redis中是否存在缓存,这样可以防止处理同时进行。我将在控制器和批处理程序中实现这个功能。
附加说明:
虽然被称为“State”,但它与设计模式中的“State模式”不同,其角色几乎类似于一个仓库。然而,该对象的任务不是“数据的读写”,而是管理程序的执行状态,因此我将其称为“ProgressState”(如果有更好的命名建议,请评论)。
答え:退出处理
def exec
progress_state = ProgressState.new
return if progress_state.running?
progress_state.start
AggregateTablesService.new.call
CreateGraph.new.call
progress_state.finish
end
在批处理中,”另一个处理正在进行中”的意思是指正在进行汇总和图表缓存创建,所以我们只需要简单地退出处理。
控制器:让处理进入睡眠状态。
def foo
progress_state = ProgressState.new
SleepProgressService.new(progress_state).call
progress_state.start
AggregateTablesService.new.call
@something = CreateGraph.new.call
progress_state.finish
end
在控制器端,我們不是想要結束處理,而是希望能夠避免重複處理並最終將圖表傳送到屏幕上,因此我們將採取“如果其他地方正在處理,則等待”的方法。
class SleepProgressService
SLEEP_SEC = 10
RETRY_COUNT = 24
def initialize(progress_state)
@progress_state = progress_state
end
def call
count = 0
loop do
if @progress_state.running?
break if count > RETRY_COUNT
count += 1
Rails.logger.info("主要指標:別の集計処理が実行中のためスリープしました。スリープ回数:#{count}回目")
sleep SLEEP_SEC
else
break
end
end
end
end
问题:一旦缓存加载了数据,在redis中删除数据后,Rails仍然持续地说“缓存存在”。
让我们尝试在上述代码中实际同时运行程序。我们会同时击打控制器两次。结果是,一方面的屏幕显示了聚合和图形创建的结束,并且屏幕已显示,尽管通过finish方法,redis中的in_progress缓存已被确实删除,而另一方面的屏幕仍然处于休眠状态。
查看日志确认后,发现尽管Redis缓存已被清空,但仍然发生了@progress_state.running?持续24次返回true的现象。
我认为可能是Rails端的redis_cache设置的问题,但通过自己使用Redis.new而不使用Rails.cache可以避免这个问题。
class ProgressState
def initialize
Rails.configuration.cache_store[1]
redis_host = config[:host]
redis_port = config[:port]
redis_db = config[:db]
@redis = Redis.new(host: redis_host, port: redis_port, db: redis_db)
end
def start
@redis.set('in_progress', '')
end
def finish
@redis.del('in_progress')
end
def running?
@redis.exists('in_progress') == 1 # exists()が0/1のintで返却されるため
end
尽管没有什么特别值得一提的,但如果一定要说的话,可能是因为一开始就将进展状态定义为了对象,所以只需在这个对象上进行修改。
由于希望能够重复使用前述的SleepProgressService,所以它并未从控制器中分离出来,而是尽可能地限制了控制器的责任领域,并将”等待处理开始”这个过程隔离到了该服务中,这是出于一种意图。
最后一点
在实际的代码中,通过日期和各种ID来分别管理执行状态,这使得实现变得稍微复杂一些。但是,核心功能应该已经满足了上述内容。
感谢您一直陪伴到最后。