Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance/concurrency #43

Merged
merged 8 commits into from
May 29, 2016
Merged

Enhance/concurrency #43

merged 8 commits into from
May 29, 2016

Conversation

nobrick
Copy link
Owner

@nobrick nobrick commented May 27, 2016

WIP.
Almost done.
Code review required.

@nobrick
Copy link
Owner Author

nobrick commented May 28, 2016

@hymRedemption 现在基本上把除管理后台以外的 controller 的 DML 操作都套上了 serializable transaction,你可以 review 一下,看看这种方式还存在哪些 bug、有没有遗漏、代码组织和风格上有哪些不好。

本来应该把 Serializable concern 的方法的文档继续完善一下,不过最近可能暂时没有时间,还要忙一些上线相关的事情~

@hymRedemption
Copy link
Collaborator

好的, 没问题

@nobrick
Copy link
Owner Author

nobrick commented May 28, 2016

目前使用 serializable 有以下几种方式(以 Order 模型为例):

  1. Order.serializable(opts) { block }
  2. order.serializable(opts) { block }
  3. Order.serializable_trigger(opts) { block }
  4. order.serializable_trigger(opts) { block }
  5. around_action :serializable

之所以增加了这么多方式,一方面是出于灵活和便捷方面的考虑,另一方面是为了和 AASM 状态转换更好地结合。

@nobrick
Copy link
Owner Author

nobrick commented May 28, 2016

下面简单说说这几种方式,先说最后一种:around_action :serializable,这个可以参考 c9dc30,应该是最方便调用一种方法,就是把整个的 action 包含在 serializable transaction 里面。能够自动重试一定次数。缺点是将整个 action 包含在 transaction 中,transaction open 的时间间隔就会比较大一些,会相对影响一点性能。

@nobrick
Copy link
Owner Author

nobrick commented May 28, 2016

另外对于所有方式,需要记得提前做好两件事:

  1. 对于 aasm transition 过程,如 order.contract && order.save,最好在前面加上 order.may_contract? &&,这样在重试过程中,如果已经不满足 transition 条件,可以自动返回 false,而不是 raise 转移无效错误。
  2. 在你的 application_controller 里面加上 rescue_from,在重试达到最大次数时跳转到 fallback_url,这个 url 对应的 action 中最好不要有 DML 操作,否则 request 访问有可能出现死循环。

@nobrick
Copy link
Owner Author

nobrick commented May 28, 2016

around_action 实际上调用的就是 Order.serializable(opts) { block } 方法,即第二种方式,这个方法的作用主要是将 block 包含在 serializable transaction 中执行,并提供自动重试策略,即每次失败(存在 RW 冲突的时候)都休眠一段时间,时间长度根据重试次数指数性加倍。

@nobrick
Copy link
Owner Author

nobrick commented May 28, 2016

其它方式都与第二种类似,主要是语法上面提供了一些便利。但把第二种方法理解了应该就 OK 了~

@nobrick
Copy link
Owner Author

nobrick commented May 28, 2016

@hymRedemption

@nobrick nobrick merged commit a9fa195 into master May 29, 2016
@nobrick
Copy link
Owner Author

nobrick commented May 29, 2016

先把这个 PR 合并,方便接下来更新其它 branch。

@@ -1,6 +1,4 @@
class Users::OrdersController < ApplicationController
before_action :set_order, only: [ :show, :charge, :cancel ]
before_action :set_phone_and_vcode, only: [ :new, :create ]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

将这些 before_action 放到每个 action 中去是什么原因呢?

@nobrick
Copy link
Owner Author

nobrick commented May 31, 2016

@hymRedemption 因为查询(set_order)要放在serializable里面,保证每次retry都能读取到最新的数据。

return save if user.phone == phone && user.phone_verified?
transaction { user.update!(phone: phone) if save }
serializable(nested_behaviour: :transaction) do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个场景 isolation 我觉得设置成 read commit 就可以了。 因为这里不存在多个用户竞争同一资源的问题,只需要保证整个过程作为一个事务进行完成就行了。

Copy link
Owner Author

@nobrick nobrick Jun 1, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

事实上这里在数据库里面真正执行的只是一个 SAVEPOINT,而外层(controller)中使用 serializable 事务的原因在于避免潜在的风险,比如创建 requested order 前(未来)执行了一些 callback。

@hymRedemption
Copy link
Collaborator

hymRedemption commented Jun 3, 2016

serializable 模块实现本身我没有太多的疑问了。 现在剩下的需要讨论的就是,现在所有这些使用 transaction 的场景,是不是必须要使用 serialize 级别的 isolation。 因为很多场景并发冲突的几率并不高(例如接单的同时,用户取消订单),所以感觉全部用 serialize 没有必要。

我感觉用乐观锁的思想应该就可以解决,当然用 rails 的乐观锁需要明确去指出锁某些数据,有些场景有哪些数据需要控制我们可能不太好预见。但是我在看了 postgresql 的文档后,我发现它的 repeated read 级别的 isolation 可以用来代替乐观锁,而且能够同时锁住我们在这个事务中所有的数据,减去了我们去人为判断的需要。

postgresql 文档中说的是

a query in a repeatable read transaction sees a snapshot as of the start of the transaction

对于并发情况是这么处理的:

they will only find target rows that were committed as of the transaction start time. However, such a target row might have already been updated (or deleted or locked) by another concurrent transaction by the time it is found. In this case, the repeatable read transaction will wait for the first updating transaction to commit or roll back (if it is still in progress). If the first updater rolls back, then its effects are negated and the repeatable read transaction can proceed with updating the originally found row. But if the first updater commits (and actually updated or deleted the row, not just locked it) then the repeatable read transaction will be rolled back with the message

所以利用这个机制,postgresql 完全能够提供乐观锁的并发处理机制。并且同时不需要我们人工判断到底要锁哪些数据。 因为整个 transaction 看到的数据都是这个事务开始之前的快照,而结束之后,由数据库来判断到底是否有其他并发对数据进行更改了。

而对于使用 serialize 级别的 isolation,我觉得只要不涉及数据集的统计相关计算,或者确定数据与某个数据集的关系。就都不需要这个级别的 isolation。(例如,判读数据是不是 uniq 等情况,而且对于判读数据 uniq 的情况,我发现在 rails 文档中说明,就算是 serialize 级别的 transaction 也不能保证 validates_uniqueness_of 能够正确给出结果,这个我很好奇是为什么?详见文档

@nobrick
Copy link
Owner Author

nobrick commented Jun 3, 2016

现在确实冲突的概率很低,主要是因为每天的订单量并不多,因此可能即使完全不做数据库并发控制也几乎可以正常运行。但另一方面,从程序逻辑的严谨性、支付系统的重要性和安全性的角度来说,我觉得也是有必要让程序进一步完善的。

我个人并不完全赞同 repeatable read (in PostgreSQL) 是乐观锁的观点,虽然国内论坛上也有人把这一隔离级别作为乐观锁、甚至把 serializable 当作悲观锁,原因可能是 serializable 完全不需要 explicit locking。但实际上 serializable 事务也并非在物理执行上完全不会重叠,它只是在 repeatable read 的基础上增加了对 serialization anomaly 的监控而已,监控本身也不会带来任何阻塞。因此 serializable 事务如果能够被正确使用,基本足够应付各类并发情况,它的处理机制更像是乐观锁。而 repeatable read 对于一些并发情形仍然需要锁定整个表或部分行,所以通常需要将隔离级别和锁两种方法结合才能构成一种并发处理策略,而由于这种策略引入了 explicit locking,更适合处理 contention heavy 的情况,因此更像是悲观锁。

而对于使用 serialize 级别的 isolation,我觉得只要不涉及数据集的统计相关计算,或者确定数据与某个数据集的关系。就都不需要这个级别的 isolation。

但我觉得问题正是 serialization anomaly 同样是需要考虑的 phenomena 之一,而且并不一定是和数据集相关的操作才会引起 anomaly。因为 repeatable read 在 transaction 开始时拍摄 snapshot,如果另一个并发 transaction 在这个transaction snapshot 拍摄后 BEGIN,却在它还没有 close 前 COMMIT,除了一些简单情况如同时对同一行 R-W 或者 W-W 可能会被较好地检测,其它的一些读写依赖都可能造成数据不一致,让 query 结果变得 stale。

另外从工程角度来说,过早优化是万恶之源。我个人觉得持续使用 serializable transaction 已经足够简单,并可以保证数据的一致性,不像 repeatable read 需要考虑各种情况,锁有时候是必须的。而且至少目前数据库操作的时间消耗仅仅占了一小部分,以后如果真的出现了瓶颈可以再考虑 profiling。

判读数据是不是 uniq 等情况,而且对于判读数据 uniq 的情况,我发现在 rails 文档中说明,就算是 serialize 级别的 transaction 也不能保证 validates_uniqueness_of 能够正确给出结果,这个我很好奇是为什么?

这个其实在 9.1 以后的 PostgreSQL 上使用 serializable 事务已经可以做到正确检测 concurrent insert 了,uniqueness validation 的作用无非是做一下 select 查询,而实现了 predicate locking 的 PostgreSQL 在检测到冲突时会自动 abort 其中一个事务,当这个事务重试的时候 validation 的查询语句就可以生效了。之所以 Rails 文档说 serializable 可能会出现问题,是针对不同的数据库和版本的,比如以前版本的 PostgreSQL 就不行,因为它实际上只有两个隔离级别。当然,使用 unique index 才是最佳实践。

@hymRedemption
Copy link
Collaborator

嗯,确实,现在的业务量并没有显出瓶颈问题。也是想得有点多,主要对数据库特性本身很多不太了解,不过借这个机会倒是熟悉了一些并发的东西。

@hymRedemption
Copy link
Collaborator

  1. 在你的 application_controller 里面加上 rescue_from,在重试达到最大次数时跳转到 fallback_url,这个 url 对应的 action 中最好不要有 DML 操作,否则 request 访问有可能出现死循环。

这个出现死循环的情况是不是说,当业务量非常大的时候,很多 DML 操作都失败,会造成这种情况?

@nobrick
Copy link
Owner Author

nobrick commented Jun 3, 2016

我也是最近集中又把这些内容回顾了一下,其实我觉得可以不仅仅通过看文档、查找资料来学习,自己实际测试体验一下也许效果会更好。

@nobrick
Copy link
Owner Author

nobrick commented Jun 3, 2016

这个出现死循环的情况是不是说,当业务量非常大的时候,很多 DML 操作都失败,会造成这种情况?

也不是。主要是当这个 URL 本身的 DML 操作失败了,你要确保它不会被 rescue redirect_to 到它自己。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants