文章系列
- 为什么你应该关心?
- 可能出现什么问题?
- 采用新技术的障碍是什么?
- 新算法如何提供帮助?
上一篇文章解释了强一致性(与最终一致性相对)。本文是系列文章的第二部分,我们将解释缺乏强一致性如何使提供良好的最终用户体验变得更加困难,如何带来严重的工程开销,以及如何使您容易受到攻击。本部分较长,因为我们将解释不同的数据库异常,并通过几个示例场景,并简要介绍哪种类型的数据库容易受到每种异常的影响。
用户体验是任何应用程序成功的驱动因素,依赖于不一致的后端会增加提供良好体验的挑战。更重要的是,在不一致的数据之上构建应用程序逻辑会导致漏洞。一篇论文将这类攻击称为“ACIDrain”。他们调查了 12 个最流行的自托管电子商务应用程序,并发现了至少 22 种可能的严重攻击。一个网站是比特币钱包服务,由于这些攻击不得不关闭。当您选择一个不是 100% ACID 的分布式数据库时,就会有麻烦。正如我们之前的例子所解释的那样,由于误解、定义不明确的术语和激进的营销,工程师很难确定特定数据库提供的保证。
什么麻烦?您的应用程序可能会出现诸如帐户余额错误、用户奖励未收到、交易执行两次、消息乱序或违反应用程序规则等问题。有关为什么需要分布式数据库以及为什么它们很困难的快速介绍,请参阅我们的第一篇文章或此精彩的视频解释。简而言之,分布式数据库是一个将数据的副本保存在多个位置的数据库,原因是可扩展性、延迟和可用性。
我们将介绍这四个潜在问题(还有更多问题),并用游戏开发中的例子来说明它们。游戏开发很复杂,这些开发人员面临许多与现实生活中的严重问题非常相似的问题。游戏有交易系统、消息系统、需要满足条件的奖励等等。记住,如果事情出错或看起来出错,游戏玩家会多么生气(或高兴?)。在游戏中,用户体验至关重要,因此游戏开发人员通常承受着巨大的压力,以确保他们的系统具有容错性。
准备好了吗?让我们深入探讨第一个潜在问题!
1. 陈旧读取
陈旧读取是指返回旧数据的读取,换句话说,返回的值根据最新的写入尚未更新。许多分布式数据库,包括使用副本进行向上扩展的传统数据库(阅读第 1 部分以了解这些数据库的工作原理),都会出现陈旧读取问题。
对最终用户的影响
首先,陈旧读取会影响最终用户。而且这不是单一的影响。
令人沮丧的体验和不公平的优势
想象一下,游戏中两个用户遇到一个装有金币的宝箱。第一个用户从一个数据库服务器接收数据,而第二个用户连接到第二个数据库服务器。事件顺序如下:
- 用户 1(通过数据库服务器 1)看到并打开宝箱,取走金币。
- 用户 2(通过数据库服务器 2)看到一个装满的宝箱,打开它,失败了。
- 用户 2 仍然看到一个装满的宝箱,并且不明白为什么它会失败。
虽然这似乎是一个小问题,但结果是第二个玩家的体验令人沮丧。他不仅处于劣势,而且还经常会在游戏中看到一些看起来存在但实际上不存在的情况。接下来,让我们来看一个玩家对陈旧读取采取行动的例子!
陈旧读取导致重复写入
想象一下,游戏中一个角色试图在商店里购买盾牌和剑的情况。如果有多个位置包含数据并且没有智能系统来提供一致性,则一个节点包含的数据将比另一个节点旧。在这种情况下,用户可能会购买物品(联系第一个节点),然后检查他的库存(联系第二个节点),却发现物品不存在。用户可能会感到困惑,并可能认为交易没有成功。在这种情况下,大多数人会怎么做?好吧,他们会再次尝试购买该商品。一旦第二个节点赶上,用户已经购买了副本,一旦副本赶上,他突然发现他的钱用完了,每种物品都有两件。他认为我们的游戏坏了。
在这种情况下,用户花费了他不希望花费的资源。如果我们在这样的数据库之上编写电子邮件客户端,用户可能会尝试发送电子邮件,然后刷新浏览器,但无法检索他刚刚发送的电子邮件,因此会再次发送。在这样的系统之上提供良好的用户体验和实现安全的交易(例如银行交易)非常困难。
对开发人员的影响
在编码时,您总是必须预期某些东西(尚未)存在并相应地进行编码。当读取最终一致时,编写防故障代码变得非常具有挑战性,并且用户很可能会在您的应用程序中遇到问题。当读取最终一致时,这些问题会在您能够调查它们之前消失。基本上,您最终是在追逐幽灵。开发人员仍然经常选择最终一致的数据库或分发方法,因为通常需要时间才能注意到问题。然后,一旦他们的应用程序中出现问题,他们就会尝试发挥创意并在其传统数据库之上构建解决方案 (1, 2) 来修复陈旧读取。存在许多这样的指南以及 Cassandra 等数据库已经实现了一些一致性功能的事实表明,这些问题是真实的,并且比您想象的更频繁地在生产系统中造成问题。在非为一致性而构建的系统之上构建的自定义解决方案非常复杂且脆弱。如果存在开箱即用地提供强一致性的数据库,为什么有人会经历这样的麻烦呢?
表现出这种异常的数据库
使用主读取复制的传统数据库(PostgreSQL、MySQL、SQL Server 等)通常会出现陈旧读取问题。许多较新的分布式数据库也最初是最终一致的,或者换句话说,没有针对陈旧读取的保护。这是由于开发人员社区中的一种强烈信念,即这是扩展所必需的。最著名的数据库就是这样开始的,但它认识到其用户如何努力处理这种异常,并且此后提供了额外的措施来避免这种情况。较旧的数据库或未设计为高效提供强一致性的数据库(如 Cassandra、CouchDB 和 DynamoDB)默认情况下最终是一致的。Riak 等其他方法最终也是一致的,但通过实现冲突解决系统来降低过时值的几率,采取了不同的路径。但是,这并不能保证您的数据安全,因为冲突解决并非万无一失。
2. 丢失写入
在分布式数据库领域,当写入同时发生时,需要做出一个重要的选择。一种选择(安全的选择)是确保所有数据库节点都能就这些写入的顺序达成一致。这远非微不足道,因为它要么需要同步时钟(为此需要特定的硬件),要么需要像 Calvin 这样的不依赖于时钟的智能算法。第二个不太安全的选择是允许每个节点本地写入,然后稍后决定如何处理冲突。选择第二种选择的数据库可能会丢失您的写入。
对最终用户的影响
考虑游戏中两个交易,我们从 11 个金币开始并购买两件物品。首先,我们以 5 个金币的价格购买一把剑,然后以 5 个金币的价格购买一个盾牌,并且这两个交易都定向到我们分布式数据库的不同节点。每个节点读取该值,在本例中,对于两个节点来说仍然是 11。两个节点都将决定写入 6 作为结果 (11-5),因为它们不知道任何复制。由于第二个事务尚未看到第一个写入的值,因此玩家最终以总共 5 个金币的价格购买了剑和盾牌,而不是 10 个金币。对用户有利,但对系统不利!为了补救这种行为,分布式数据库有几种策略——有些比其他策略更好。
解决策略包括“最后写入获胜”(LWW)或“最长版本历史记录”(LVH)获胜。长期以来,LWW 一直是 Cassandra 的策略,如果您不进行不同的配置,它仍然是默认行为。
如果我们将 LWW 冲突解决应用于我们之前的示例,玩家仍然剩下 6 个金币,但只会购买一件物品。这是一种糟糕的用户体验,因为应用程序确认了他对第二件物品的购买,即使数据库没有将其识别为存在于他的库存中。
不可预测的安全性
正如您可能想象的那样,在这样的系统之上编写安全规则是不安全的。许多应用程序依赖于后端(或尽可能直接在数据库上)的复杂安全规则来确定用户是否可以访问资源。当这些规则基于不可靠地更新的陈旧数据时,我们如何才能确保永远不会发生违规?想象一下,PaaS 应用程序的一位用户打电话给他的管理员并询问:“您能否将此公共组设为私有,以便我们可以将其重新用于内部数据?”管理员应用该操作并告诉他已完成。但是,由于管理员和用户可能位于不同的节点上,因此用户可能会开始将敏感数据添加到技术上仍然是公共的组中。
对开发人员的影响
当写入丢失时,调试用户问题将是一场噩梦。想象一下,用户报告说他在您的应用程序中丢失了数据,然后过了一天才有时间回复。您将如何尝试找出问题是由您的数据库还是由有故障的应用程序逻辑引起的?在允许跟踪数据历史记录的数据库(如 FaunaDB 或 Datomic)中,您可以追溯到过去以查看数据是如何被操作的。但是,这些数据库都不会受到丢失写入的影响,并且确实容易受到这种异常影响的数据库通常不具备时光倒流功能。
容易丢失写入的数据库
所有使用冲突解决而不是冲突避免的数据库都会丢失写入。Cassandra 和 DynamoDB 使用最后写入获胜 (LWW) 作为默认值;MongoDB 曾经使用 LWW,但此后已放弃使用。传统数据库(如 MySQL)中的主-主分发方法提供不同的冲突解决策略。许多未为一致性而构建的分布式数据库都会丢失写入。Riak 最简单的冲突解决是由 LWW 驱动的,但它们也实现了更智能的系统。但即使使用智能系统,有时也没有明显的方法来解决冲突。Riak 和 CouchDB 将选择正确写入的责任交给客户端或应用程序,允许他们手动选择要保留哪个版本。
由于分发很复杂,大多数数据库都使用不完善的算法,因此当节点崩溃或出现网络分区时,许多数据库中都会经常发生丢失写入。即使是 MongoDB(它不分发写入(写入转到一个节点)),在节点在写入后立即宕机的罕见情况下也可能发生写入冲突。
3. 写入偏差
写入偏差是数据库供应商称之为快照一致性的保证类型中可能发生的事情。在快照一致性中,事务从事务开始时拍摄的快照中读取。快照一致性可以防止许多异常。事实上,许多人认为它是完全安全的,直到出现论文 (PDF) 证明事实并非如此。因此,开发人员难以理解为什么某些保证不够好也就不足为奇了。
在我们讨论快照一致性中哪些不起作用之前,让我们首先讨论哪些起作用。假设我们有一个骑士和法师之间的战斗,他们各自的生命力由四个心组成。
当任何角色受到攻击时,事务是一个计算已移除多少心的函数:
<code>damageCharacter(character, damage) { character.hearts = character.hearts - damage character.dead = isCharacterDead(character) }</code>
并且,在每次攻击之后,另一个 isCharacterDead 函数也会运行以查看角色是否还有任何心:
<code>isCharacterDead(character) { if (character.hearts </code>
在一个简单的情况下,骑士的攻击从法师身上移除了三个心,然后法师的法术从骑士身上移除了四个心,使他自己的生命值恢复到四个。如果一个事务在另一个事务之后运行,那么这两个事务在大多数数据库中的行为将是正确的。
但是,如果我们添加第三个事务,即骑士的攻击,它与法师的法术同时运行呢?
骑士死了吗,法师还活着吗?
为了处理这种混乱,快照一致性系统通常实现一个称为“第一个提交者获胜”的规则。只有当另一个事务尚未写入同一行时,事务才能结束,否则它将回滚。在这个例子中,由于两个事务都试图写入同一行(法师的健康值),只有生命吸取法术会起作用,而骑士的第二次攻击将被回滚。然后,最终结果将与之前的示例相同:一个死去的骑士和一个拥有完整生命值的法师。
但是,MySQL 和 InnoDB 等一些数据库并不认为“第一个提交者获胜”是快照隔离的一部分。在这种情况下,我们将有一个丢失写入:法师现在死了,尽管他应该在骑士的攻击生效之前从生命吸取中获得生命值。(我们确实提到了定义不明确的术语和宽松的解释,对吧?)
包含“第一个提交者获胜”规则的快照一致性确实很好地处理了一些事情,这并不奇怪,因为它长期以来被认为是一个很好的解决方案。这仍然是 PostgreSQL、Oracle 和 SQL Server 的方法,但它们都有不同的名称。PostgreSQL 将此保证称为“可重复读取”,Oracle 将其称为“可序列化”(根据我们的定义是不正确的),而 SQL Server 将其称为“快照隔离”。难怪人们在这个术语森林中迷路了。让我们看看它没有按预期运行的示例!
对最终用户的影响
接下来的战斗将发生在两支军队之间,如果所有军队的角色都死了,则一支军队被认为是死了的:
isArmyDead(army){ if (all characters are dead) { return true } else { return false } }
在每次攻击之后,以下函数确定角色是否死亡,然后运行上述函数以查看军队是否死亡:
damageArmyCharacter(army, character, damage){ character.hearts = character.hearts - damage character.dead = isCharacterDead(character) armyDead = isArmyDead(army) if (army.dead != armyDead){ army.dead = armyDead } }
首先,角色的心脏会因受到的伤害而减少。然后,我们通过检查每个角色是否没有生命值来验证军队是否死亡。然后,如果军队的状态发生了变化,我们将更新军队的“死亡”布尔值。
有三个法师分别攻击一次,导致三个“生命吸取”事务。快照是在事务开始时拍摄的,由于所有事务同时开始,因此快照是相同的。每个事务都有一个数据副本,其中所有骑士仍然拥有完整的生命值。
让我们看看第一个“生命吸取”事务是如何解决的。在这个事务中,mage1 攻击 knight1,骑士损失 4 个生命值,而攻击法师恢复了全部生命值。事务决定骑士军队没有死亡,因为它只能看到一个快照,其中两个骑士仍然拥有完整的生命值,一个骑士死了。其他两个事务作用于另一个法师和骑士,但以类似的方式进行。这些事务中的每一个最初在其数据副本中都有三个活着的骑士,并且只看到一个骑士死亡。因此,每个事务都决定骑士军队仍然活着。
当所有事务完成后,没有一个骑士还活着,但我们指示军队是否死亡的布尔值仍然设置为 false。为什么?因为在拍摄快照时,没有一个骑士死亡。因此,每个事务都看到自己的骑士死亡,但不知道军队中的其他骑士。虽然这是我们系统中的异常(称为写入偏差),但写入已通过,因为它们各自写入不同的角色,并且对军队的写入从未改变。太棒了,我们现在有一支幽灵军队!
对开发人员的影响
数据质量
如果我们想确保用户拥有唯一的名称怎么办?我们创建用户的事务将检查名称是否存在;如果不存在,我们将使用该名称写入新用户。但是,如果两个用户尝试使用相同的名称注册,快照将不会注意到任何事情,因为用户被写入不同的行,因此不会发生冲突。我们现在系统中有两个同名用户。
由于写入偏差,可能会出现许多其他异常示例。如果您感兴趣,Martin Kleppman 的著作《设计数据密集型应用程序》对此进行了更详细的描述。
以不同的方式编写代码以避免回滚
现在,让我们考虑一种不同的方法,其中攻击并非针对军队中的特定角色。在这种情况下,数据库负责选择首先应该攻击哪个骑士。
damageArmy(army, damage){ character = getFirstHealthyCharacter(knight) character.hearts = character.hearts - damage character.dead = isCharacterDead(character) // ... }
如果我们像之前的示例一样并行执行多次攻击,则 getFirstHealthyCharacter 将始终针对相同的骑士,这将导致多个事务写入同一行。这将被“第一个提交者获胜”规则阻止,该规则将回滚其他两次攻击。虽然它可以防止异常,但开发人员需要了解这些问题并围绕它们创造性地编写代码。但是,如果数据库开箱即用地为您做到这一点,岂不是更容易吗?
容易出现写入偏差的数据库
任何提供快照隔离而不是可序列化性的数据库都可能出现写入偏差。有关数据库及其隔离级别的概述,请参阅本文。
4. 乱序写入
为了避免丢失写入和陈旧读取,分布式数据库的目标是所谓的“强一致性”。我们提到数据库可以选择就全局顺序达成一致(安全的选择)或决定解决冲突(导致丢失写入的选择)。如果我们决定一个全局顺序,这意味着尽管剑和盾牌是并行购买的,但最终结果应该表现得好像我们先买了剑,然后买了盾牌一样。这通常也称为“线性化”,因为您可以线性化数据库操作。线性化是确保数据安全的黄金标准。
不同的供应商提供不同的隔离级别,您可以在此处进行比较。经常出现的一个术语是可序列化性,它是强一致性(或线性化)的略微不太严格的版本。可序列化性已经相当强,并且涵盖了大多数异常,但仍然为由于写入被重新排序而导致的一种非常细微的异常留下了空间。在这种情况下,即使事务已提交,数据库也可以自由地切换该顺序。简单来说,线性化是可序列化性加上保证的顺序。当数据库缺少此保证的顺序时,您的应用程序容易受到乱序写入的影响。
对最终用户的影响
对话重新排序
如果有人因错误而发送第二条消息,则可以以令人困惑的方式对对话进行排序。
用户操作重新排序
如果我们的玩家有 11 个金币,并且只是按重要性顺序购买物品,而没有主动检查他拥有的金币数量,那么数据库可以重新排序这些购买订单。如果他没有足够的钱,他本可以先购买最重要性最低的物品。
在这种情况下,有一个数据库检查验证我们是否有足够的黄金。想象一下,我们没有足够的钱,让账户低于零会花费我们钱,就像银行在您低于零时向您收取透支费一样。您可能会快速出售一件物品以确保您有足够的钱购买所有三件物品。但是,旨在增加您余额的销售可能会重新排序到交易列表的末尾,这将有效地将您的余额推至零以下。如果这是一家银行,您很可能会产生您绝对不应承担的费用。
不可预测的安全性
在配置安全设置后,用户会期望这些设置将应用于所有即将执行的操作,但是当用户通过不同的渠道相互交谈时,可能会出现问题。记住我们讨论过的例子,管理员正在与想要将一个组设为私有的用户通话,然后向其中添加敏感数据。虽然在提供可序列化性的数据库中,这种情况可能发生的时间窗口变得更小,但这仍然可能发生,因为管理员的操作可能直到用户操作之后才能完成。当用户通过不同的渠道进行沟通并期望数据库实时排序时,事情就会出错。
如果由于负载平衡而将用户重定向到不同的节点,则也会发生此异常。在这种情况下,两个连续的操作最终位于不同的节点上,并且可能会重新排序。如果一个女孩将她的父母添加到一个查看权限有限的 facebook 群组中,然后发布她的春假照片,那么这些图片仍然可能会出现在她父母的 feed 中。
在另一个例子中,自动交易机器人可能具有设置,例如最高购买价格、支出限额和要关注的股票列表。如果用户更改机器人应该购买的股票列表,然后更改支出限额,那么如果这些交易被重新排序并且交易机器人已将新分配的预算用于旧股票,他将不会感到高兴。
对开发人员的影响
漏洞
一些漏洞取决于事务的潜在反转。想象一下,游戏玩家一旦拥有 1,000 个金币就会获得奖杯,而他非常想要这个奖杯。游戏通过将多个容器的金币加在一起(例如他的存储和他的携带物(他的库存))来计算玩家有多少金币。如果玩家在存储和库存之间快速交换金钱,他实际上可以欺骗系统。
在下图中,第二个玩家充当犯罪伙伴,以确保存储和库存之间的资金转移发生在不同的交易中,从而增加这些交易被路由到不同节点的机会。现实世界中更严重的例子是使用第三方账户转账的银行;银行可能会错误计算某人是否有资格获得贷款,因为各种交易已被发送到不同的节点并且没有足够的时间进行排序。
容易出现乱序写入的数据库
任何不提供线性化的数据库都可能出现写入偏差。有关哪些数据库提供线性化的概述,请参阅本文。剧透:并不多。
所有异常都可能在一致性受限时返回
最后要讨论的强一致性的一个放松是仅保证在一定的范围内。典型的范围是数据中心区域、分区、节点、集合或行。如果您在对强一致性施加这些类型边界的数据库之上进行编程,那么您需要记住这些边界,以避免意外地再次打开潘多拉魔盒。
下面是一个一致性的示例,但仅保证在一个集合内。下面的示例包含三个集合:一个用于玩家,一个用于铁匠(即修理玩家物品的黑铁匠),另一个用于物品。每个玩家和每个铁匠都有一个指向物品集合中物品的 id 列表。
如果您想在两个玩家之间交易盾牌(例如,从 Brecht 到 Robert),那么一切都会很好,因为您仍然在一个集合中,因此您的事务仍然在保证一致性的范围内。但是,如果罗伯特的剑在铁匠那里进行修理,并且他想取回它怎么办?然后,事务跨越两个集合,即铁匠的集合和玩家的集合,并且保证被取消。这种限制通常存在于 MongoDB 等文档数据库中。然后,您需要更改编程方式以找到围绕限制的创造性解决方案。例如,您可以将物品的位置编码到物品本身。
当然,真正的游戏很复杂。您可能希望能够将物品掉落在地板上或将它们放置在市场上,以便物品可以由玩家拥有,但不一定在玩家的库存中。当事情变得更复杂时,这些变通方法会显着增加技术深度并改变您的编码方式以保持在数据库的保证范围内。
结论
我们已经看到了当您的数据库行为与预期不符时可能出现的不同问题的示例。虽然有些情况乍一看似乎微不足道,但它们都会对开发人员的生产力产生重大影响,尤其是在系统扩展时。更重要的是,它们使您容易受到不可预测的安全漏洞的攻击——这可能会对您应用程序的声誉造成不可挽回的损害。
我们讨论了几种一致性程度,但是现在我们已经看到了这些示例,让我们将它们放在一起:
还请记住,这些正确性保证中的每一个都可能带有边界:
最后,请意识到我们只提到了少数异常和一致性保证,而实际上还有更多。对于感兴趣的读者,我强烈推荐 Martin Kleppman 的《设计数据密集型应用程序》。
我们生活在一个我们不再需要关心的时代,只要我们选择一个没有限制的强一致性数据库即可。由于 Calvin(FaunaDB)和 Spanner(Google Spanner、FoundationDB)等新方法的出现,我们现在拥有多区域分布式数据库,这些数据库可以提供极低的延迟,并且在每种情况下都按预期运行。那么,为什么您还要冒着搬起石头砸自己的脚的风险,选择一个不提供这些保证的数据库呢?
在本系列的下一篇文章中,我们将介绍对开发人员体验的影响。为什么很难让开发人员相信一致性很重要?剧透:大多数人需要亲身体验才能看到必要性。但是请考虑一下:“如果出现错误,是您的应用程序错误,还是数据错误?您如何知道?”一旦数据库的限制表现为错误或糟糕的用户体验,您就需要解决数据库的限制,这会导致效率低下的粘合代码,这些代码无法扩展。当然,在那时,您已经投入了大量资金,并且意识到得太晚了。
文章系列
- 为什么你应该关心?
- 可能出现什么问题?
- 采用新技术的障碍是什么?
- 新算法如何提供帮助?
以上是一致的后端和UX:怎么了?的详细内容。更多信息请关注PHP中文网其他相关文章!

具有CSS的自定义光标很棒,但是我们可以将JavaScript提升到一个新的水平。使用JavaScript,我们可以在光标状态之间过渡,将动态文本放置在光标中,应用复杂的动画并应用过滤器。

互动CSS动画和元素相互启动的元素在2025年似乎更合理。虽然不需要在CSS中实施乒乓球,但CSS的灵活性和力量的增加,可以怀疑Lee&Aver Lee&Aver Lee有一天将是一场

有关利用CSS背景滤波器属性来样式用户界面的提示和技巧。您将学习如何在多个元素之间进行背景过滤器,并将它们与其他CSS图形效果集成在一起以创建精心设计的设计。

好吧,事实证明,SVG的内置动画功能从未按计划进行弃用。当然,CSS和JavaScript具有承载负载的能力,但是很高兴知道Smil并没有像以前那样死在水中

是的,让#039;跳上文字包装:Safari Technology Preview In Pretty Landing!但是请注意,它与在铬浏览器中的工作方式不同。

此CSS-tricks更新了,重点介绍了年鉴,最近的播客出现,新的CSS计数器指南以及增加了几位新作者,这些新作者贡献了有价值的内容。

在大多数情况下,人们展示了@Apply的@Apply功能,其中包括Tailwind的单个property实用程序之一(会改变单个CSS声明)。当以这种方式展示时,@Apply听起来似乎很有希望。如此明显


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

mPDF
mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

VSCode Windows 64位 下载
微软推出的免费、功能强大的一款IDE编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

ZendStudio 13.5.1 Mac
功能强大的PHP集成开发环境