Rails 服务器(配合 passenger 启在 ubuntu 上)会接受 API,然后直接更新用户余额(可以理解为无脑加钱,因为有加密解密,以及验证)
用户反映,资金记录是没问题,但是余额不正确。比如:
MoneyRecord 1,金额 100,成功后用户余额 100
MoneyRecord 2,金额 200,成功后用户余额 200(这里应该是 300 才对)
after_save :check_status_change_to_success
def check_status_change_to_success
if self.status_changed? && self.success?
self.account.with_lock {
self.account.reload
self.account.balance = self.account.money_records.success.sum(:money)
self.account.save!
}
end
end
希望各位有经验的能帮忙讨论解决下。
self.account.reload(lock: true)
http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-reload
没有实际处理数据库问题的经验,只有纸上谈兵:
self.account.balance = self.account.money_records.success.sum(:money)
这个实现方案,不仅需要锁 account
记录所在的行,还要设法去锁 money_records
表(锁整表或者按查询条件锁多个行,具体要看数据库是否支持细粒度的锁)。再就是还要从数据库读取 money_records 进来,浪费时间。
如果换作是我来实现,会写成这样:
# @money_record 就是正在检查的,是否已完成金额转移的交易
self.account.balance += @money_record.money
看 Rails 的 API 文档实例,也是这么做的:http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html
话又说回到事务处理:transaction 功能并不能智能地摆平一切冲突问题。数据库可以设置成不同的隔离级别(isolation level)。高隔离级别更能保证数据完整性,但可能会牺牲性能。你的数据库具体支持哪些级别,你的应用场合需要哪种级别,这个只能花时间去研究了: https://www.bing.com/search?q=transaction+isolation
不要在大项目中乱用 after_save 这种 callback。否则会被坑的很惨。 我们公司现在的做法是给某次操作指定一个 key 然后用 redis 的 SETNX 来判断是否能进行操作。
赞同八楼的观点。我们公司也有类似的业务逻辑。业务复杂了以后,save 引起回调实在难以掌控。现在我们是把逻辑统一提取到 service 中处理。整个过程很清晰,维护起来方便很多。
是的,只有这个地方用了逻辑的 callback,其他地方基本都是 model 本身得检查或者初始化。否则好乱。另外用 redis 的 setnx 能具体说下是怎么来用吗?
我们也基本用 service,但是现在遇到 2 个问题:1 是 service 太多了。2 是 service 有可能会嵌套。不知道你们是怎么来用的?
一个 service 只干一个事情
你可以看下 redis 的 setnx 文档 这个命令可以在 redis 中获取一个状态 当 redis 中没有这个 key 的时候会返回 1 并且可以设置过期时间 有了返回 0 可以根据这个来判断当前是否有并发操作
一个 service 只干一个的话,会巨多 service 文件了。哈哈哈。我们好多业务逻辑。
我比较好奇你们用 setnx 来怎么来进行上面的回调操作。你们用 setnx 相当于是“加锁靠谱”队列是么?把要进行的操作塞到 redis,然后另外一个服务去拿,如果没操作过就执行?不知道流程是不是这样?
我们是按照业务类型区分文件夹的。目前有 30 个文件夹。 个人认为,如果把业务划分清楚,放文件夹下,会整齐一些,就好了 。
另外,“service 有可能会嵌套”。我目前还没有遇到过。能否举个栗子,看下呢?
begin;
insert into money_records(account_id, money) values(123, 100); # 1
select * from accounts where id=123 for update; # 2
select sum(money) from money_records where account_id=123; #3
update accounts set balance=200 where id=123; # 4
commit;
因为用了 after_save 所以 insert 语句是在锁 account 之前执行的,应该先锁 account 再去插入 money_record,即 #1 和 #2 顺序颠倒一下。而且#3 也是不需要的,锁住 account 的情况下,直接 blance+money 就够了
LZ fork 是指 Thread or Process?
另外,在后端开发的编程观念里,业务逻辑用多线程是没有意义的(除非异步 IO,先返回后存储,或者除非你写服务器)。
在一般场景中,因为你要 response 客户端,但是你开线程去执行的逻辑可能 request 已经 response 了,而 result 没拿到,结果 response 了没有 result 的结果。
嵌套:比如有个 service 是购买产品。另外一个 service 是根据条件自动购买产品。后面那个 service 就会去调用前面那个啦
实际在插入 moneyrecord 的时候在外面 service 已经锁住了 account。不想用简单的加钱操作,因为有加就有减,用重新计算资金记录比单纯加加减减更靠谱和可以幂等。哈哈
money_record = MoneyRecord.new
money_record.transaction do
money_record.save!
account.lock!
account.balance += money_record.money
account.save!
end
直接累加就好了,不用每次求和吧
这只要把
after_save :check_status_change_to_success
换成
after_commit :check_status_change_to_success, on: :create
就可以了呀!
突然注意到这个 if self.status_changed? 这一行 如果放在 after_commit 就失效了,所以用回调来处理业务逻辑就很蛋疼了
还成 after_save 去掉 x lock, 用 redis blpop 弄一个 semaphore 应该是可以的,这个有 gem 的。或者合成一个 sql,直接 update。