REST 服务和竞争条件
REST service and race conditions
让我们想象一个问题:
我有一个 REST 服务,它是使用 Java/MySQL/Spring 和 HTTP/JSON 技术实现的。
REST 服务的客户端是移动应用程序。
所以有可能有人会反编译代码并获得REST服务的API。
(是的,代码被混淆等,但无论如何)。
问题:有一个 POST 方法可以向应用程序的其他用户汇款。
我担心,有人可以获得 API,编写一个机器人并使这个 POST 每秒请求 500 或 5,000 甚至 50,000 次。
结果,他可能会发送比他实际拥有的更多的钱,因为如果同时处理 1000 个请求,那么余额检查可能是
所有 1000 个请求都成功,但是一个帐户上的实际金额可能只够 50 个请求。
所以,基本上,它更像是多线程的标准 "race" 条件。
问题是,我有多个服务器,而且它们之间无论如何都不相关。
因此,300 个请求可以到达服务器 A,300 个请求可以到达服务器 B,其余请求可以到达服务器 C。
我最好的想法是使用 "SELECT ... FOR UPDATE" 之类的东西并在数据库级别进行同步。
但是,我想考虑其他解决方案。
有什么想法或建议吗?
您有几个选择:
依赖数据库的 ACID 实现(MySQL 在你的例子中)。假设您正在使用 InnoDB 引擎,您需要选择正确的事务隔离级别 (SET TRANSACTION syntax) in combination with the right locking reads mechanism (SELECT ... FOR UPDATE and SELECT ... LOCK IN SHARE MODE Locking Reads)。您需要很好地理解这些概念才能做出正确的选择。即使没有锁定读取,简单地使用正确的隔离级别也可能已经防止了竞争条件。缺点是您正在牺牲一致性以换取可伸缩性并将您的应用程序绑定到 RDBMS 数据库,因此您将更难迁移到 NoSQL。
将您的后端分解为 Web 层和服务层(atk 在评论中建议的选项)。这将允许您独立扩展 Web 层实例,同时保留单个服务层实例。拥有单个服务层实例可以使用 Java 同步机制,例如 synchronised
块或 ReadWriteLock
。尽管此解决方案可行,但我不推荐它,因为它会降低服务层的可扩展性。
这是对前一个选项的增强。您可以使用 Distributed lock manager 而不是内置的 java 同步机制。它将允许您独立扩展 Web 层和服务层。
对于关键任务应用程序,最好具有多级锁定机制。
"SELECT ... FOR UPDATE" 是一个很好的方法,但它们非常昂贵,当你尝试用 Charles 轰炸它时,你会发现你的上层 API 堆栈会受到影响,而且这种简单的机制会很容易地削弱您的基础设施,就像 DDoS 事件一样。
首先在负载 Balancer/Proxy 层实施它,以限制来自单个 IP 地址的每个指定时间间隔的 N 次请求。
然后应用共享缓存层锁,您的所有盒子都会在某些键上同步,具体取决于您要锁定的关键事务。例如,在进入关键代码路径之前,您可以使用 Redis GETSET 或 INCR 功能以原子方式设置标志。迅速拒绝其他任何事情,以免那些不良行为者坚持CPU/memory。
您还可以实现 APC 缓存之类的东西(在访问您的 Redis / Memcache 集群之前)以在每个盒子的基础上进行类似的锁定。由于不涉及网络延迟,因此速度更快。
除了使用 "SELECT ... FOR UPDATE"
之外还需要以上这些
让我们想象一个问题: 我有一个 REST 服务,它是使用 Java/MySQL/Spring 和 HTTP/JSON 技术实现的。 REST 服务的客户端是移动应用程序。 所以有可能有人会反编译代码并获得REST服务的API。 (是的,代码被混淆等,但无论如何)。
问题:有一个 POST 方法可以向应用程序的其他用户汇款。 我担心,有人可以获得 API,编写一个机器人并使这个 POST 每秒请求 500 或 5,000 甚至 50,000 次。 结果,他可能会发送比他实际拥有的更多的钱,因为如果同时处理 1000 个请求,那么余额检查可能是 所有 1000 个请求都成功,但是一个帐户上的实际金额可能只够 50 个请求。
所以,基本上,它更像是多线程的标准 "race" 条件。 问题是,我有多个服务器,而且它们之间无论如何都不相关。 因此,300 个请求可以到达服务器 A,300 个请求可以到达服务器 B,其余请求可以到达服务器 C。
我最好的想法是使用 "SELECT ... FOR UPDATE" 之类的东西并在数据库级别进行同步。 但是,我想考虑其他解决方案。
有什么想法或建议吗?
您有几个选择:
依赖数据库的 ACID 实现(MySQL 在你的例子中)。假设您正在使用 InnoDB 引擎,您需要选择正确的事务隔离级别 (SET TRANSACTION syntax) in combination with the right locking reads mechanism (SELECT ... FOR UPDATE and SELECT ... LOCK IN SHARE MODE Locking Reads)。您需要很好地理解这些概念才能做出正确的选择。即使没有锁定读取,简单地使用正确的隔离级别也可能已经防止了竞争条件。缺点是您正在牺牲一致性以换取可伸缩性并将您的应用程序绑定到 RDBMS 数据库,因此您将更难迁移到 NoSQL。
将您的后端分解为 Web 层和服务层(atk 在评论中建议的选项)。这将允许您独立扩展 Web 层实例,同时保留单个服务层实例。拥有单个服务层实例可以使用 Java 同步机制,例如
synchronised
块或ReadWriteLock
。尽管此解决方案可行,但我不推荐它,因为它会降低服务层的可扩展性。这是对前一个选项的增强。您可以使用 Distributed lock manager 而不是内置的 java 同步机制。它将允许您独立扩展 Web 层和服务层。
对于关键任务应用程序,最好具有多级锁定机制。
"SELECT ... FOR UPDATE" 是一个很好的方法,但它们非常昂贵,当你尝试用 Charles 轰炸它时,你会发现你的上层 API 堆栈会受到影响,而且这种简单的机制会很容易地削弱您的基础设施,就像 DDoS 事件一样。
首先在负载 Balancer/Proxy 层实施它,以限制来自单个 IP 地址的每个指定时间间隔的 N 次请求。
然后应用共享缓存层锁,您的所有盒子都会在某些键上同步,具体取决于您要锁定的关键事务。例如,在进入关键代码路径之前,您可以使用 Redis GETSET 或 INCR 功能以原子方式设置标志。迅速拒绝其他任何事情,以免那些不良行为者坚持CPU/memory。
您还可以实现 APC 缓存之类的东西(在访问您的 Redis / Memcache 集群之前)以在每个盒子的基础上进行类似的锁定。由于不涉及网络延迟,因此速度更快。
除了使用 "SELECT ... FOR UPDATE"
之外还需要以上这些