dot Redis 8 来了 - 而且是开源的

了解更多

6.2.1 为什么锁很重要

返回首页

6.2.1 为什么锁很重要

在我们自动完成的第一个版本中,我们从 LIST 中添加和删除项目。我们通过将我们的多个调用包装在 MULTI/EXEC 对中来实现这一点。回顾第 4.6 节,我们首先在游戏内物品市场的上下文中介绍了 WATCH/MULTI/EXEC 事务。 如果您还记得,市场被构建为单个 ZSET,成员是将对象和所有者 ID 连接在一起,以及物品价格作为分数。 每个用户都有自己的 HASH,其中包含用户名、当前可用资金和其他相关信息的列。 图 6.2 显示了市场、用户库存和用户信息的示例。

您还记得,为了将物品添加到市场,我们 WATCH 卖家的库存以确保该物品仍然可用,将该物品添加到市场 ZSET 中,并将其从用户的库存中移除。下面显示了第 4.4.2 节中我们早期的 list_item() 函数的核心。

图 6.2 第 4.6 节中我们市场的结构。 左边是市场上的四个物品——ItemA、ItemC、ItemE 和 ItemG——价格分别为 35、48、60 和 73,卖家 ID 分别为 4、7、2 和 3。 在中间,我们有两个用户 Frank 和 Bill,以及他们的当前资金,右边是他们的库存。
清单 6.6 第 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() 函数的核心。

清单 6.7 第 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 显示了运行此模拟的结果。

表 6.1 高负载市场在 60 秒内的性能
 

列出的物品

购买的物品

购买重试

每次购买的平均等待时间

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 事务有时无法在负载下扩展,这是因为在尝试完成交易时,我们失败并且不得不一遍又一遍地重试。 保持我们的数据正确很重要,但实际完成工作也很重要。 为了克服这个限制并实际开始大规模地执行销售,我们必须确保我们一次只在市场上列出或出售一个物品。 我们通过使用锁来做到这一点。