我正在构建一个需要 ACID 的 python 烧瓶后端。用法就像银行余额,一个必须准确增加,而另一个却减少了多少。
我正在将 heroku 与 postgres 以及 SQLAlchemy 用于我的 ORM。我像这样初始化数据库:
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy()
db.init_app(app)
JSON 正文采用付款接收方的电话号码以及付款发送方的访问令牌。整个路线很大,所以我不会只附加实际的数据库操作的数据验证。所以:
# getting the sender
user = User.verify_auth_token(data.get('access_token'))
# getting recipient (phone is unique)
recipient = db.session.query(User).filter_by(phone=data.get('phone')).first()
if recipient is None:
return make_error("Recipient does not exist")
else:
try:
good1 = user.update_balance(amount, True, ticker)
good2 = recipient.update_balance(amount, False, ticker)
transaction = Transaction(sender_id=user.id, recipient_id=recipient.id, amount=amount, operation="regular")
if good1 is None and good2 is None and transaction is not None:
db.session.add(transaction)
db.session.commit()
else:
db.session.rollback()
return make_error(good1)
return make_error(good2)
response["success"] = True
response["transaction"] = TransactionNested(many=False).dump(transaction)
return response
except:
db.session.rollback()
return make_error("Something went wrong")
为了操作余额,我使用了 user 的 update_balance 函数。该函数如下:
def update_balance(self, amount, sender, ticker):
balance = self.balances.filter_by(curr=ticker).first()
if balance is None:
return "Balance not supported by user"
if sender:
if balance.amount < amount:
return "Not enough funds to send"
else:
balance.amount = balance.amount - amount
else:
balance.amount = balance.amount + amount
因此,该函数要么返回字符串错误,要么在成功时不返回任何内容。 问题如下:当我运行说 3 个不同的 shell 窗口并运行一个机器人一遍又一遍地执行相同的交易时,两个用户的余额总和不正确。这意味着这里有一些东西使交易变得不酸。例如,如果我在循环中从用户 1 向用户 2 发送 10 次 1000 次,比如 time.sleep(0.5) 和 3 个不同的外壳,当不应该有一个单一的差异单位时,我最终会得到数百个平衡的差异。
任何帮助将不胜感激。
答案 0 :(得分:0)
update_balance
似乎不见了
db.session.add(balance)
答案 1 :(得分:0)
您似乎正在使用默认隔离级别(为 postgresql 提交读取),但您的代码假定值在执行过程中不会更改。例如,您读取余额 100,替换 10 并准备写入 90,但在此期间余额可能已经是 70(它已被其他工作人员更改),因此您将用不正确的 1 (90) 覆盖此值。< /p>
要改变这种行为,您需要串行运行代码,而不是并发运行:在数据库端(通过将隔离级别更改为可序列化)或在架构上(将所有请求放入队列)。
将事务隔离级别更改为 Serializable 似乎是最直接的解决方案。但它也有缺点:当序列化失败时,您需要调整代码以适应重试事务的逻辑:
SQLALCHEMY_ENGINE_OPTIONS = {'isolation_level': 'SERIALIZABLE'}
运行任务队列可以通过 celery 完成,它也有权衡(设置复杂性等)。