【干货】乐视秒杀:每秒十万笔交易的数据架构解读

本贴最后更新于 2905 天前,其中的信息可能已经事过景迁

随着乐视硬件抢购的不断升级,乐视集团支付面临的请求压力百倍乃至千倍的暴增。作为商品购买的最后一环,保证用户快速稳定地完成支付尤为重要。所以在 2015 年 11 月,我们对整个支付系统进行了全面的架构升级,使之具备了每秒稳定处理 10 万订单的能力。为乐视生态各种形式的抢购秒杀活动提供了强有力的支撑。

一. 分库分表

在 redis,memcached 等缓存系统盛行的互联网时代,构建一个支撑每秒十万只读的系统并不复杂,无非是通过一致性哈希扩展缓存节点,水平扩展 web 服务器等。支付系统要处理每秒十万笔订单,需要的是每秒数十万的数据库更新操作(insert 加 update),这在任何一个独立数据库上都是不可能完成的任务,所以我们首先要做的是对订单表(简称 order)进行分库与分表。
在进行数据库操作时,一般都会有用户 ID(简称 uid)字段,所以我们选择以 uid 进行分库分表。
分库策略我们选择了“二叉树分库”,所谓“二叉树分库”指的是:我们在进行数据库扩容时,都是以 2 的倍数进行扩容。比如:1 台扩容到 2 台,2 台扩容到 4 台,4 台扩容到 8 台,以此类推。这种分库方式的好处是,我们在进行扩容时,只需 DBA 进行表级的数据同步,而不需要自己写脚本进行行级数据同步。
光是有分库是不够的,经过持续压力测试我们发现,在同一数据库中,对多个表进行并发更新的效率要远远大于对一个表进行并发更新,所以我们在每个分库中都将 order 表拆分成 10 份:order_0,order_1,....,order_9。
最后我们把 order 表放在了 8 个分库中(编号 1 到 8,分别对应 DB1 到 DB8),每个分库中 10 个分表(编号 0 到 9,分别对应 order_0 到 order_9),部署结构如下图所示:

1462851593368

根据 uid 计算数据库编号:
数据库编号 = (uid / 10) % 8 + 1
根据 uid 计算表编号:
表编号 = uid % 10
当 uid=9527 时,根据上面的算法,其实是把 uid 分成了两部分 952 和 7,其中 952 模 8 加 1 等于 1 为数据库编号,而 7 则为表编号。所以 uid=9527 的订单信息需要去 DB1 库中的 order_7 表查找。具体算法流程也可参见下图:

1462851617019

有了分库分表的结构与算法最后就是寻找分库分表的实现工具,目前市面上约有两种类型的分库分表工具:
1.客户端分库分表,在客户端完成分库分表操作,直连数据库
2.使用分库分表中间件,客户端连分库分表中间件,由中间件完成分
库分表操作
这两种类型的工具市面上都有,这里不一一列举,总的来看这两类工具各有利弊。客户端分库分表由于直连数据库,所以性能比使用分库分表中间件高 15% 到 20%。而使用分库分表中间件由于进行了统一的中间件管理,将分库分表操作和客户端隔离,模块划分更加清晰,便于 DBA 进行统一管理。
我们选择的是在客户端分库分表,因为我们自己开发并开源了一套数据层访问框架,它的代号叫“芒果”,芒果框架原生支持分库分表功能,并且配置起来非常简单。
芒果主页:mango.jfaster.org
芒果源码:github.com/jfaster/mango

二. 订单 ID

订单系统的 ID 必须具有全局唯一的特征,最简单的方式是利用数据库的序列,每操作一次就能获得一个全局唯一的自增 ID,如果要支持每秒处理 10 万订单,那每秒将至少需要生成 10 万个订单 ID,通过数据库生成自增 ID 显然无法完成上述要求。所以我们只能通过内存计算获得全局唯一的订单 ID。
JAVA 领域最著名的唯一 ID 应该算是 UUID 了,不过 UUID 太长而且包含字母,不适合作为订单 ID。通过反复比较与筛选,我们借鉴了 Twitter 的 Snowflake 算法,实现了全局唯一 ID。下面是订单 ID 的简化结构图:

