我有一个API,用于处理基于Web的插件以处理电子邮件。 API负责两件事:
想象一下插件向API发送请求的场景:
PUT http://server.com/api/email/update/<SessionID> -d "to=<address1,address2>&subject=<subject>"
在测试中,这很好用:数据保存正常。但是,该插件无法每秒多次发送该请求,使用相同的请求轰炸我的服务器。结果是我获得了EmailSession对象,保存了多个收件人副本。
就我的数据库架构而言,我有一个EmailSession模型,它有很多EmailRecipients。
以下是我的API控制器中更新方法的相关部分:
@email_session = EmailSession.find_or_create_by_session_id(:session_id => params[:id], :user_id => @user.id)
if opts[:params][:cm_to].blank? == false
self.email_recipients.destroy_all
unless opts[:params][:cm_to].blank?
opts[:params][:cm_to].strip.split(",").each do |t|
self.email_recipients << EmailRecipient.create(:recipient_email => t)
end
end
end
不可否认,“find_or_create”动态方法对我来说是新的,我想知道是否有一些关于搞砸了作品的东西。
我看到的症状包括:
ActiveRecord错误抱怨尝试将非唯一键保存到数据库中(我在SessionId上有索引)
以EmailRecipients集合结尾的重复收件人
如果有多个用户使用该插件,我会收到来自其他电子邮件的收件人,这些收件人最终会收到错误的电子邮件会话集。
我试图使用delayed_job尝试以某种方式序列化这些请求。由于当前版本中的各种错误,我没有太多运气。但我想知道我的解决方案是否存在更基本的问题?任何帮助将不胜感激。
答案 0 :(得分:1)
我仍然不确定我明白你在做什么,但这是我的建议。
首先,我认为您没有正确使用find_or_create_by
。这种方法稍微有些混淆语义(这就是为什么3.2引入了一些更清晰的替代方法)但是因为它没有使用user_id
来查找记录(尽管如果创建了一条记录它正在设置user_id
)。我不认为这是你想要的。而是使用find_or_create_by_session_id_and_user_id
这仍然会引发重复键错误,因为在find_or_create
检查和创建记录之间,其他人有时间创建记录。如果你没有做任何事情而不是创建电子邮件会话行,那么抢救这个重复的密钥错误然后重试就应该这样:在重试时你会找到阻塞你插入的行。
但是,当您继续添加收件人时仍然存在潜在问题,因为有两件事情可能会尝试删除收件人并同时将其添加到同一个电子邮件会话中。这可能是悲观锁定的一个很好的用例。
begin
EmailSession.transaction do
session = EmailSession.lock(true).find_or_create_by_bla_bla(...)
# use the session object here, add recipients etc.
end
rescue ActiveRecord::StatementInvalid => e
end
这里发生的事情是,当从数据库中检索电子邮件会话时,该行被锁定(即使它尚不存在 - 实际上您可以锁定记录所在的间隙)。这意味着任何其他想要添加收件人或进行任何其他操作的人都必须等待锁定被释放。锁定持续时间与它们发生的事务持续有效,因此所有工作都应该在这里发生(即使在第二部分中你实际上不再更改电子邮件会话对象)。
你最终可能会遇到死锁 - 我不知道你的应用程序还发生了什么,但如果你正在使用交易,你应该为它们做好准备。这就是救援块的用途:如果错误消息看起来像死锁,那么你应该重试一些有限的次数。
锁是(至少在MySQL上)行级锁:只要你在session_id上有一个索引,user_id然后只是因为你的一个实例有一个电子邮件会话对象被锁定不会阻止另一个实例使用另一个实例。