看透Spring MVC:源代码分析与实践
上QQ阅读APP看书,第一时间看更新

1.4 海量数据的解决方案

现在无论是企业的业务系统还是互联网上的网站程序都面临着数据量大的问题,这个问题如果解决不好将严重影响系统的运行速度,下面就针对这个问题的各种解决方案进行系统介绍。

1.4.1 缓存和页面静态化

数据量大这个问题最直接的解决方案就是使用缓存,缓存就是将从数据库中获取的结果暂时保存起来,在下次使用的时候无需重新到数据库中获取,这样可以大大降低数据库的压力。

缓存的使用方式可以分为通过程序直接保存到内存中和使用缓存框架两种方式。程序直接操作主要是使用Map,尤其是ConcurrentHashMap,而常用的缓存框架有Ehcache、Memcache和Redis等。缓存使用过程中最重要问题是什么时候创建缓存和缓存的失效机制。缓存可以在第一次获取的时候创建也可以在程序启动和缓存失效之后立即创建,缓存的失效可以定期失效,也可以在数据发生变化的时候失效,如果按数据发生变化让缓存失效,还可以分粗粒度失效和细粒度失效。

多知道点

缓存中空数据的管理方法

如果缓存是在第一次获取的时候创建的,那么在使用缓存的时候最好将没有数据的缓存使用特定的类型值来保存,因为这种方式下如果从缓存中获取不到数据就会从数据库中获取,如果数据库中本来就没有相应的数据就不会创建缓存,这样将每次都会查询数据库。比如有个专门保存文章评论的缓存,不同的评论按照不同文章的Id来保存,如果有一篇文章本来就没有评论,那么就没有相应的缓存或者缓存的值为null,这样程序在每次调用这篇文章的评论时都会查询数据库。这就没起到缓存的作用,我们可以创建一个专门的类(如NoComment)来保存没有评论的缓存,这样程序从缓存中查询后就可以知道是还没有创建缓存还是本来就没有评论内容。

不过缓存也不是什么情况都适用,它主要用于数据变化不是很频繁的情况。而且如果是定期失效(数据修改时不失效)的失效机制,实时性要求也不能太高,因为这样缓存中的数据和真实数据可能会不一致。如果是文章的评论则关系不是很大,但如果是企业业务系统中要生成报表的数据则问题就大了。

跟缓存相似的另外一种技术叫页面静态化,它在原理上跟缓存非常相似,缓存是将从数据库中获取到的数据(当然也可以是别的任何可以序列化的东西)保存起来,而页面静态化是将程序最后生成的页面保存起来,使用页面静态化后就不需要每次调用都重新生成页面了,这样不但不需要查询数据库,而且连应用程序处理都省了,所以页面静态化同时对数据量大和并发量高两大问题都有好处。

页面静态化可以在程序中使用模板技术生成,如常用的Freemarker和Velocity都可以根据模板生成静态页面,另外也可以使用缓存服务器在应用服务器的上一层缓存生成的页面,如可以使用Squid,另外Nginx也提供了相应的功能。

1.4.2 数据库优化

要解决数据量大的问题,是避不开数据库优化的。数据库优化可以在不增加硬件的情况下提高处理效率,这是一种用技术换金钱的方式。数据库优化的方法非常多,常用的有表结构优化、SQL语句优化、分区和分表、索引优化、使用存储过程代替直接操作等,另外有时候合理使用冗余也能获得非常好的效果。

表结构优化

表结构优化是数据库中最基础也是最重要的,如果表结构优化得不合理,就可能导致严重的性能问题,具体怎么设计更合理也没有固定不变的准则,需要根据实际情况具体处理。

SQL语句优化

SQL语句优化也是非常重要的,基础的SQL优化是语法层面的优化,不过更重要的是处理逻辑的优化,这也需要根据实际情况具体处理,而且要和索引缓存等配合使用。不过SQL优化有一个通用的做法就是,首先要将涉及大数据的业务的SQL语句执行时间详细记录下来,其次通过仔细分析日志(同一条语句对不同条件的执行时间也可能不同,这点也需要仔细分析)找出需要优化的语句和其中的问题,然后再有的放矢地优化,而不是不分重点对每条语句都花同样的时间和精力优化。

分区

当数据量变多的时候,如果可以分区或者分表,那将起到非常好的效果。当一张表中的数据量变多的时候操作速度就慢了,所以很容易想到的就是将数据分到多个表中保存,但是这么做之后操作起来比较麻烦,想操作(增删改查)一个数据还需要先找到对应的表,如果涉及多个表还得跨表操作。其实在常用的数据库中可以不分表而达到跟分表类似的效果,那就是分区。分区就是将一张表中的数据按照一定的规则分到不同的区来保存,这样在查询数据时如果数据的范围在同一个区内那么可以只对一个区的数据进行操作,这样操作的数据量更少,速度更快,而且这种方法对程序是透明的,程序不需要做任何改动。

分表

如果一张表中的数据可以分为几种固定不变的类型,而且如果同时对多种类型共同操作的情况不多,那么都可以通过分表来处理,这也需要具体情况具体对待。笔者之前对一个业务系统进行重构开发时就将其中保存工人工作卡片的数据表分成了三个表,并且对每个表进行分区,在同时使用缓存(主要用于在保存和修改时对其他表的数据获取中,如根据工人Id获取工人姓名、工人类别、所在单位、所在工段及班组等信息)、索引、SQL优化等的情况下操作速度比原来提高了100倍以上。那时的分表是按照工作卡片的类型来划分的,因为当时的要求是要保留所有的记录。比如,修改了卡片的信息,则需要保存是谁在什么时候对卡片进行修改,修改前的数据是什么,添加删除也一样,这种需求一般的做法就是用一个字段来做卡片状态的标志位,将卡片分成不同的类型。不过这里由于数据量非常大所以就将卡片分别保存到了三个表中,第一个表保存正常卡片,第二个表保存删除后的卡片,第三个表保存修改之前的卡片,并且对每个表都进行了分区。由于报表一般是按月份、季度、半年和年来做的,所以分区是按月份来分的,每个月一个分区,这样问题就解决了。当然随着时间的推移,如果总数据量达到一定程度,还需要进一步处理。

另外一种分表的方法是将一个表中不同类型的字段分到不同的表中保存,这么做最直接的好处就是增删改数据的时候锁定的范围减小了,没被锁定的表中的数据不受影响。如果一个表的操作频率很高,在增删改其中一部分字段数据的同时另一部分字段也可能被操作,而且(主要指查询)用不到被增删改的字段,那么就可以把不同类型的字段分别保存到不同的表中,这样可以减少操作时锁定数据的范围。不过这样分表之后,如果需要查询完整的数据就得使用多表操作了。

索引优化

索引的大致原理是在数据发生变化(增删改)的时候就预先按指定字段的顺序排列后保存到一个类似表的结构中,这样在查找索引字段为条件的记录时就可以很快地从索引中找到对应记录的指针并从表中获取到记录,这样速度就快多了。不过索引也是一把双刃剑,它在提高查询速度的同时也降低了增删改的速度,因为每次数据的变化都需要更新相应的索引。不过合理使用索引对提升查询速度的效果非常明显,所以对哪些字段使用索引、使用什么类型的索引都需要仔细琢磨,并且最好再做一些测试。

使用存储过程代替直接操作

在操作过程复杂而且调用频率高的业务中,可以通过使用存储过程代替直接操作来提高效率,因为存储过程只需要编译一次,而且可以在一个存储过程里面做一些复杂的操作。

上面这些就是经常用到的数据库优化的方法,实际环境中怎么优化还得具体情况具体分析。除了这些优化方法,更重要的是业务逻辑的优化。

1.4.3 分离活跃数据

虽然有些数据总数据量非常大,但是活跃数据并不多,这种情况就可以将活跃数据单独保存起来从而提高处理效率。比如,对网站来说,用户很多时候就是这种数据,注册用户很多,但是活跃用户却不多,而不活跃的用户中有的偶尔也会登录网站,因此还不能删除。这时就可以通过一个定期处理的任务将不活跃的用户转移到别的数据表中,在主要操作的数据表中只保存活跃用户,查询时先从默认表中查找,如果找不到再从不活跃用户表中查找,这样就可以提高查询的效率。判断活跃用户可以通过最近登录时间,也可以通过指定时间段内登录次数。除了用户外还有很多这种类型的数据,如一个网站上的文章(特别是新闻类的)、企业业务系统中按时间记录的数据等。

1.4.4 批量读取和延迟修改

批量读取和延迟修改的原理是通过减少操作的次数来提高效率,如果使用得恰当,效率将会呈数量级提升。

批量读取是将多次查询合并到一次中进行,比如,在一个业务系统中需要批量导入工人信息,在导入前需要检查工人的编码是否已经在数据库中、工人对应的部门信息是否正确(在部门表中是否存在)、工人的工种信息在工种表中是否存在等,如果每保存一条记录都查询一次数据库,那么对每个需要检查的字段,都需要查询与要保存的记录条数相同次数的数据库,这时可以先将所有要保存的数据的相应字段读取到一个变量中,然后使用in语句统一查询一次数据库,这样就可以将n(要保存记录的条数)次查询变为一次查询了。除了这种对同一个请求中的数据批量读取,在高并发的情况下还可以将多个请求的查询合并到一次进行,如将3秒或5秒内的所有请求合并到一起统一查询一次数据库,这样就可以有效减少查询数据库的次数,这种类型可以用异步请求来处理。

延迟修改主要针对高并发而且频繁修改(包括新增)的数据,如一些统计数据。这种情况可以先将需要修改的数据暂时保存到缓存中,然后定时将缓存中的数据保存到数据库中,程序在读取数据时可以同时读取数据库中和缓存中的数据。这里的缓存和前面介绍的缓存有本质的区别,前面的缓存在使用过程中,数据库中的数据一直是最完整的,但这里数据库中的数据会有一段时间不完整。这种方式下如果保存缓存的机器出现了问题将可能会丢失数据,所以如果是重要的数据就需要做一些特殊处理。笔者之前所在的单位有一个系统需要每月月末各厂分别导入自己厂当月的相应数据,每到月末那个系统就处于基本瘫痪的状态了,而且各厂从整理出数据到导入系统只有几天的时间,所以有的厂就专门等晚上人少的时候才进行操作,对于这种情况就可采用延迟修改的策略来解决。

1.4.5 读写分离

读写分离的本质是对数据库进行集群,这样就可以在高并发的情况下将数据库的操作分配到多个数据库服务器去处理从而降低单台服务器的压力,不过由于数据库的特殊性——每台服务器所保存的数据都需要一致,所以数据同步就成了数据库集群中最核心的问题。如果多台服务器都可以写数据那么数据同步将变得非常复杂,所以一般情况下是将写操作交给专门的一台服务器处理,这台专门负责写的服务器叫做主服务器。当主服务器写入(增删改)数据后从底层同步到别的服务器(从服务器),读数据的时候到从服务器读取,从服务器可以有多台,这样就可以实现读写分离,并且将读请求分配到多个服务器处理。主服务器向从服务器同步数据时,如果从服务器数量多,那么可以让主服务器先向其中一部分从服务器同步数据,第一部分从服务器接收到数据后再向另外一部分同步,这时的结构如图1-5所示。

图1-5 数据库读写分离结构图

简单的数据同步方式可以采用数据库的热备份功能,不过读取到的数据可能会存在一定的滞后性,高级的方式需要使用专门的软硬件配合。另外既然是集群就涉及负载均衡问题,负载均衡和读写分离的操作一般采用专门程序处理,而且对应用系统来说是透明的。

1.4.6 分布式数据库

分布式数据库是将不同的表存放到不同的数据库中然后再放到不同的服务器。这样在处理请求时,如果需要调用多个表,则可以让多台服务器同时处理,从而提高处理速度。

数据库集群(读写分离)的作用是将多个请求分配到不同的服务器处理,从而减轻单台服务器的压力,而分布式数据库是解决单个请求本身就非常复杂的问题,它可以将单个请求分配到多个服务器处理,使用分布式后的每个节点还可以同时使用读写分离,从而组成多个节点群,结构图如图1-6所示。

图1-6 分布式数据库架构图

实际使用中分布式数据库有很多复杂的问题需要解决,如事务处理、多表查询等。分布式的另外一种使用的思路是将不同业务的数据表保存到不同的节点,让不同的业务调用不同的数据库,这种用法其实是和集群一样起分流的作用,不过这种情况就不需要同步数据了。使用后面这种思路时架构还是和上面图中的一样,所以技术和架构只是一个工具,真正重要的是思路,也就是工具的使用方法。

1.4.7 NoSQL和Hadoop

NoSQL是近年来发展非常迅速的一项技术,它的核心就是非结构化。我们一般使用的数据库(SQL数据库)都是需要先将表的结构定义出来,一个表有几个字段,每个字段各是什么类型,然后才能往里面按照相应的类型保存数据,而且按照数据库范式的规定,一个字段只能保存单一的信息,不可以包括多层内容,这就对使用的灵活性带来了很大的制约,NoSQL就是突破了这些条条框框,可以非常灵活地进行操作,另外因为NoSQL通过多个块存储数据的特点,其操作大数据的速度也非常快,这些特性正是现在的互联网程序最需要的,所以NoSQL发展得非常快。现在NoSQL主要使用在互联网的程序中,在企业业务系统中使用的还不多,而且现在NoSQL还不是很成熟,但由于灵活和高效的特性,NoSQL发展的前景是非常好的。

Hadoop是专门针对大数据处理的一套框架,随着近年来大数据的流行Hadoop也水涨船高,出世不久就红得发紫。Hadoop对数据的存储和处理都提供了相应的解决方案,底层数据的存储思路类似于1.4.6节介绍的分布式加集群的方案,不过Hadoop是将同一个表中的数据分成多块保存到多个节点(分布式),而且每一块数据都有多个节点保存(集群),这里集群除了可以并行处理相同的数据,还可以保证数据的稳定性,在其中一个节点出现问题后数据不会丢失。这里的每个节点都不包含一个完整的表的数据,但是一个节点可以保存多个表的数据,结构图如图1-7所示。

图1-7 Hadoop数据存储结构图

Hadoop对数据的处理是先对每一块的数据找到相应的节点并进行处理,然后再对每一个处理的结果进行处理,最后生成最终的结果。比如,要查找符合条件的记录,Hadoop的处理方式是先找到每一块中符合条件的记录,然后再将所有获取到的结果合并到一起,这样就可以将同一个查询分到多个服务器处理,处理的速度也就快了,这一点传统的数据库是做不到的。