1462851628576

上图分为 3 个部分:
1 时间戳
这里时间戳的粒度是毫秒级,生成订单 ID 时,使用 System.currentTimeMillis 作为时间戳
2 机器号
每个订单服务器都将被分配一个唯一的编号,生成订单 ID 时,直接使用该唯一编号作为机器号即可。
3 自增序号
当在同一服务器的同一毫秒中有多个生成订单 ID 的请求时,会在当前毫秒下自增此序号,下一个毫秒此序号继续从 0 开始。比如在同一服务器同一毫秒有 3 个生成订单 ID 的请求,这 3 个订单 ID 的自增序号部分将分别是 0,1,2。
上面 3 个部分组合,我们就能快速生成全局唯一的订单 ID。不过光全局唯一还不够,很多时候我们会只根据订单 ID 直接查询订单信息,这时由于没有 uid,我们不知道去哪个分库的分表中查询,遍历所有的库的所有表?这显然不行。所以我们需要将分库分表的信息添加到订单 ID 上,下面是带分库分表信息的订单 ID 简化结构图:

1462851653506

我们在生成的全局订单 ID 头部添加了分库与分表的信息,这样只根据订单 ID,我们也能快速的查询到对应的订单信息。
分库分表信息具体包含哪些内容?第一部分有讨论到,我们将订单表按 uid 维度拆分成了 8 个数据库,每个数据库 10 张表,最简单的分库分表信息只需一个长度为 2 的字符串即可存储,第 1 位存数据库编号,取值范围 1 到 8,第 2 位存表编号,取值范围 0 到 9。
还是按照第一部分根据 uid 计算数据库编号和表编号的算法,当 uid=9527 时,分库信息 =1,分表信息 =7,将他们进行组合,两位的分库分表信息即为"17"。具体算法流程参见下图:

1462851669654

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

1462851686408

如上图所示,在计算分库信息的时候采用了模 64 的方式冗余了分库信息精度,这样当我们的系统以后需要扩容到 16 库,32 库,64 库都不会再有问题。
上面的订单 ID 结构已经能很好的满足我们当前与之后的扩容需求,但考虑到业务的不确定性,我们在订单 ID 的最前方加了 1 位用于标识订单 ID 的版本,这个版本号属于冗余数据,目前并没有用到。下面是最终订单 ID 简化结构图:

1462851699663

Snowflake 算法:github.com/twitter/snowflake

三. 最终一致性

到目前为止,我们通过对 order 表 uid 维度的分库分表,实现了 order 表的超高并发写入与更新,并能通过 uid 和订单 ID 查询订单信息。但作为一个开放的集团支付系统,我们还需要通过业务线 ID(又称商户 ID,简称 bid)来查询订单信息,所以我们引入了 bid 维度的 order 表集群,将 uid 维度的 order 表集群冗余一份到 bid 维度的 order 表集群中,要根据 bid 查询订单信息时,只需查 bid 维度的 order 表集群即可。
上面的方案虽然简单,但保持两个 order 表集群的数据一致性是一件很麻烦的事情。两个表集群显然是在不同的数据库集群中,如果在写入与更新中引入强一致性的分布式事务,这无疑会大大降低系统效率,增长服务响应时间,这是我们所不能接受的,所以我们引入了消息队列进行异步数据同步,来实现数据的最终一致性。当然消息队列的各种异常也会造成数据不一致,所以我们又引入了实时监控服务,实时计算两个集群的数据差异,并进行一致性同步。
下面是简化的一致性同步图:

1462851711668

四. 数据库高可用

没有任何机器或服务能保证在线上稳定运行不出故障。比如某一时间,某一数据库主库宕机,这时我们将不能对该库进行读写操作,线上服务将受到影响。
所谓数据库高可用指的是:当数据库由于各种原因出现问题时,能实时或快速的恢复数据库服务并修补数据,从整个集群的角度看,就像没有出任何问题一样。需要注意的是,这里的恢复数据库服务并不一定是指修复原有数据库,也包括将服务切换到另外备用的数据库。
数据库高可用的主要工作是数据库恢复与数据修补,一般我们以完成这两项工作的时间长短,作为衡量高可用好坏的标准。这里有一个恶性循环的问题,数据库恢复的时间越长,不一致数据越多,数据修补的时间就会越长,整体修复的时间就会变得更长。所以数据库的快速恢复成了数据库高可用的重中之重,试想一下如果我们能在数据库出故障的 1 秒之内完成数据库恢复,修复不一致的数据和成本也会大大降低。
下图是一个最经典的主从结构:

1462851726511

上图中有 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 实现统一恢复,不需要项目组操作服务,这样便于做到自动化,缩短服务恢复时间。
先来看从库高可用结构图:

1462851736677

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

1462851744785

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

1462851752590

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

五. 数据分级
支付系统除了最核心的支付订单表与支付流水表外,还有一些配置信息表和一些用户相关信息表。如果所有的读操作都在数据库上完成,系统性能将大打折扣,所以我们引入了数据分级机制。
我们简单的将支付系统的数据划分成了 3 级:
第 1 级:订单数据和支付流水数据;这两块数据对实时性和精确性要求很高,所以不添加任何缓存,读写操作将直接操作数据库。
第 2 级:用户相关数据;这些数据和用户相关,具有读多写少的特征,所以我们使用 redis 进行缓存。
第 3 级:支付配置信息;这些数据和用户无关,具有数据量小,频繁读,几乎不修改的特征,所以我们使用本地内存进行缓存。
使用本地内存缓存有一个数据同步问题,因为配置信息缓存在内存中,而本地内存无法感知到配置信息在数据库的修改,这样会造成数据库中数据和本地内存中数据不一致的问题。
为了解决此问题,我们开发了一个高可用的消息推送平台,当配置信息被修改时,我们可以使用推送平台,给支付系统所有的服务器推送配置文件更新消息,服务器收到消息会自动更新配置信息,并给出成功反馈。

六. 粗细管道
黑客攻击,前端重试等一些原因会造成请求量的暴涨,如果我们的服务被激增的请求给一波打死,想要重新恢复,就是一件非常痛苦和繁琐的过程。
举个简单的例子,我们目前订单的处理能力是平均 10 万下单每秒,峰值 14 万下单每秒,如果同一秒钟有 100 万个下单请求进入支付系统,毫无疑问我们的整个支付系统就会崩溃,后续源源不断的请求会让我们的服务集群根本启动不起来,唯一的办法只能是切断所有流量,重启整个集群,再慢慢导入流量。
我们在对外的 web 服务器上加一层“粗细管道”,就能很好的解决上面的问题。
下面是粗细管道简单的结构图:

1462851761961

请看上面的结构图,http 请求在进入 web 集群前,会先经过一层粗细管道。入口端是粗口,我们设置最大能支持 100 万请求每秒,多余的请求会被直接抛弃掉。出口端是细口,我们设置给 web 集群 10 万请求每秒。剩余的 90 万请求会在粗细管道中排队,等待 web 集群处理完老的请求后,才会有新的请求从管道中出来,给 web 集群处理。这样 web 集群处理的请求数每秒永远不会超过 10 万,在这个负载下,集群中的各个服务都会高校运转,整个集群也不会因为暴增的请求而停止服务。
如何实现粗细管道?nginx 商业版中已经有了支持,相关资料请搜索 nginx max_conns,需要注意的是 max_conns 是活跃连接数,具体设置除了需要确定最大 TPS 外,还需确定平均响应时间。
nginx 相关:
http://nginx.org/en/docs/http/ngx_http_upstream_module.html

作者:头条号 / DBAplus 社群
链接:http://toutiao.com/i6282460032487391746/
来源:头条号(今日头条旗下创作平台)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    140 引用 • 441 回帖
  • 乐视
    1 引用 • 8 回帖
  • 数据库

    据说 99% 的性能瓶颈都在数据库。

    330 引用 • 614 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
请输入回帖内容 ...

推荐标签 标签

  • ZeroNet

    ZeroNet 是一个基于比特币加密技术和 BT 网络技术的去中心化的、开放开源的网络和交流系统。

    1 引用 • 21 回帖 • 591 关注
  • C

    C 语言是一门通用计算机编程语言,应用广泛。C 语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。

    83 引用 • 165 回帖 • 46 关注
  • Gitea

    Gitea 是一个开源社区驱动的轻量级代码托管解决方案,后端采用 Go 编写,采用 MIT 许可证。

    4 引用 • 16 回帖 • 1 关注
  • 锤子科技

    锤子科技(Smartisan)成立于 2012 年 5 月,是一家制造移动互联网终端设备的公司,公司的使命是用完美主义的工匠精神,打造用户体验一流的数码消费类产品(智能手机为主),改善人们的生活质量。

    4 引用 • 31 回帖 • 10 关注
  • OpenShift

    红帽提供的 PaaS 云,支持多种编程语言,为开发人员提供了更为灵活的框架、存储选择。

    14 引用 • 20 回帖 • 602 关注
  • PWL

    组织简介

    用爱发电 (Programming With Love) 是一个以开源精神为核心的民间开源爱好者技术组织,“用爱发电”象征开源与贡献精神,加入组织,代表你将遵守组织的“个人开源爱好者”的各项条款。申请加入:用爱发电组织邀请帖
    用爱发电组织官网:https://programmingwithlove.stackoverflow.wiki/

    用爱发电组织的核心驱动力:

    • 遵守开源守则,体现开源&贡献精神:以分享为目的,拒绝非法牟利。
    • 自我保护:使用适当的 License 保护自己的原创作品。
    • 尊重他人:不以各种理由、各种漏洞进行未经允许的抄袭、散播、洩露;以礼相待,尊重所有对社区做出贡献的开发者;通过他人的分享习得知识,要留下足迹,表示感谢。
    • 热爱编程、热爱学习:加入组织,热爱编程是首当其要的。我们欢迎热爱讨论、分享、提问的朋友,也同样欢迎默默成就的朋友。
    • 倾听:正确并恳切对待、处理问题与建议,及时修复开源项目的 Bug ,及时与反馈者沟通。不抬杠、不无视、不辱骂。
    • 平视:不诋毁、轻视、嘲讽其他开发者,主动提出建议、施以帮助,以和谐为本。只要他人肯努力,你也可能会被昔日小看的人所超越,所以请保持谦虚。
    • 乐观且活跃:你的努力决定了你的高度。不要放弃,多年后回头俯瞰,才会发现自己已经成就往日所仰望的水平。积极地将项目开源,帮助他人学习、改进,自己也会获得相应的提升、成就与成就感。
    1 引用 • 487 回帖 • 8 关注
  • GitHub

    GitHub 于 2008 年上线,目前,除了 Git 代码仓库托管及基本的 Web 管理界面以外,还提供了订阅、讨论组、文本渲染、在线文件编辑器、协作图谱(报表)、代码片段分享(Gist)等功能。正因为这些功能所提供的便利,又经过长期的积累,GitHub 的用户活跃度很高,在开源世界里享有深远的声望,并形成了社交化编程文化(Social Coding)。

    207 引用 • 2031 回帖
  • ReactiveX

    ReactiveX 是一个专注于异步编程与控制可观察数据(或者事件)流的 API。它组合了观察者模式,迭代器模式和函数式编程的优秀思想。

    1 引用 • 2 回帖 • 126 关注
  • HTML

    HTML5 是 HTML 下一个的主要修订版本,现在仍处于发展阶段。广义论及 HTML5 时,实际指的是包括 HTML、CSS 和 JavaScript 在内的一套技术组合。

    103 引用 • 294 回帖
  • WebSocket

    WebSocket 是 HTML5 中定义的一种新协议,它实现了浏览器与服务器之间的全双工通信(full-duplex)。

    48 引用 • 206 回帖 • 400 关注
  • GitLab

    GitLab 是利用 Ruby 一个开源的版本管理系统,实现一个自托管的 Git 项目仓库,可通过 Web 界面操作公开或私有项目。

    46 引用 • 72 回帖
  • SQLite

    SQLite 是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 是全世界使用最为广泛的数据库引擎。

    4 引用 • 7 回帖 • 3 关注
  • 工具

    子曰:“工欲善其事,必先利其器。”

    273 引用 • 679 回帖
  • jQuery

    jQuery 是一套跨浏览器的 JavaScript 库,强化 HTML 与 JavaScript 之间的操作。由 John Resig 在 2006 年 1 月的 BarCamp NYC 上释出第一个版本。全球约有 28% 的网站使用 jQuery,是非常受欢迎的 JavaScript 库。

    63 引用 • 134 回帖 • 744 关注
  • Git

    Git 是 Linux Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

    205 引用 • 357 回帖
  • 智能合约

    智能合约(Smart contract)是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易,这些交易可追踪且不可逆转。智能合约概念于 1994 年由 Nick Szabo 首次提出。

    1 引用 • 11 回帖 • 10 关注
  • Sublime

    Sublime Text 是一款可以用来写代码、写文章的文本编辑器。支持代码高亮、自动完成,还支持通过插件进行扩展。

    10 引用 • 5 回帖
  • OAuth

    OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。oAuth 是 Open Authorization 的简写。

    36 引用 • 103 回帖 • 9 关注
  • Wide

    Wide 是一款基于 Web 的 Go 语言 IDE。通过浏览器就可以进行 Go 开发,并有代码自动完成、查看表达式、编译反馈、Lint、实时结果输出等功能。

    欢迎访问我们运维的实例: https://wide.b3log.org

    30 引用 • 218 回帖 • 605 关注
  • Jenkins

    Jenkins 是一套开源的持续集成工具。它提供了非常丰富的插件,让构建、部署、自动化集成项目变得简单易用。

    51 引用 • 37 回帖
  • uTools

    uTools 是一个极简、插件化、跨平台的现代桌面软件。通过自由选配丰富的插件,打造你得心应手的工具集合。

    5 引用 • 13 回帖
  • Ubuntu

    Ubuntu(友帮拓、优般图、乌班图)是一个以桌面应用为主的 Linux 操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu”一词,意思是“人性”、“我的存在是因为大家的存在”,是非洲传统的一种价值观,类似华人社会的“仁爱”思想。Ubuntu 的目标在于为一般用户提供一个最新的、同时又相当稳定的主要由自由软件构建而成的操作系统。

    123 引用 • 168 回帖
  • 游戏

    沉迷游戏伤身,强撸灰飞烟灭。

    169 引用 • 799 回帖 • 1 关注
  • GAE

    Google App Engine(GAE)是 Google 管理的数据中心中用于 WEB 应用程序的开发和托管的平台。2008 年 4 月 发布第一个测试版本。目前支持 Python、Java 和 Go 开发部署。全球已有数十万的开发者在其上开发了众多的应用。

    14 引用 • 42 回帖 • 683 关注
  • Electron

    Electron 基于 Chromium 和 Node.js,让你可以使用 HTML、CSS 和 JavaScript 构建应用。它是一个由 GitHub 及众多贡献者组成的活跃社区共同维护的开源项目,兼容 Mac、Windows 和 Linux,它构建的应用可在这三个操作系统上面运行。

    15 引用 • 136 回帖 • 8 关注
  • 资讯

    资讯是用户因为及时地获得它并利用它而能够在相对短的时间内给自己带来价值的信息,资讯有时效性和地域性。

    53 引用 • 85 回帖
  • Latke

    Latke 是一款以 JSON 为主的 Java Web 框架。

    70 引用 • 532 回帖 • 712 关注