我需要检查credits
列(如果值> 0)并同时减去1(多个进程/连接将同时轰炸数据库,因此需要在原子时尚)。
仅仅启动事务并从那里获取+更新行就足够了吗?像:
Repo.transaction(fn ->
user = Repo.get(User, user_id)
if user.credits >= 1 do
MyRepo.update!(%{user | credits: user.credits - 1})
# ... If above works, create row in another table here.
end
end)
这是否会锁定"锁定"在用户行? (想象一下另一个想要在上面的交易中更新信用的过程。)
答案 0 :(得分:3)
这是否会锁定"锁定"在用户行? (想象一下另一个想要在上面的交易中更新信用的过程。)
没有。这是一个简单的演示:
alias MyApp.{Repo, User}
import Ecto.{Changeset, Query}
user_id = Repo.insert!(%User{credits: 1}).id
pid = spawn(fn ->
user = Repo.get!(User, user_id)
caller = receive do caller -> caller end
Repo.update!(user |> change |> put_change(:credits, -5))
send(caller, :cont)
end)
Repo.transaction(fn ->
user = Repo.get!(User, user_id)
IO.inspect {:before, user.credits}
if user.credits >= 1 do
# Ask the other process to update a column.
send(pid, self)
# Wait until the other process is done.
receive do :cont -> :cont end
IO.inspect {:middle, Repo.get!(User, user_id).credits}
Repo.update!(user |> change |> put_change(:credits, user.credits - 1))
end
IO.inspect {:after, Repo.get!(User, user.id).credits}
end)
输出:
{:before, 1}
{:middle, -5}
{:after, 0}
解决此问题的一种方法是获取row-level lock FOR UPDATE
之类的https://msdn.microsoft.com/en-us/magazine/jj991977.aspx。这将确保在提交或中止当前事务之前不会对同一行进行任何其他更改。
更改
user = Repo.get!(User, user_id)
到
user = from(u in User, where: u.id == ^user_id, lock: "FOR UPDATE") |> Repo.one!
上面代码片段中的事务中的将(正确地)创建死锁,因为在事务完成之前其他进程的SELECT将不会返回,并且事务将不会继续,直到另一个进程获得用户。 / p>
因此,总之,您应该替换:
user = Repo.get(User, user_id)
与
user = from(u in User, where: u.id == ^user_id, lock: "FOR UPDATE") |> Repo.one!
如果您想在事务完成之前阻止对返回用户的更新。
(免责声明:我不是数据库专家,但我相信我上面写的是正确的。如果有任何疑问或疑问,请发表评论!)