以下是【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来分别管理执行状态,这使得实现变得稍微复杂一些。但是,核心功能应该已经满足了上述内容。

感谢您一直陪伴到最后。

bannerAds