在我们自动完成的第一个版本中,我们从 LIST 中添加和删除项目。我们通过将我们的多个调用包装在 MULTI/EXEC 对中来实现这一点。回顾第 4.6 节,我们首先在游戏内物品市场的上下文中介绍了 WATCH/MULTI/EXEC 事务。 如果您还记得,市场被构建为单个 ZSET,成员是将对象和所有者 ID 连接在一起,以及物品价格作为分数。 每个用户都有自己的 HASH,其中包含用户名、当前可用资金和其他相关信息的列。 图 6.2 显示了市场、用户库存和用户信息的示例。
您还记得,为了将物品添加到市场,我们 WATCH 卖家的库存以确保该物品仍然可用,将该物品添加到市场 ZSET 中,并将其从用户的库存中移除。下面显示了第 4.4.2 节中我们早期的 list_item() 函数的核心。
def list_item(conn, itemid, sellerid, price): #...
pipe.watch(inv)
监视用户库存的变化。
if not pipe.sismember(inv, itemid): pipe.unwatch()
验证用户是否仍然具有要列出的项目。
return None
pipe.multi() pipe.zadd("market:", item, price) pipe.srem(inv, itemid) pipe.execute()
实际列出该项目。
return True #...
此代码中的简短注释只是隐藏了大量的设置和 WATCH/MULTI/EXEC 处理,这些处理隐藏了我们正在做的事情的核心,这就是我在此处省略它的原因。如果您觉得需要再次查看该代码,请随时跳回第 4.4.2 节以刷新您的记忆。
现在,为了回顾我们购买物品的过程,我们 WATCH 市场和买家的 HASH。 在获取买家的总资金和物品的价格后,我们验证买家是否有足够的钱。 如果买家有足够的钱,我们会在帐户之间转移资金,将物品添加到买家的库存中,然后从市场中移除该物品。 如果买家没有足够的钱,我们会取消交易。 如果 WATCH 错误是由其他人写入市场 ZSET 或买家 HASH 更改引起的,我们会重试。 以下清单显示了第 4.4.3 节中我们早期的 purchase_item() 函数的核心。
def purchase_item(conn, buyerid, itemid, sellerid, lprice): #...
pipe.watch("market:", buyer)
监视市场和买家帐户信息的更改。
price = pipe.zscore("market:", item) funds = int(pipe.hget(buyer, 'funds')) if price != lprice or price > funds: pipe.unwatch()
检查是否有售出/重新定价的物品或资金不足的情况。
return None
pipe.multi() pipe.hincrby(seller, 'funds', int(price)) pipe.hincrby(buyerid, 'funds', int(-price)) pipe.sadd(inventory, itemid) pipe.zrem("market:", item) pipe.execute()
将资金从买家转移到卖家,并将物品转移到买家。
return True #...
和以前一样,我们省略了设置和 WATCH/MULTI/EXEC 处理,以专注于我们正在做的事情的核心。
为了了解大规模锁的必要性,让我们花点时间在几个不同的负载场景中模拟市场。 我们将进行三个不同的运行:一个列表和一个购买过程,然后是五个列表过程和一个购买过程,最后是五个列表和五个购买过程。 表 6.1 显示了运行此模拟的结果。
列出的物品 |
购买的物品 |
购买重试 |
每次购买的平均等待时间 |
|
---|---|---|---|---|
1 个发布者,1 个购买者 |
145,000 |
27,000 |
80,000 |
14 毫秒 |
5 个发布者,1 个购买者 |
331,000 |
<200 |
50,000 |
150 毫秒 |
5 个发布者,5 个购买者 |
206,000 |
<600 |
161,000 |
498 毫秒 |
当我们的过载系统达到其极限时,我们从一个列表和购买过程完成的每次销售大约有 3 比 1 的重试率,一直到每次完成销售有 250 次重试。 结果,完成销售的延迟从适度负载系统中的 10 毫秒以下增加到过载系统中的近 500 毫秒。 这是一个完美的例子,说明了为什么 WATCH/MULTI/EXEC 事务有时无法在负载下扩展,这是因为在尝试完成交易时,我们失败并且不得不一遍又一遍地重试。 保持我们的数据正确很重要,但实际完成工作也很重要。 为了克服这个限制并实际开始大规模地执行销售,我们必须确保我们一次只在市场上列出或出售一个物品。 我们通过使用锁来做到这一点。