高い同時実行の下のデータベース設計

データベースの学習:高並行データベース設計

グループは、時代の爆発や数千もが直面百倍の要求圧力を支払ったとして、ハードウェアがエスカレートスナップ、音楽などの音楽に。商品の購入の最後のリンクとして、ユーザーが迅速に支払い安定性を完了していることを確認することが特に重要です。だから、2003年11月に、我々は全体の決済システムの総合的実施インフラ、それが一貫秒10万件の注文を処理する能力を持っているように、アップグレードを。これは、スパイク活動として生態購入音楽の様々な形のための強力なサポートを提供します。

ライブラリポイントテーブル

Redisのインターネットの時代、勝っmemcachedのキャッシュシステムのような、第二の読み取り専用システムごと10万は複雑ではありませんサポートを構築、一貫したハッシュがキャッシュノード、水平スケーリングWebサーバによって拡張ということです。支払秒ペンあたり10万注文を処理するためのシステム、毎秒何十万も必要データベースの更新操作(挿入プラス更新)、これは任意の独立したデータベース上不可能な作業ですので、我々は最初に行う必要があります分割されている(順序と称する)注文フォームとサブテーブルのライブラリです。

通常、ユーザID(UID称する)フィールドと、データベースの操作中に、私たちは、UIDライブラリサブテーブルを分割することを選択します。

私たちが選んだのサブライブラリー戦略「バイナリーツリーサブライブラリーは、」いわゆる「バイナリサブライブラリー」とは:私達のデータベースの拡張中、拡張のための2の倍数に基づいています。たとえば、次のように1〜2個の拡張ユニット、拡張ユニット2~4、4〜8個の拡張ユニット、および。このサブライブラリーアプローチの利点は、行レベルのデータ同期用のスクリプトを記述する必要なく、私たちの拡張時、唯一のテーブルレベルのDBAのデータ同期、ということです。

ただ、ライブラリは十分に、持続的な圧力の後にされていない見て、テスト、我々は同じデータベースに、同時更新複数のテーブルの効率がテーブルに同時更新よりもはるかに大きいことがわかったので、我々は、各サブライブラリーでありますテーブルの順序は、10個の部分に分割:order_0、order_1、...、order_9 ..

最後に、我々は、各サブライブラリ10のパーツテーブル(order_9をorder_0に対応する、0〜9の番号が付け)、展開構造、(DB8にそれぞれDB1、1~8と番号付け)8つのサブライブラリの順序テーブルを配置しました下図のように:

画像のキャプション

UIDは、データベースの数に応じて計算されます。

データベース番号=(UID / 10)%8 + 1

テーブル番号をUIDに計算されます。

テーブルID = UIDの10%

場合UID、上記に係る= 9527 アルゴリズムダイ952 1〜8のプラス番号データベース、及び7またはテーブル数に等しい、請求、実際には、二つの部分952及び7にUIDです。だから、UID = 9527注文情報は、ライブラリorder_7のテーブルルックアップをDB1する必要があります。詳細はまた、アルゴリズムのフロー図を参照してください。

画像のキャプション

サブライブラリーサブテーブルを有する構造とアルゴリズムは、サブライブラリーサブテーブルツールの2種類について、現在市場に出て、最後のサブライブラリーのサブテーブルのインプリメンテーションツールを見つけることです。

  1. クライアントライブラリサブサブテーブル、クライアント側のサブテーブルの操作上のサブライブラリー、直接データベース

  2. 使用するサブライブラリーの中間サブテーブルを、ミドルウェアサブライブラリー操作のサブテーブルによって完成しても、中間サブテーブルのサブライブラリー・クライアント、

ツールの両方のタイプは、市場で利用可能な、ここで列挙されていない、総合的なツールこれらの2つのタイプには長所と短所があります。直接接続データベース、ミドルウェア15%〜20%以上のサブライブラリサブテーブルの性能にクライアントライブラリサブサブテーブル。サブライブラリー一元管理するので、サブテーブルミドルウェアミドルウェア、サブライブラリー操作サブテーブルとクライアント分離、モジュール分割明確の使用、DBAは、一元管理を容易にします。

我々は、我々が開発し、アクセスフレームワークのデータレイヤのオープンソースのセットを所有しているため、それがサブライブラリーのサブテーブルの機能のための「マンゴー」マンゴーフレームワークネイティブサポートコードネームだし、設定は非常に簡単です、クライアントのサブライブラリーのサブテーブルを選択しました。

  • マンゴーホーム:mango.jfaster.org

  • マンゴーソース:github.com/jfaster/mango

第二に、注文ID(UID寸法)

IDのオーダーシステムは、最も簡単な方法は、配列のデータベースを使用することで、グローバルにユニークな特徴を持っている必要がありますが、第二10万件の注文の処理をサポートしたい場合は、それぞれの動作時間は、少なくともその、毎秒、グローバルに一意なIDの増分を取得することができますID 100,000注文を生成する必要が、データベースIDによって生成された増分は明らかに、上記の要件を完了することができません。だから我々は唯一のグローバルに一意の注文IDメモリ・コンピューティングを介して取得することができます。

Java领域最著名的唯一ID应该算是UUID了,不过UUID太长而且包含字母,不适合作为订单ID。通过反复比较与筛选,我们借鉴了Twitter的Snowflake算法,实现了全局唯一ID。下面是订单ID的简化结构图:

画像のキャプション

上图分为3个部分:

  1. 时间戳

这里时间戳的粒度是毫秒级,生成订单ID时,使用System.currentTimeMillis()作为时间戳。

  1. 机器号

每个订单服务器都将被分配一个唯一的编号,生成订单ID时,直接使用该唯一编号作为机器号即可。

  1. 自增序号

当在同一服务器的同一毫秒中有多个生成订单ID的请求时,会在当前毫秒下自增此序号,下一个毫秒此序号继续从0开始。比如在同一服务器同一毫秒有3个生成订单ID的请求,这3个订单ID的自增序号部分将分别是0,1,2。

上面3个部分组合,我们就能快速生成全局唯一的订单ID。不过光全局唯一还不够,很多时候我们会只根据订单ID直接查询订单信息,这时由于没有uid,我们不知道去哪个分库的分表中查询,遍历所有的库的所有表?这显然不行。所以我们需要将分库分表的信息添加到订单ID上,下面是带分库分表信息的订单ID简化结构图:

画像のキャプション

我们在生成的全局订单ID头部添加了分库与分表的信息,这样只根据订单ID,我们也能快速的查询到对应的订单信息。

分库分表信息具体包含哪些内容?第一部分有讨论到,我们将订单表按uid维度拆分成了8个数据库,每个数据库10张表,最简单的分库分表信息只需一个长度为2的字符串即可存储,第1位存数据库编号,取值范围1到8,第2位存表编号,取值范围0到9。

还是按照第一部分根据uid计算数据库编号和表编号的算法,当uid=9527时,分库信息=1,分表信息=7,将他们进行组合,两位的分库分表信息即为”17”。具体算法流程参见下图:

画像のキャプション

上述使用表编号作为分表信息没有任何问题,但使用数据库编号作为分库信息却存在隐患,考虑未来的扩容需求,我们需要将8库扩容到16库,这时取值范围1到8的分库信息将无法支撑1到16的分库场景,分库路由将无法正确完成,我们将上诉问题简称为分库信息精度丢失。

为解决分库信息精度丢失问题,我们需要对分库信息精度进行冗余,即我们现在保存的分库信息要支持以后的扩容。这里我们假设最终我们会扩容到64台数据库,所以新的分库信息算法为:

分库信息 = (uid / 10) % 64 + 1

当uid=9527时,根据新的算法,分库信息=57,这里的57并不是真正数据库的编号,它冗余了最后扩展到64台数据库的分库信息精度。我们当前只有8台数据库,实际数据库编号还需根据下面的公式进行计算:

实际数据库编号 = (分库信息 - 1) % 8 + 1

当uid=9527时,分库信息=57,实际数据库编号=1,分库分表信息=”577”。

由于我们选择模64来保存精度冗余后的分库信息,保存分库信息的长度由1变为了2,最后的分库分表信息的长度为3。具体算法流程也可参见下图:

画像のキャプション

如上图所示,在计算分库信息的时候采用了模64的方式冗余了分库信息精度,这样当我们的系统以后需要扩容到16库,32库,64库都不会再有问题。

当577找不到订单时,采用(分库信息57 - 1) % 8 + 1,就能找到实际数据库编号。

可以这么理解:

1、扩容后的分库分表信息,可以兼容扩容之前的分库分表信息,也就是说:9527这个uid本来按照64库设计是在57库1表中,由于目前最多只有8库,所有只能让它分配在8库中,经换算是在7库1表中。

9527按照8库设计也是在7库1表中。这种算法在某中程度上换算结果是一致的。

2、扩容后的分库分表,后续的用户会均匀分布在扩容的库和表中;之前的库表信息存在就存在了,所以说扩容前的已存在的库表中存的订单信息(如8库9表)要比后面扩容后(如64库9表)存在的订单信息(9-64库9表) 要多。也就是说扩容前的库表信息冗余了。

上面的订单ID结构已经能很好的满足我们当前与之后的扩容需求,但考虑到业务的不确定性,我们在订单ID的最前方加了1位用于标识订单ID的版本,这个版本号属于冗余数据,目前并没有用到。下面是最终订单ID简化结构图:

画像のキャプション

Snowflake算法:github.com/twitter/snowflake

三、最终一致性(bid维度)

到目前为止,我们通过对order表uid维度的分库分表,实现了order表的超高并发写入与更新,并能通过uid和订单ID查询订单信息。但作为一个开放的集团支付系统,我们还需要通过业务线ID(又称商户ID,简称bid)来查询订单信息,所以我们引入了bid维度的order表集群,将uid维度的order表集群冗余一份到bid维度的order表集群中,要根据bid查询订单信息时,只需查bid维度的order表集群即可。

上面的方案虽然简单,但保持两个order表集群的数据一致性是一件很麻烦的事情。两个表集群显然是在不同的数据库集群中,如果在写入与更新中引入强一致性的分布式事务,这无疑会大大降低系统效率,增长服务响应时间,这是我们所不能接受的,所以我们引入了消息队列进行异步数据同步,来实现数据的最终一致性。当然消息队列的各种异常也会造成数据不一致,所以我们又引入了实时监控服务,实时计算两个集群的数据差异,并进行一致性同步

下面是简化的一致性同步图:

画像のキャプション

四、数据库高可用

没有任何机器或服务能保证在线上稳定运行不出故障。比如某一时间,某一数据库主库宕机,这时我们将不能对该库进行读写操作,线上服务将受到影响。

所谓数据库高可用指的是:当数据库由于各种原因出现问题时,能实时或快速的恢复数据库服务并修补数据,从整个集群的角度看,就像没有出任何问题一样。需要注意的是,这里的恢复数据库服务并不一定是指修复原有数据库,也包括将服务切换到另外备用的数据库。

数据库高可用的主要工作是数据库恢复与数据修补,一般我们以完成这两项工作的时间长短,作为衡量高可用好坏的标准。这里有一个恶性循环的问题,数据库恢复的时间越长,不一致数据越多,数据修补的时间就会越长,整体修复的时间就会变得更长。所以数据库的快速恢复成了数据库高可用的重中之重,试想一下如果我们能在数据库出故障的1秒之内完成数据库恢复,修复不一致的数据和成本也会大大降低。

下图是一个最经典的主从结构:

画像のキャプション

上图中有1台web服务器和3台数据库,其中DB1是主库,DB2和DB3是从库。我们在这里假设web服务器由项目组维护,而数据库服务器由DBA维护。

当从库DB2出现问题时,DBA会通知项目组,项目组将DB2从web服务的配置列表中删除,重启web服务器,这样出错的节点DB2将不再被访问,整个数据库服务得到恢复,等DBA修复DB2时,再由项目组将DB2添加到web服务。

