付款&交易
我维护一个支付平台,我们的用户可以从第三方代理处支付各种物品。不幸的是,我所在行业的性质是,这些付款活动并非实时发送给我们,它们捆绑在一起,并在几个星期后发送给我们。
这些是影响用户钱包余额的主要对象:
class Transaction(ndb.model):
user = ndb.KeyProperty(User, required=True)
amount = ndb.FloatProperty(required=True)
# ... other fields
class Payment(ndb.model):
user = ndb.KeyProperty(User, required=True)
amount = ndb.FloatProperty(required=True)
# ... other fields
@classmethod
def charge(cls, user, amount):
# ... make a call to braintree/stripe & save result if successful
(未显示退款,'商店信用',调整等)
电子钱包
然而,大部分交易金额是< $ 1由于我们必须将信用卡处理的成本转嫁给用户,因此我们的用户会与我们保持一致,以尽量减少这些费用。
他们可以加载10到200美元,交易从该余额中扣除,当他们的余额很低(低于2美元)时,我们会向他们的卡充值以补充他们的帐户。
这就是我设想钱包活动模式的工作方式
class WalletActivity(ndb.Model):
user = ndb.KeyProperty(User, required=True)
post_date = ndb.DateTimeProperty(required=True)
balance_increment = ndb.FloatProperty(required=True)
balance_result = ndb.FloatProperty(required=True)
# the key to the Transaction or Payment object that this is for
object_key = ndb.KeyProperty(required=True)
@classmethod
def create(cls, obj, previous_balance):
return WalletActivity(
user_key=obj.user,
post_date=datetime.datetime.now(),
balance_increment=obj.amount,
balance_result=previous_balance+obj.amount,
object_key=obj.key)
@classmethod
def fetch_last_wallet_activity(cls, user_key):
return cls.query(cls.user == user_key).order(-cls.post_date).get()
为了确定平衡,光谱的两端似乎是:
WalletActivity.fetch_last_wallet_activity().balance_result
)这里的正确答案听起来像2的组合。 在每个帐户的每一天结束时存储某种BalanceUpdate / WalletDaySummary对象。 然后,您只需总结今天的活动并将其添加到昨天的BalanceUpdate中。 https://stackoverflow.com/a/4376221/4458510
class BalanceUpdate(ndb.model):
user = ndb.KeyProperty(User)
cut_off_date = ndb.DateTimeProperty()
balance = ndb.IntegerProperty()
@classmethod
def current_balance(cls, user_key):
last_balance_update = cls.query(cls.user == user_key).order(
-cls.cut_off_date).get()
recent_wallet_activity = WalletActivity.query(cls.user == user_key,
cls.post_date > last_balance_update.cut_off_date).fetch()
return (last_balance_update.balance +
sum([i.balance_increment for i in recent_wallet_activity]))
但是,这可能不适用于在一天内产生大量交易的公司帐户。
最好使用最新balance_result
WalletActivity
选项1
要处理一批交易,我们
代码:
def _process_transactions(user, transactions, last_wallet_activity):
transactions_amount = sum([i.amount for i in transactions])
# 2. Replenish their account if the existing balance is low
if last_wallet_activity.balance_result - transactions_amount < user.wallet_bottom_threshold:
payment = Payment.charge(
user=user,
amount=user.wallet_replenish_amount + transactions_amount)
payment.put()
last_wallet_activity = WalletActivity.create(
obj=payment,
previous_balance=last_wallet_activity.balance_result)
last_wallet_activity.put()
# 3. Add the transactions to their wallet
new_objects = []
for transaction in transactions:
last_wallet_activity = WalletActivity.create(
obj=transaction,
previous_balance=last_wallet_activity.balance_result)
new_objects.append(last_wallet_activity)
ndb.put_multi(new_objects)
return new_objects
def process_transactions_1(user, transactions):
# 1. Get the user's balance from the last WalletActivity
last_wallet_activity = WalletActivity.fetch_last_wallet_activity(user_key=user.key)
return _process_transactions(user, transactions, last_wallet_activity)
WalletActivity.fetch_last_wallet_activity().balance_result
和{。}的问题
BalanceUpdate.current_balance()
是数据存储区查询最终是一致的。
我想过使用实体组&amp;祖先查询,但听起来你会遇到争用错误:
选项2 - 按键获取最后一个WalletActivity
我们可以跟踪上一个WalletActivity
的密钥,因为按密钥提取非常一致:
class LastWalletActivity(ndb.Model):
last_wallet_activity = ndb.KeyProperty(WalletActivity, required=True)
@classmethod
def get_for_user(cls, user_key):
# LastWalletActivity has the same key as the user it is for
return ndb.Key(cls, user_key.id()).get(use_cache=False, use_memcache=False)
def process_transactions_2(user, transactions):
# 1. Get the user's balance from the last WalletActivity
last_wallet_activity = LastWalletActivity.get_for_user(user_key=user.key)
new_objects = _process_transactions(user, transactions, last_wallet_activity.last_wallet_activity)
# update LastWalletActivity
last_wallet_activity.last_wallet_activity = new_objects[-1].key
last_wallet_activity.put()
return new_objects
或者,我可以将last_wallet_activity
存储在User
对象上,但我不想担心竞争条件
用户更新其电子邮件并清除last_wallet_activity
选项3 - 付款锁
但是竞争条件如何,其中2个作业试图同时处理同一用户的交易。 我们可以添加另一个对象来锁定&#39;一个帐户。
class UserPaymentLock(ndb.Model):
lock_time = ndb.DateTimeProperty(auto_now_add=True)
@classmethod
@ndb.transactional()
def lock_user(cls, user_key):
# UserPaymentLock has the same key as the user it is for
key = ndb.Key(cls, user_key.id())
lock = key.get(use_cache=False, use_memcache=False)
if lock:
# If the lock is older than a minute, still return False, but delete it
# There are situations where the instance can crash and a user may never get unlocked
if datetime.datetime.now() - lock.lock_time > datetime.timedelta(seconds=60):
lock.key.delete()
return False
key.put()
return True
@classmethod
def unlock_user(cls, user_key):
ndb.Key(cls, user_key.id()).delete()
def process_transactions_3(user, transactions):
# Attempt to lock the account, abort & try again if already locked
if not UserPaymentLock.lock_user(user_key=user.key):
raise Exception("Unable to acquire payment lock")
# 1. Get the user's balance from the last WalletActivity
last_wallet_activity = LastWalletActivity.get_for_user(user_key=user.key)
new_objects = _process_transactions(user, transactions, last_wallet_activity.last_wallet_activity)
# update LastWalletActivity
last_wallet_activity.last_wallet_activity = new_objects[-1].key
last_wallet_activity.put()
# unlock the account
UserPaymentLock.unlock_user(user_key=user.key)
return new_objects
我想在事务中尝试完成这一切,但我需要阻止将2 http发送到braintree / stripe。
我倾向于选项3,但随着我介绍的每个新模型,系统越来越脆弱。
答案 0 :(得分:2)
一般情况下,我完全同意Dan's answer与您的original question,但在您的特定用例中,与大型实体组合作可能是合理的。
每秒对同一实体组的写操作数超过1次时可能会发生争用错误,例如:一个特定的钱包。此限制也适用于已从数据存储区事务中的数据存储区读取的实体,但未明确写回(我的理解是它们为written together with the modified entities for serializability)。
虽然,1秒规则不是强制限制,根据我的经验,云数据存储通常可以处理略高于此限制的短突发,但无法保证,并且通常建议和最佳做法是避免大型实体组,特别是在写操作不是来自同一用户的情况下。相反,将用户发布的所有评论存储在同一实体组(author = parent)中可能是安全的,因为用户每秒可以发布多个评论是非常不可能的,甚至可能是不可取的。另一个例子可能是时间敏感且不面向用户的后台任务,其中实体组的写操作是按实体组编排的,或者至少可以在发生争用时显着退回写操作。 / p>
在以非常高的速率添加新实体的情况下,争用错误也可能由单调增加的键/ ID或索引属性和复合索引引起,其中索引值彼此太靠近(例如时间戳)。建议让Datastore自动创建新实体的ID(例如用户ID),因为Datastore会将密钥扩展得足够远。并且要么避免索引可以发生单调递增值的属性,要么使用散列为值添加前缀。
Cloud Datastore文章Best Practices包含一个关于Designing for scale的部分,该部分提供了非常有用的建议。
也就是说,可以更容易地设计一个应用程序,以便在写入限制内安全地工作,并依赖于数据存储(或其他数据库)的事务性和强一致性支持,而不是编写试图模仿的应用程序逻辑交易方面。竞争条件,死锁等等可能出错,使系统更加脆弱,容易出错。
在原始问题中,您提到了公司帐户和付款,这表明了一些实时付款解决方案。场景:该公司中的数千名用户可以为单个帐户提交新的交易,但很多人可能同时这样做。如果每个事务都是同一个实体组(公司帐户),这很容易导致争用错误。如果在写操作中实现重试,那么这将导致延迟延迟,直到用户获得对其事务请求的响应。但即使重试,写操作也可能经常失败,用户经常会遇到服务器错误。这将带来糟糕的用户体验。
我倾向于选项(2),但有Wallet
类。您表示担心存储last_wallet_activity
的位置。您可以拥有自己的Wallet
种类,它始终与User
具有相同的ID。在这种情况下,您可以拥有两个单独的实体组,而不必关心用户触发的User
对象的中间更改。我也会使用数据存储区事务。它允许同一事务中最多25个不同的实体组,即一批中可能有23个事件。根据您当前的设计,这也是您每个钱包的最大写入率。不确定这是您的应用程序的可接受限制。
但是,如果某个钱包的交易真的很多(通过频繁更新last_wallet_activity
),您可能再次面临争用的风险。为了实现每个钱包更高的写入速率并避免这种争用,您还可以将选项(1)与某种sharding结合使用。
选项(3)尝试实现事务方面(见前文)。选项(1)确实遭受了查询最终只是一致的事实。
但是,这里的问题提到所有这些钱包活动都是在后台处理(而不是由用户请求处理),并且事件不是实时处理的,而是延迟数小时或数周。这将允许通过后台任务批量处理它们。假设您的应用将保持在另一个Cloud Datastore limits内,例如对于10 MiB的交易的最大大小,您的应用可以将每个公司钱包(实体组)的最多500个钱包活动批量转换为每秒一次写入操作。这相当于每个公司帐户每小时1.8米钱包活动。即使有重试和停电,这对即使是最大的企业账户也足够了吗?如果是,如果您的产品永远不会改变为面向用户的实时钱包活动,我就不明白为什么您不应该将钱包活动放入每个钱包的实体组中。另一种方法当然是每个公司帐户只有多个钱包。
在这种情况下,您的选项(1)应该有效,因为祖先查询(钱包是WalletActivity
查询中的祖先)非常一致。
ndb.put_multi()
我已经看到您在同一个请求中使用多个put()
调用,您可以在某些toPut
列表中完美地收集实体,然后将所有实体一起编写。通过这样做,您可以保存实例运行时,还可以减少对同一实体组的写操作数。
关于对Braintree或Stripe的付款请求:
您需要处理Braintree / Stripe拒绝付款请求的案例。
我也不知道你的活动是如何到达应用程序的,因此我不确定如何协调每个钱包的批处理任务。上面的模式表明,对于每个钱包,只有一个任务在运行(即,不是同一个钱包并行的多个任务/批次)。但是,有不同的方法可以执行此操作,具体取决于事件在您的应用中的到达方式。