Database Design under high concurrency

Database Learning: high concurrent database design

With music as the hardware snapping escalating, music, as the Group paid a hundred times Request pressure faced by the explosion and even thousands of times. As the last link in the purchase of goods, to ensure that users quickly complete the payment stability is particularly important. So in November, 2003, we conducted a comprehensive of the entire payment system infrastructure upgrades, so that it has the ability to consistently handle 100,000 orders per second. It provides strong support for the various forms of ecological buying music as the spike activity.

A library points table

In Redis Internet age, memcached caching system such as prevailed, build a support one hundred thousand per second read-only system is not complicated, is that consistent hashing extended by caching nodes, horizontal scaling web servers. Payment systems to process one hundred thousand orders per second pen, need hundreds of thousands per second database update operations (insert plus update), this is an impossible task on any independent database, so we need to do first is the order form (referred to as the order) is divided and sub-table library.

During operation of the database, usually with a user ID (referred uid) field, so we choose to divide uid library sub-table.

Sub-library strategy we chose the "binary tree sub-libraries," so-called "binary sub-library" means: during our database expansion, are based on a multiple of 2 for expansion. For example: 1 to 2 expansion units, expansion units 2 to 4, 4 to 8 expansion units, and so on. The benefits of this sub-library approach is that, during our expansion, only table-level DBA data synchronization without the need to write scripts for row-level data synchronization.

Just watched the library is not enough, after sustained pressure testing , we found that in the same database, the efficiency of concurrent update multiple tables is much larger than concurrent updates to a table, so we are in each sub-library table order split into 10 parts: order_0, order_1, ..., order_9 ..

Finally, we placed the order table in the eight sub-library (numbered 1-8, respectively DB1 to DB8), each sub-library 10 part tables (numbered 0 to 9, corresponding to order_0 order_9), deployment structure As shown below:

image description

Uid is calculated according to the database Number:

Database Number = (uid / 10)% 8 + 1

The table number is calculated uid:

Table ID = uid% 10

When uid = 9527 when, according to the above algorithm , in fact, is the uid into two parts 952 and 7, wherein the die 952 is equal to 1 to 8 plus a number database, and 7 or the table number. So uid = 9527 order information needs to DB1 library order_7 table lookup. DETAILED see also the algorithm flow diagram:

image description

Structures and Algorithms With sub-library sub-table is to find the last sub-library sub-table implementation tools currently on the market about two types of sub-library sub-table tools:

  1. The client library sub-sub-table , the sub-library on the client side sub-table operation, direct database

  2. Use sub-library intermediate sub-table , the sub-library client even intermediate sub-table, completed by the middleware sub-library operations sub-table

Both types of tools are available in the market, not enumerated here, Overall these two types of tools have advantages and disadvantages. The client library sub-sub-table due to the direct connection database, the performance of sub-library sub-table than middleware 15% to 20% higher. The use of sub-library sub-table middleware middleware since a unified management, the sub-library operations sub-table and client isolation, module division clearer, DBA facilitate unified management.

We chose the client sub-library sub-table, because we own development and open source set of access framework data layers, it's code-named "Mango" Mango framework native support for sub-library sub-table functionality, and configuration is very simple.

  • Mango Home: mango.jfaster.org

  • Mango Source: github.com/jfaster/mango

Second, the order ID (uid dimension)

ID order system must have a globally unique feature, the easiest way is to use a database of sequences, each operating time will be able to get a globally unique ID increment, if you want to support processing 100,000 orders per second, per second, at least that need to generate 100,000 orders for ID, increment generated by the database ID is clearly unable to complete the above requirements. So we can only get through the globally unique order ID memory computing.

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

image description

上图分为3个部分:

  1. 时间戳

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

  1. 机器号

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

  1. 自增序号

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

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

image description

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

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

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

image description

上述使用表编号作为分表信息没有任何问题,但使用数据库编号作为分库信息却存在隐患,考虑未来的扩容需求,我们需要将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。具体算法流程也可参见下图:

image description

如上图所示,在计算分库信息的时候采用了模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简化结构图:

image description

Snowflake算法:github.com/twitter/snowflake

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

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

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

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

image description

四、数据库高可用

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

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

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

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

image description

上图中有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实现统一恢复,不需要项目组操作服务,这样便于做到自动化,缩短服务恢复时间。

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

image description

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

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

image description

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

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

image description

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

五、数据分级

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

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

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

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

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

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

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

六、粗细管道

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

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

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


Guess you like

Origin blog.51cto.com/14550123/2439018