-
Notifications
You must be signed in to change notification settings - Fork 0
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
Conversation
04e560f
to
94a6760
Compare
In case of reaching maximum limit of serializable transaction retries.
dcdbf64
to
41a2b03
Compare
@hymRedemption 现在基本上把除管理后台以外的 controller 的 DML 操作都套上了 serializable transaction,你可以 review 一下,看看这种方式还存在哪些 bug、有没有遗漏、代码组织和风格上有哪些不好。 本来应该把 Serializable concern 的方法的文档继续完善一下,不过最近可能暂时没有时间,还要忙一些上线相关的事情~ |
好的, 没问题 |
目前使用 serializable 有以下几种方式(以 Order 模型为例):
之所以增加了这么多方式,一方面是出于灵活和便捷方面的考虑,另一方面是为了和 AASM 状态转换更好地结合。 |
下面简单说说这几种方式,先说最后一种: |
另外对于所有方式,需要记得提前做好两件事:
|
|
其它方式都与第二种类似,主要是语法上面提供了一些便利。但把第二种方法理解了应该就 OK 了~ |
先把这个 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 ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
将这些 before_action 放到每个 action 中去是什么原因呢?
@hymRedemption 因为查询( |
return save if user.phone == phone && user.phone_verified? | ||
transaction { user.update!(phone: phone) if save } | ||
serializable(nested_behaviour: :transaction) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个场景 isolation 我觉得设置成 read commit 就可以了。 因为这里不存在多个用户竞争同一资源的问题,只需要保证整个过程作为一个事务进行完成就行了。
There was a problem hiding this comment.
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。
serializable 模块实现本身我没有太多的疑问了。 现在剩下的需要讨论的就是,现在所有这些使用 transaction 的场景,是不是必须要使用 serialize 级别的 isolation。 因为很多场景并发冲突的几率并不高(例如接单的同时,用户取消订单),所以感觉全部用 serialize 没有必要。 我感觉用乐观锁的思想应该就可以解决,当然用 rails 的乐观锁需要明确去指出锁某些数据,有些场景有哪些数据需要控制我们可能不太好预见。但是我在看了 postgresql 的文档后,我发现它的 repeated read 级别的 isolation 可以用来代替乐观锁,而且能够同时锁住我们在这个事务中所有的数据,减去了我们去人为判断的需要。 postgresql 文档中说的是
对于并发情况是这么处理的:
所以利用这个机制,postgresql 完全能够提供乐观锁的并发处理机制。并且同时不需要我们人工判断到底要锁哪些数据。 因为整个 transaction 看到的数据都是这个事务开始之前的快照,而结束之后,由数据库来判断到底是否有其他并发对数据进行更改了。 而对于使用 serialize 级别的 isolation,我觉得只要不涉及数据集的统计相关计算,或者确定数据与某个数据集的关系。就都不需要这个级别的 isolation。(例如,判读数据是不是 uniq 等情况,而且对于判读数据 uniq 的情况,我发现在 rails 文档中说明,就算是 serialize 级别的 transaction 也不能保证 validates_uniqueness_of 能够正确给出结果,这个我很好奇是为什么?详见文档) |
现在确实冲突的概率很低,主要是因为每天的订单量并不多,因此可能即使完全不做数据库并发控制也几乎可以正常运行。但另一方面,从程序逻辑的严谨性、支付系统的重要性和安全性的角度来说,我觉得也是有必要让程序进一步完善的。 我个人并不完全赞同 repeatable read (in PostgreSQL) 是乐观锁的观点,虽然国内论坛上也有人把这一隔离级别作为乐观锁、甚至把 serializable 当作悲观锁,原因可能是 serializable 完全不需要 explicit locking。但实际上 serializable 事务也并非在物理执行上完全不会重叠,它只是在 repeatable read 的基础上增加了对 serialization anomaly 的监控而已,监控本身也不会带来任何阻塞。因此 serializable 事务如果能够被正确使用,基本足够应付各类并发情况,它的处理机制更像是乐观锁。而 repeatable read 对于一些并发情形仍然需要锁定整个表或部分行,所以通常需要将隔离级别和锁两种方法结合才能构成一种并发处理策略,而由于这种策略引入了 explicit locking,更适合处理 contention heavy 的情况,因此更像是悲观锁。
但我觉得问题正是 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。
这个其实在 9.1 以后的 PostgreSQL 上使用 serializable 事务已经可以做到正确检测 concurrent insert 了,uniqueness validation 的作用无非是做一下 select 查询,而实现了 predicate locking 的 PostgreSQL 在检测到冲突时会自动 abort 其中一个事务,当这个事务重试的时候 validation 的查询语句就可以生效了。之所以 Rails 文档说 serializable 可能会出现问题,是针对不同的数据库和版本的,比如以前版本的 PostgreSQL 就不行,因为它实际上只有两个隔离级别。当然,使用 unique index 才是最佳实践。 |
嗯,确实,现在的业务量并没有显出瓶颈问题。也是想得有点多,主要对数据库特性本身很多不太了解,不过借这个机会倒是熟悉了一些并发的东西。 |
这个出现死循环的情况是不是说,当业务量非常大的时候,很多 DML 操作都失败,会造成这种情况? |
我也是最近集中又把这些内容回顾了一下,其实我觉得可以不仅仅通过看文档、查找资料来学习,自己实际测试体验一下也许效果会更好。 |
也不是。主要是当这个 URL 本身的 DML 操作失败了,你要确保它不会被 rescue redirect_to 到它自己。 |
WIP.Almost done.
Code review required.