满足亿级用户的高扩展Java解决方案

0分享至

用扫码二维码

分享至好友和朋友圈

前言

在大厂我们都是一个小小的螺丝钉,了解系统全貌的机会不多。

在创业公司,程序员独当一面,但业务量上不去,没人带领,视野有限,接触的技术有限,各种方案调研停留在纸面上,没经过实战考验。

互联网特点是用户规模大、需求变化快,系统的扩展性、稳定性、灵活性非常重要,本文将带你全面了解 Java 生态下在线服务全流程解决方案,如何搭建满足亿级用户需求并能水平扩展的系统。

综述

我们的系统用 Java 作为主开发语言,Spring Framework 为基础框架,关系型数据库 MySQL 做存储,Mybatis 为对象关系映射框架。

上图是最原始的系统架构,当数据库性能成为瓶颈时,需对系统升级。大部分情况下读请求远多于写,系统通过读写分离和 NoSQL 缓存读为主的数据等方式提升系统整体性能,系统架构演变成了下图。

随着用户规模的增长,业务越来越复杂,此时可以按功能拆分系统(如下图),如用户相关的信息放入用户中心、订单相关的操作进入订单平台等等。系统拆分成小的子系统,更易维护、迭代,此时不同系统间的调用需要远程过程调用 Remote Procedure Call(RPC),也引出了另一个概念微服务。

采用微服务架构后,随着业务量的增长,数据库再次成了系统瓶颈,此时要对数据水平扩展,可以做分库分表,有多种解决方案:

1. 业务层配置多个数据源,完成 SQL 的拼装;2. 在业务系统和数据库之间搭一个 proxy,由 proxy 完成分库分表;3. 分布式数据库。

交易类应用,如电商、金融等要求数据强一致行的应用更复杂些,这类应用的接口必须可重入,同时用分布式锁和乐观锁来保证数据一致性,还需通过离线对账、补偿对数据进行一致性的校验和处理。离线任务引出了跑批框架和调度的使用,需保证跑批是分布式抢占型的,不能重复跑不能单点。下图的技术架构基本保证了系统和数据两个层面的水平扩展,支撑亿级用户不成问题。

本文将围绕上面的内容展开介绍以下技术的基本概念和选型:

1. ioc、aop、orm 为基础框架,关系型数据库做存储;2. 读写分离,NoSQL 缓存读为主的数据;3. 微服务化;4. 分库分表;5. 分布式锁;6. 离线任务及调度;7. 分布式配置中心。

基础框架

在线服务,通俗点就是网站,大家肯定听过很多名词,如 Ruby on Rails、MVC、PHP、Python、Strusts、Spring、HTML、CSS、Hibernate 等等。

Server 容器

本文选型以 Java 生态为主,首先要选择一个开源的 Server 作为容器:

1. Tomcat:HTTP 服务器和 Java Servlet 容器,应用最广泛的,稳定成熟,其市场霸主地位仍然难以撼动;2. Jetty:HTTP 服务器和 Java Servlet 容器,按需加载组件,可以作为嵌入式服务器使用;3. Resin:HTTP 服务器和 Java Servlet 容器,报错十分简洁而明确,中文的支持比 Tomcat 好不少;4. JBoss:Java EE 服务器,不仅是 HTTP 服务器和 Java Servlet 容器,还包括 EJB 容器和 JMS 等其他 Java EE 所有功能,比较重,资源消耗也比较多;5. GlassFish:同 JBoss 一样,是一个 Java EE 服务器。

Tomcat 应用最广泛、最成熟、文档最多,是首选。

控制反转

控制反转 Inversion of Control(IOC),指依赖对象的过程被反转了,控制被反转后,获得依赖对象的过程由自身管理变为了由 IOC 容器主动注入。控制反转还叫“依赖注入 Dependency Injection(DI)”,指由 IOC 容器在运行期间,动态地将某种依赖关系注入到对象之中。

DI 和 IOC 是从不同角度描述同一件事,是指通过引入 IOC 容器,利用依赖关系注入的方式,实现对象之间的解耦。想更深入了解 IOC 可以读 Martin Fowler 的文章:Inversion of Control Containers and the Dependency Injection pattern。

IOC 容器产品有很多,如 Spring、Guice、JBoss Microcontainer、Pico Container、Yan、Annocon、Butterfly Container、HiveMind 等等。

IOC 容器产品很多,Spring 必须是首选。

面向切面编程

面向切面编程 Aspect Oriented Programming(AOP),动态地将代码切入到类的指定方法、指定位置上的编程思想。切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法叫切入点。有了 AOP,可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去改变其原有行为。

AspectJ 不是 Spring 的一部分,但谈到 Spring AOP 一般会提到 AspectJ。AspectJ 是一套独立的面向切面编程的解决方案。

Spring AOP 对目标类增强,生成代理类。与 AspectJ 的区别在于 Spring AOP 是运行时增强,而 AspectJ 是编译时增强。Spring AOP 使用了 AspectJ 的 Annotation,使用了 Aspect 来定义切面,使用 Pointcut 来定义切入点,使用 Advice 来定义增强处理。虽然使用了 Aspect 的 Annotation,但并没有使用它的编译器和织入器,实现原理是 JDK 动态代理,运行时生成代理类。

Spring AOP,一个轻量级框架,实现了 AOP 20% 的技术,可以支撑 80% 的需求,对 AspectJ 进行了集成,但内部仍使用 Spring AOP,所以只能使用 AspectJ 的部分功能。

Spring AOP 是首选。

对象关系映射

对象关系映射 Object Relational Mapping(ORM),是在关系型数据库和对象之间作一个映射。

常见的 ORM 框架有:Hibernate、iBATIS、TopLink、Castor JDO、Apache OJB 等。ORM 的实现原理,是实现 JavaBean 属性到数据库表字段的映射,任何 ORM 框架都是读某个配置文件把 JavaBean 属性和数据库表字段关联起来,当从数据库 Query 时,自动把字段值塞进 JavaBean 属性里,当做 INSERT 或 UPDATE 时,自动把 JavaBean 属性值绑定到 SQL 语句中。

不要纠结使用何种 ORM,实现一个最简单功能的增删改查时,做到修改数据结构而不修改任何代码,就初步实现了业务逻辑和数据访问的分离。

ORM 目的是将数据库和对象映射,Mybatis 够用了,更多复杂功能不能提高效率,还带来学习成本,SQL 做简单增删改查,其他的在内存中完成。

关系型数据库,就是 MySQL 了。

读写分离

互联网应用很多业务都是读多写少,这些读写业务直接反应到对数据库操作的读和写。MySQL 的部署都是一主多从,写操作只能在主库上,所以主库支持的请求数有限,为提升系统整体吞吐量,可以将读请求分流到从库上。

读写分离要求我们对业务有深入的理解,能分开哪些是纯读请求,哪些是写后读请求。

1. 纯读请求:数据库的主从同步有延时,大部分读请求对这个延迟不敏感,称为纯读请求;2. 写后读请求:指对主从同步延时非常敏感的这部分读请求。

注释驱动的 Spring cache,不是具体的缓存实现方案(如 JDK cache、EHCache、OSCache、Redis),是一个对缓存使用的抽象,通过在既有代码中添加少量的 annotation,减少编写常见缓存的代码量,即可达到使代码具备缓存的能力。

数据缓存一般放到内存或者 NoSQL 中,NoSQL 数据库是以 Redis、MongoDB 为代表的,Redis 应用相对更广泛,这里不展开对比了。

纯读请求缓存后,再设置一个失效时间,一般都不会有太大问题。但写后读请求的缓存,以及在事务中读请求的缓存,要慎重些,特别是事务中读请求的缓存,现在大部分缓存实现方案都支持的不好。

下图是在事务中读请求的缓存清除流程,其中的关键点是将缓存置为失效,而不是直接删除缓存。

Spring-data-redis 中有对 Redis 缓存的支持,但事务中的缓存 evict 和 put 操作只是在事务完成后进行缓存操作,对数据变更特别敏感的缓存接口需根据上面的流程改造 Redis 缓存。

纯读请求可加缓存,要设置失效时间,对于写后读请求,特别是事务中读,慎重使用缓存。

微服务化

随着业务的不断发展,应用变得越来越复杂,不同功能的开发相互影响,此时将一个大系统拆小,并完成服务化架构势在必行。系统拆分成小的子系统,更易维护、迭代,此时不同系统间的交互引出了另一个概念微服务。

1. 把单个应用分解为多个服务解决了复杂性问题。功能不变,应用分解为多个可管理的应用,每个应用都有一个接口定义清楚的边界。微服务提供模块化解决方案,单个更小的应用更易开发、理解和维护;2. 每个应用由专门团队维护,团队自由选择技术,提供服务。团队不需要被迫使用某项目开始时采用的过时技术,因为服务都相对简单,使用前沿技术重写以前代码也相对容易;3. 每个应用独立部署,不再需要协调其它服务部署对本服务的影响,加快部署速度;4. 每个应用独立扩展,可根据服务的规模来部署,也可以使用更适合于服务资源需求的硬件。如在 Compute Optimized instances 上部署 CPU 敏感的服务、在 Memory Optimized instances 上部署内存数据库。

微服务是一整套工具集,其中服务调用的主要流程如下图所示:

微服务的主要功能有服务的发现、注册、路由、熔断、降级、分布式配置。国内 Dubbo 应用非常广泛,Dubbo 于 2017 年开始重启维护。下图[1]展示了主要功能的关系——服务调用、注册发现。

[1]

微服务架构重点在于如何合理的利用微服务。Dubbo 的功能是 Spring Cloud 体系的一部分,Dubbo 诞生于 SOA 时代,主要关注服务的调用、流量分发、监控和熔断。而 Spring Cloud 是微服务架构时代的产物,包括微服务治理的方方面面,依托 Spring、Spring Boot 的优势,两个框架目标就不一致,Dubbo 定位服务治理、Spring Cloud 是一个生态。

Spring Cloud 是目前最好的微服务框架。

分库分表

分库分表重点是把一个数据库切分成多个部分放到不同库上,进而缓解单一数据库性能问题。

分库分表有垂直切分和水平两种方式:

1. 垂直切分:按业务拆分,跟微服务一起切分部署到不同库上;2. 水平切分:把一个表的数据按某种规则切分到多个数据库上。

水平切分比垂直切分稍微复杂一些,因为拆分规则更复杂,后期的数据维护也更复杂。本文讲的分库分表特指水平切分。

分库分表面临的问题:

1. 跨结点聚合2. 数据迁移3. 扩容4. 唯一 ID

分库分表有很多种方式:

1. 数据访问层:如通过 Spring 多数据源自主研发;2. ORM 框架层:如新增 Mybatis plugin;3. 应用服务器与数据库之间代理层:对业务透明,最复杂,也最灵活;

数据访问层分库分表最简单,我们用自主研发的 ORM 框架层分库分表。

分布式锁

互联网应用中电商和金融场景下,某些特定场景中同一时间下不允许同一用户进行多个相同或不同操作,这种情况下需要加锁,在单机环境中 Java 提供了很多并发处理相关的 API,这些 API 在分布式场景中无能为力,并不能提供分布式锁的能力。

分布式锁应具备以下条件:

1. 分布式系统中,同一方法同一时间内只能被一个机器的一个线程执行;2. 高可用的获取锁与释放锁;3. 高性能的获取锁与释放锁;4. 可重入;5. 锁失效机制,防止死锁;6. 非阻塞锁特性,即未获取到锁直接返回获取锁失败。

分布式锁主要有三种实现方式:

1. 基于数据库的分布式锁2. 基于缓存,如 Redis 等3. 基于 Zookeeper

基于数据库的分布式锁

基于数据库的实现大致可以分为两种:

1. 建一张表,有字段 METHOD_NAME(需要执行的方法名),此字段作为唯一索引,插入成功则获取锁成功,删掉记录释放锁;2. 通过 for update 操作,数据库在查询时就会给这条记录加上排它锁。

方法一需要解决的后续问题:

1. 失效时间:通过定时任务清理数据库中的超时数据。2. 阻塞性:循环直到插入成功返回成功。3. 可重入:在表中加字段,记录当前获得锁的机器主机和线程,下次获取锁时先查库,如果当前信息匹配则直接分配锁。

方法二的问题:

1. 加索引:InnoDB 引擎在加锁的时候,只有通过索引进行检索时才会使用行级锁,否则会使用表级锁;2. 阻塞锁:for update 语句会在执行成功后立即返回,在执行失败时一直处于阻塞状态,直到成功;3. 无法释放:使用这种方式,服务宕机后数据库会把锁释放掉;4. 无法解决可重入问题。

基于 NoSQL 的分布式锁

对比基于数据库的方案,基于 NoSQL 来实现性能更好一点,而且可以集群部署消除单点。基于 Redis 的分布式锁主要通过下面命令实现的:

基于 Redis 获取锁使用命令:SET methodname randomvalue NX PX 30000; NX:键不存在时,才对键操作; PX millisecond:设置键的过期时间为毫秒,超过这个时间后,键自动失效。

Redis 从 2.6.12 版本开始,SET 命令才支持这些参数。

这个命令在某个 key 不存在时,才执行成功。当多个进程同时并去设置同一个 key 时,只会有一个成功。释放锁只需删除这个 key,删除之前要判断 key 对应的 value 是之前设置的。失效时间设置多长为好:太短,方法没执行完,锁自动释放了就会产生并发问题。如果太长,其他获取锁的线程就要多等一段时间。

可以使用 REDISSON( Redis 官方分布式锁组件):每获得一个锁时,只设置一个很短的超时时间,同时起一个线程在每次快要到超时时间时去刷新锁的超时时间。在释放锁的同时结束这个线程。Redis 是集群部署的,客户端 A 加锁成功,在这个 key 被异步复制给其他节点过程中主节点挂了,数据没及时同步到其他节点导致 key 丢失。主备切换后,客户端B加锁成功,就导致了多个客户端对一个分布式锁加锁成功。

另外,主从的配置方式存在一定的安全风险,由于 Redis 的主从复制是异步进行的, 可能会发生多个客户端同时持有一个锁的现象。如果不能容忍偶尔发生竞态问题,则需要针对 Redis 集群模式的分布式锁,可以采用 Redis 的 Redlock 机制。

基于 zookeeper 的分布式锁

基于 zookeeper 临时有序节点实现分布式锁,可使用 zookeeper 第三方库 Curator 客户端,封装了一个可重入的锁服务。下面是基于 zk 分布式锁的大致思想:

1. 在 lock 节点下创建一个有序临时节点( EPHEMERAL_SEQUENTIAL );2. 判断创建的节点序号是否最小,是则锁成功,不是则失败,然后 watch 序号比本身小的前一个节点;3. 锁失败,等待 watch 事件,再次判断是否序号最小;4. 取锁成功执行代码,最后删除该节点释放锁。

由于网络抖动,客户端到 zk 集群的 session 连接断了,zk 以为客户端挂了会删除临时节点,此时其他客户端就能锁成功,并导致并发问题。这个问题不常见因为 zk 会重试,Curator 客户端支持多种重试策略,多次重试之后还不行才会删除临时节点。

基于数据库的方式最容易理解,基于 Redis 的性能最高,基于 zk 的最稳定,我们选的是基于 Redis 的实现。

离线任务

把定时任务通过集群的方式进行管理调度,并采用分布式部署,保证系统的高可用,提高了容错。那么如何保证定时任务只在集群的某一个节点上执行,或者一个任务如何拆分为多个独立的任务项,由分布式的机器去分别执行,众多的定时任务如何统一管理,现在有很多成熟的分布式定时任务框架,都能很好的实现上述的功能。

Quartz 是 Java 事实上的定时任务标准,但关注点在定时任务而非数据,并无一套根据数据处理而定制化的流程,可以基于数据库实现作业的高可用,缺少分布式并行执行作业的功能。

Elastic-Job 是无中心化的分布式定时调度框架,由两个相对独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成,在 Quartz 基于数据库的高可用方案基础上增加了弹性扩容和数据分片。

Spring Batch 提供了统一的读写接口、丰富的任务处理方式、灵活的事务管理及并发处理,还支持日志、监控、任务重启与跳过等特性,将开发人员从复杂的任务配置管理过程中解放出来,使他们更多地去关注核心的业务处理过程。

Elastic-Job 基本够用。

分布式配置中心

随着业务的发展,应用系统中的配置通越来越多,常见的配置大致有数据源、业务组件配置等,这类配置比较稳定较少变化,放在文件中随应用一起发布。实际中有某些配置信息变化有一定频率和规律,希望能够做到实时,如一些营销类、或活动类应用系统,若使用传统的配置文件,重新发布应用不方便,因此,才有了分布式配置中心,旨在更好地解决这类问题。

一个可靠的分布式配置中心,应该满足以下基本特性:

1. 高可用性:无单点,只要集群中还有存活的节点,就能提供服务;2. 容错性:在配置平台不可用时,也不影响客户端正常运行;3. 高性能:不能因为获取配置给应用带来不可接受的损失;4. 可靠的存储:包括数据的备份容灾,一致性等;5. 准实时生效:客户端应用能够及时感知配置的变更;6. 负载均衡:保证客户端的请求能均衡负载到各服务器节点;7. 扩展性:无感扩容,提升集群服务能力。

开源系统主要有:

1. spring-cloud-config:可以和 Spring Cloud 无缝配合;2. disconf:蚂蚁金服技术专家发起,业界使用广泛;3. ctrip apollo:Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,具备规范的权限、流程治理等特性。

ctrip applo是比较好的选择。

案例分析

系统起步阶段,不纠结系统选型,以 Java 生态为主搭建系统后台,server 服务器使用 Tomcat,Spring Framework 为基础框架,关系型数据库 MySQL 做存储,Mybatis 为对象关系映射框架。为了支持一些系统配置准实时生效,使用 ctrip applo 作为分布式配置中心。当数据库成为系统瓶颈后,通过 Spring 多数据源把读请求分流到从库,使用 Spring-data-redis 缓存读为主的数据提升系统整体性能。随着用户规模的增长,业务越来越复杂,引入微服务架构,按功能拆分系统,如用户相关的信息放入用户中心、订单相关的操作进入订单平台等等。使用 Spring Cloud 搭建微服务架构,使系统变小、解耦。采用 Spring Cloud 后,随着业务量的增长,数据库再次成了系统瓶颈,此时要对数据水平扩展,使用 Spring 的 AbstractRoutingDataSource 多数据源做分库分表。交易类应用,使用乐观锁和基于 Redis 的 REDISSON 分布式锁来保证数据一致性,使用 Elastic-Job 开发离线对账、补偿对数据进行一致性的校验和处理。

总结

本文介绍的是 Java 生态下系统解决方案,互联网的特点是用户规模大、需求变化快,所以系统的扩展性、稳定性、灵活性就非常重要。对于互联网应用最大的特点就是分布式,保证系统能水平扩展,微服务是最好的选择,将大应用拆小,有利于维护。对于数据,也是一样,主要解决方案是业务库表拆分、加 cache、读写分离、分库分表。保证系统和数据的水平扩展后,对于一部分电商和金融类应用要求数据强一致性,可以通过分布式锁、乐观锁、离线对账、实时补偿来完成。

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

/阅读下一篇/

返回网易首页 下载网易新闻客户端