当主库DB1出现问题时,DBA会将DB2切换为主库,并通知项目组,项目组使用DB2替换原有的主库DB1,重启web服务器,这样web服务将使用新的主库DB2,而DB1将不再被访问,整个数据库服务得到恢复,等DBA修复DB1时,再将DB1作为DB2的从库即可。

上面的经典结构有很大的弊病:不管主库或从库出现问题,都需要DBA和项目组协同完成数据库服务恢复,这很难做到自动化,而且恢复工程也过于缓慢。

我们认为,数据库运维应该和项目组分开,当数据库出现问题时,应由DBA实现统一恢复,不需要项目组操作服务,这样便于做到自动化,缩短服务恢复时间。

先来看从库高可用结构图:

画像のキャプション

如上图所示,web服务器将不再直接连接从库DB2和DB3,而是连接LVS负载均衡,由LVS连接从库。这样做的好处是LVS能自动感知从库是否可用,从库DB2宕机后,LVS将不会把读数据请求再发向DB2。同时DBA需要增减从库节点时,只需独立操作LVS即可,不再需要项目组更新配置文件,重启服务器来配合。

再来看主库高可用结构图:

画像のキャプション

如上图所示,web服务器将不再直接连接主库DB1,而是连接KeepAlive虚拟出的一个虚拟ip,再将此虚拟ip映射到主库DB1上,同时添加DB_bak从库,实时同步DB1中的数据。正常情况下web还是在DB1中读写数据,当DB1宕机后,脚本会自动将DB_bak设置成主库,并将虚拟ip映射到DB_bak上,web服务将使用健康的DB_bak作为主库进行读写访问。这样只需几秒的时间,就能完成主数据库服务恢复。

组合上面的结构,得到主从高可用结构图:

画像のキャプション

数据库高可用还包含数据修补,由于我们在操作核心数据时,都是先记录日志再执行更新,加上实现了近乎实时的快速恢复数据库服务,所以修补的数据量都不大,一个简单的恢复脚本就能快速完成数据修复。

五、数据分级

支付系统除了最核心的支付订单表与支付流水表外,还有一些配置信息表和一些用户相关信息表。如果所有的读操作都在数据库上完成,系统性能将大打折扣,所以我们引入了数据分级机制。

我们简单的将支付系统的数据划分成了3级:

第1级:订单数据和支付流水数据;这两块数据对实时性和精确性要求很高,所以不添加任何缓存,读写操作将直接操作数据库。

第2级:用户相关数据;这些数据和用户相关,具有读多写少的特征,所以我们使用redis进行缓存。

第3级:支付配置信息;这些数据和用户无关,具有数据量小,频繁读,几乎不修改的特征,所以我们使用本地内存进行缓存。

使用本地内存缓存有一个数据同步问题,因为配置信息缓存在内存中,而本地内存无法感知到配置信息在数据库的修改,这样会造成数据库中数据和本地内存中数据不一致的问题。

为了解决此问题,我们开发了一个高可用的消息推送平台,当配置信息被修改时,我们可以使用推送平台,给支付系统所有的服务器推送配置文件更新消息,服务器收到消息会自动更新配置信息,并给出成功反馈。

六、粗细管道

***,前端重试等一些原因会造成请求量的暴涨,如果我们的服务被激增的请求给一波打死,想要重新恢复,就是一件非常痛苦和繁琐的过程。

举个简单的例子,我们目前订单的处理能力是平均10万下单每秒,峰值14万下单每秒,如果同一秒钟有100万个下单请求进入支付系统,毫无疑问我们的整个支付系统就会崩溃,后续源源不断的请求会让我们的服务集群根本启动不起来,唯一的办法只能是切断所有流量,重启整个集群,再慢慢导入流量。

我们在对外的web服务器上加一层“粗细管道”,就能很好的解决上面的问题。高并发下的数据库设计高并发下的数据库设计高并发下的数据库设计高并发下的数据库设计高并发下的数据库设计高并发下的数据库设计高并发下的数据库设计高并发下的数据库设计


おすすめ

転載: blog.51cto.com/14550123/2439018