Rails 大家是如何解决并发问题的

siriuszhuang · 2012年08月28日 · 最后由 hpyhacking 回复于 2012年09月03日 · 10283 次阅读

现在我们的 master 会启 16 个 workers。有时候会导致并发的问题,请问大家有没有解决的好办法?

不知道你说的并发的问题是什么问题?

比如,一个用户只能够同时建造一个建筑,并发的话可能就会同时造两个建筑……

全局锁,唯一限制 都可以

#3 楼 @huacnlee 请问如何实现呢?

顺序执行啊,只让一个个过啊...

加个约束吧,比如这个用户在某一个位置只能建立一个建筑

#4 楼 @siriuszhuang 这个要看具体的场景的,看这个例子:

先需要统计文章的阅读量,如果你是这样

def show
  @post = Post.find(prams[:id])
  @post.hits += 1
  @post.save
end

那就会出现并发的问题,统计数字会有出入,应为你是从数据库拿出来计算以后在存进去的,并发高的时候就会出现统计数字有出入,因为取出到存入的过程就会有其他的改写了 hits 的值。

所以这类情况就得将 +1 的动作放到数据库里面执行(由于数据库里面是有执行优先级的设计的)

其他的并发冲突的场景也是类似的实现

还有建筑升级之类的问题,比如同时升级一个。并发的话就可能出现同时升级 2 个建筑。

#7 楼 @huacnlee RDBMS 里,对于复杂的情况是可以直接用SELECT FOR UPDATE的,但是无论用哪种,第二个请求会被第一个请求 block 住的。现在这情况明显是需要直接失败,而不是 block 住等到前一个 transaction 完成之后再失败。需要直接失败的时候,是不能这么用数据库的。

不是说用数据库一定做不了,而是说,用数据库来做这个会很蛋疼。

现在这个情况,更适合的模式是,在数据库前面加一个 FSM。假设每个连接,在服务端产生一个 agent 来处理,当 agent 收到请求的时候,向对应的 FSM 发消息 (有个队列让消息顺序过)。

FSM 收到消息,如果按规则是允许操作的,那么状态就变成正在操作。如果不运行的直接返回失败。

#7 楼 @huacnlee 受教。但是逻辑并不只是简单的++,可能会创建升级事件或者其他复杂的逻辑。

#10 楼 @siriuszhuang 所以,对你来说最简单的办法就是花一个下午学一下 Erlang,看一下gen_server/gen_fsm/gen_event

#10 楼 @siriuszhuang @bhuztez 的建议是正确的,不过可能楼主没看懂,我换种说法,并发本身并不是问题,问题出在并发的同时对某些地方又要求顺序执行(比如同时做就会“建立两个建筑”),所以关键是把顺序执行和并发分开,简单的解决方式是使用消息队列——无论前面怎么并发,进队列的时候永远是顺序执行。 这样做的话,一般意义上的“创建升级事件或者其他复杂的逻辑”都可以在处理消息的阶段顺序进行,虽然未必快,但是可以保证正确

补:看来不是没看懂,是被吓住了,嘿嘿

#11 楼 @bhuztez 学习曲线有点陡峭啊。

#12 楼 @fsword 谢谢,你的解释很好,对我非常有帮助。谢谢@bhuztez 的建议。

#12 楼 @fsword 还请教一下,对于并发执行的时候,将需要顺序执行的逻辑放入队列。这一块的解决方法有哪些呢?另外,Erlang 确实没有看懂……:'(

#12 楼 @fsword 很多情况其实真不如直接用 Erlang,首先你要把外界交互包装一下,消息封包,交给 RabbitMQ,另外一端再解包,再对外界操作。如果对消息可靠性没有什么特别的要求的话,直接用 Erlang 就把 封包->RabbitMQ->解包 这一步就可以省掉了,哪怕有啥特殊的要求,想清楚逻辑,自己写个 gen_xxx 就好了。

#15 楼 @siriuszhuang 你做的事情可以看作一个状态无关的函数,所以消息队列其实就是存储执行函数所需要的参数值的,这是逻辑队列,实现可以是多样的,最简单的做法可以在数据库创建一个 records 表,每次请求直接 insert,然后外部启动一个 ruby 进程周期性的从数据库按照 id 一条一条读出来慢慢执行。 当然,更好的方式是使用专门的消息引擎,比如 @bhuztez 提到的 rabbitmq 等等,不过我猜你的场景还没到这个地步

#17 楼 @fsword

比如,一个用户只能够同时建造一个建筑,并发的话可能就会同时造两个建筑……

我觉得队列用数据库实现很悬

这是一个简单的唯一性验证问题吧,数据库加唯一索引。

我觉得都是楼主的应用设计问题,楼主把问题描述的无比复杂,我想问一下,你的应用和普通论坛有什么特殊的地方?大家都是多进程啊都并发啊。。

#19 楼 @hooopo 比如一级的可以一次造一个建筑,两级的可以一次造两个建筑,这你也打算用unique实现么。而且我不觉得往 RDBMS 里存一个东西表示正在操作是个好主意。

#20 楼 @bhuztez 两级的一次造两个建筑就是在数据库里插入两个条记录,这存在并发问题么

数据库加一列作为用户动作的完成时间,没到这个时间就不能执行新动作。这个简单吧 再来个 lock_version,彻底解决问题

#21 楼 @hooopo 建筑不是立即就造完的吧,比如造一个需要一分钟。

#23 楼 @bhuztez 加一个字段啊 status,默认是未完成状态,一分钟后去把状态改成完成状态。也没有并发问题吧

#24 楼 @hooopo

比如一级的可以同时在造一个建筑,两级的可以同时在造两个建筑,这你咋解决?

#25 楼 @bhuztez 没看懂你说的

#18 楼 @bhuztez 要看怎么做,添加一个锁字段比较容易犯错,用单独的队列表配合独立进程就会很简单,用数据库的一个好处是可以直接借助事务和锁强制并发请求顺序进入队列,缺点是性能太差

@fsword +1,数据库是通过事务 (隔离级别) 来保证并发操作的,带来的后果之一就是应用层面必须去处理失败,不然就演变成了上面一些朋友说的在数据库层面顺序操作(如隔离级别之串行化),那么数据库就失去了并发的意义了,当然性能也差了,还有的一些做法如采取锁机制,而实际应用中情况会很复杂,有的情况不是通过锁行就可以解决的,甚至要锁表,性能也高不了

这个问题简单的在应用层面也不太好解决,尤其是像 rails 这种多进程甚至多实例部署的场景,如果应用都已经到了这种并发要求,那么并发应该用单独的方案来解决,如消息队列。

不过到现在都还不清楚楼主的具体场景。。。不知楼主的问题是否是这样“如何同时开始造两个建筑,而每个建筑都要花费 200 游戏币”,要保证的是并发请求之后用户账号上的游戏币被扣掉了 400 而不能错?

悲观锁 乐观锁

#22 楼

我感觉我的解决方法很简单啊,怎么感觉像是在面非面的比赛上做了馄饨

居然为了说明自己的方案好,去假设楼主的场景。

其实就是让可以并发的并发,让一定要排队的排队

至于用什么做并发用什么做队列完全看你的负载够不够

最悲剧的是,明明是排队的,你非要用并发解决,就出来各种锁了

底下吵成一片,楼主跑了。楼主你快回来说说你的场景到底是什么样子的。

#2 楼 @siriuszhuang 由 lz 的那个比如,我猜测 lz 可能还没有在实际的生产活动中遇到由并发引起的问题,lz 可能是出于职业习惯在预估一些可能由并发引起的问题。 对于 lz 提到的 16 个 workers 和那个比如,我们可以想像一下银行的存款业务。 比如我今天打算去工行存 5 毛钱,工行丫的给我开了 16 个窗口,结果一并发,工行给我同时存了两份 5 毛,当然这种事情是不会发生的,要不然我就不会只存 5 毛钱了。其实你的 master 启再多的 workers 也没有关系,因为一个用户创建城市的请求最终会分发到某一个 worker 去完成的,而不是同时交给多个 worker 去做的。 对于并发可能引起的问题,lz 得先问问自己,我的应用里是否有多个消费者同时竞争同一个资源. 其实 lz 举的例子里是一个用户创建一个城市,没有竞争的情况出现,所以在这个例子里是不用担心并发的问题。

看上去是做 web game 的,队列服务器轻松搞定。

互联网应用特征,就是资源往往都是用户独有的。譬如用户的帖子,这个资源就是属于用户的。只有用户才能操作自己的帖子。所以压根就没有什么并发问题。比如一些语言的有并发那是因为使用多线程模型,而类是单例的。所以需要考虑并发之类的。

#7 楼 @huacnlee 这种方案肯定不好。应该用 cache .比如 redis 之流。定时更新入数据库。并发控制则自动由缓存服务器控制了。不需要应用端担心。

嗯,cache 才是王道,不过要根据不同应用场合选择不同类型的 cache 和架构,总之,目的就是尽可能的减少数据库的 I/O 操作,毕竟内存的操作速度会快好几个数量级

谢谢大家这么热情,我来说一下吧。 首先,确实是生产环境中出现的问题,比如双方发生战斗,两家打一家。在同时进行战斗的时候,两次战斗都会取出被攻击方所有的兵力,战斗结束的时候减去损失的兵力,如果攻击的双方都将被攻击方全部歼灭了,就会出错。

或者,在同一时间并发建造两个建筑,数据库读取建造队列的时候,队列为空,然后两个进程都同时判断可以建造。这样的话就同时建造了两个建筑。

#32 楼 @hooopo #34 楼 @quakewang #28 楼 @donnior 今天上班比较忙,现在才有时间看看帖子,顺便贴上了一些场景。谢谢大家。

#35 楼 @WilliamZhu 互联网应用应该有大量的并发情况吧。电商网站比比皆是,就哪怕是一个简单的博客,点一下“喜欢”,也要考虑吧。不过好在数据库帮我们解决了。 @siriuszhuang 我的理解是,你的情况,利用数据库读加锁,这样会提前判断(相对写加锁)。这个应该是经验问题,请有经验的人说说看。

#38 楼 @siriuszhuang #39 楼 @siriuszhuang 你这两个场景恐怕是两类问题。 后一个比较简单,如果对队列的“消费”如果是排它的,那么一开始就不应该有多个线程去消费,“建造”这个活动应该只有一个独立的后台进程去负责实施,它在执行时检查建造队列是否为空,然后决定自己的行为,其它线程随便给队列丢任务。这里的要点是:判断是否能建造和实施建造过程是个顺序执行的任务,不能把判断拆出来并发。 前一个比较复杂,看来你们确实是 web game 之类,这个地方的问题其实是“实时更新”,如前所述,所谓“并发”(你这里是并行)本身并不是问题,完全彻底的异步化就可以了,但是计算“战斗结果”这个是不能并行的,只能有独立进程去频繁更新,并第一时间广播给客户端,难点在于这个独立进程的性能,通知是否及时。

BTW:看来还是 @quakewang 经验丰富,我没想到是 web game,所以要纠正一下我之前的建议,如果确实是 web game,任务队列用数据库实现绝对不合理,这时候应该用专业的消息引擎了。

#39 楼 @siriuszhuang 用 Erlang 有两年的时间了,深感进程设计在应付这种问题上的无比轻松和自然。学习曲线是有的,也确实很大,但很值得。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号