这篇文章主要介绍“如何使用高并发大对象处理”,在日常操作中,相信很多人在如何使用高并发大对象处理问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何使用高并发大对象处理”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
常年浸润在互联网高并发中的同学,在写代码时会有一些约定俗成的规则:宁可将请求拆分成10个1秒的,也不去做一个耗时5秒的请求;宁可将对象拆成1000个10KB的,也尽量避免生成一个1MB的对象。
为什么?这是对于“大”的恐惧。
“大对象”,是一个泛化的概念,它可能存放在JVM中,也可能正在网络上传输,也可能存在于数据库中。
为什么大对象会影响我们的应用性能呢?有三点原因。
大对象占用的资源多,垃圾回收器要花一部分精力去对它进行回收;
大对象在不同的设备之间交换,会耗费网络流量,以及昂贵的I/O;
对大对象的解析和处理操作是耗时的,对象职责不聚焦,就会承担额外的性能开销。
接下来,xjjdog将从数据的结构纬度和时间维度,来逐步看一下一些把对象变小,把操作聚焦的策略。
1. String的substring方法
我们都知道,String在Java中是不可变的,如果你改动了其中的内容,它就会生成一个新的字符串。
如果我们想要用到字符串中的一部分数据,就可以使用substring方法。
如图所示,当我们需要一个子字符串的时候。substring生成了一个新的字符串,这个字符串通过构造函数的Arrays.copyOfRange函数进行构造。
这个函数在JDK7之后是没有问题的,但在JDK6中,却有着内存泄漏的风险。我们可以学习一下这个案例,来看一下大对象复用可能会产生的问题。
这是我从JDK官方的一张截图。可以看到,它在创建子字符串的时候,并不只拷贝所需要的对象,而是把整个value引用了起来。如果原字符串比较大,即使不再使用,内存也不会释放。
比如,一篇文章内容可能有几MB,我们仅仅需要其中的摘要信息,也不得维持着整个的大对象。
String content = dao.getArticle(id); String summary=content.substring(0,100); articles.put(id,summary);
这对我们的借鉴意义是。如果你创建了比较大的对象,并基于这个对象生成了一些其他的信息。这个时候,一定要记得去掉和这个大对象的引用关系。
2. 集合大对象扩容
对象扩容,在Java中是司空见惯的现象。比如StringBuilder、StringBuffer,HashMap,ArrayList等。概括来讲,Java的集合,包括List、Set、Queue、Map等,其中的数据都不可控。在容量不足的时候,都会有扩容操作。
我们先来看下StringBuilder的扩容代码。
void expandCapacity(int minimumCapacity) { int newCapacity = value.length * 2 + 2; if (newCapacity - minimumCapacity < 0) newCapacity = minimumCapacity; if (newCapacity < 0) { if (minimumCapacity < 0) // overflow throw new OutOfMemoryError(); newCapacity = Integer.MAX_VALUE; } value = Arrays.copyOf(value, newCapacity); }
容量不够的时候,会将内存翻倍,并使用Arrays.copyOf复制源数据。
下面是HashMap的扩容代码,扩容后大小也是翻倍。它的扩容动作就复杂的多,除了有负载因子的影响,它还需要把原来的数据重新进行散列。由于无法使用native的Arrays.copy方法,速度就会很慢。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
List的代码大家可自行查看,也是阻塞性的,扩容策略是原长度的1.5倍。
由于集合在代码中使用的频率非常高,如果你知道具体的数据项上限,那么不妨设置一个合理的初始化大小。比如,HashMap需要1024个元素,需要7次扩容,会影响应用的性能。
但是要注意,像HashMap这种有负载因子的集合(0.75),初始化大小=需要的个数/负载因子+1。如果你不是很清楚底层的结构,那就不妨保持默认。
3. 保持合适的对象粒度
曾经碰到一个并发量非常高的业务系统,需要频繁使用到用户的基本数据。由于用户的基本信息,都是存放在另外一个服务中,所以每次用到用户的基本信息,都需要有一次网络交互。更加让人无法接受的是,即使是只需要用户的性别属性,也需要把所有的用户信息查询,拉取一遍。
为了加快数据的查询速度,对数据进行了初步的缓存,放入到了redis中。查询性能有了大的改善,但每次还是要查询很多冗余数据。
原始的redis key是这样设计的。
type: string key: user_${userid} value: json
这样的设计有两个问题:(1)查询其中某个字段的值,需要把所有json数据查询出来,并自行解析。(2)更新其中某个字段的值,需要更新整个json串,代价较高。
针对这种大粒度json信息,就可以采用打散的方式进行优化,使得每次更新和查询,都有聚焦的目标。
接下来对redis中的数据进行了以下设计,采用hash结构而不是json结构:
type: hash key: user_${userid} value: {sex:f, id:1223, age:23}
这样,我们使用hget命令,或者hmget命令,就可以获取到想要的数据,加快信息流转的速度。
4. Bitmap把对象变小
还能再进一步优化么?比如,我们系统中就频繁用到了用户的性别数据,用来发放一些礼品,推荐一些异性的好友,定时循环用户做一些清理动作等。或者,存放一些用户的状态信息,比如是否在线,是否签到,最近是否发送信息等,统计一下活跃用户等。
对是、否这两个值的操作,就可以使用Bitmap这个结构进行压缩。
如代码所示,通过判断int中的每一位,它可以保存32个boolean值!
int a= 0b0001_0001_1111_1101_1001_0001_1111_1101;
Bitmap就是使用Bit进行记录的数据结构,里面存放的数据不是0就是1。Java中的相关结构类,就是java.util.BitSet。BitSet底层是使用long数组实现的,所以它的最小容量是64。
10亿的boolean值,只需要128MB的内存。下面既是一个占用了256MB的用户性别的判断逻辑,可以涵盖长度为10亿的id。
static BitSet missSet = new BitSet(010_000_000_000); static BitSet sexSet = new BitSet(010_000_000_000); String getSex(int userId) { boolean notMiss = missSet.get(userId); if (!notMiss) { //lazy fetch String lazySex = dao.getSex(userId); missSet.set(userId, true); sexSet.set(userId, "female".equals(lazySex)); } return sexSet.get(userId) ? "female" : "male"; }
这些数据,放在堆内内存中,还是过大了。幸运的是,Redis也支持Bitmap结构,如果内存有压力,我们可以把这个结构放到redis中,判断逻辑也是类似的。
这样的问题还有很多:给出一个1GB内存的机器,提供60亿int数据,如何快速判断有哪些数据是重复的?大家可以类比思考一下。
Bitmap是一个比较底层的结构,在它之上还有一个叫做布隆过滤器的结构(Bloom Filter)。布隆过滤器可以判断一个值不存在,或者可能存在。
相比较Bitmap,它多了一层hash算法。既然是hash算法,就会有冲突,所以有可能有多个值落在同一个bit上。
Guava中有一个BloomFilter的类,可以方便的实现相关功能。
5. 数据的冷热分离
上面这种优化方式,本质上也是把大对象变成小对象的方式,在软件设计中有很多类似的思路。像一篇新发布的文章,频繁用到的是摘要数据,就不需要把整个文章内容都查询出来;用户的feed信息,也只需要保证可见信息的速度,而把完整信息存放在速度较慢的大型存储里。
数据除了横向的结构纬度,还有一个纵向的时间维度。对时间维度的优化,最有效的方式就是冷热分离。
所谓热数据,就是靠近用户的,被频繁使用的数据,而冷数据是那些访问频率非常低,年代非常久远的数据。同一句复杂的SQL,运行在几千万的数据表上,和运行在几百万的数据表上,前者的效果肯定是很差的。所以,虽然你的系统刚开始上线时速度很快,但随着时间的推移,数据量的增加,就会渐渐变得很慢。
冷热分离是把数据分成两份。如图,一般都会保持一份全量数据,用来做一些耗时的统计操作。
下面简单介绍一下冷热分离的三种方案。
(1)数据双写。把对冷热库的插入、更新、删除操作,全部放在一个统一的事务里面。由于热库(比如MySQL)和冷库(比如Hbase)的类型不同,这个事务大概率会是分布式事务。在项目初期,这种方式是可行的,但如果是改造一些遗留系统,分布式事务基本上是改不动的。我通常会把这种方案直接废弃掉。
(2)写入MQ分发。通过MQ的发布订阅功能,在进行数据操作的时候,先不落库,而是发送到MQ中。单独启动消费进程,将MQ中的数据分别落到冷库、热库中。使用这种方式改造的业务,逻辑非常清晰,结构也比较优雅。像订单这种结构比较清晰、对顺序性要求较低的系统,就可以采用MQ分发的方式。但如果你的数据库实体量非常的大,用这种方式就要考虑程序的复杂性了。
(3)使用binlog同步 针对于MySQL,就可以采用Binlog的方式进行同步。使用Canal组件,可持续获取最新的Binlog数据,结合MQ,可以将数据同步到其他的数据源中。
End
关于大对象,我们可以再举两个例子。
像我们常用的数据库索引,也是一种对数据的重新组织、加速。B+ tree可以有效的减少数据库与磁盘交互的次数,它通过类似B+ tree的数据结构,将最常用的数据进行索引,存储在有限的存储空间中。
还有在RPC中常用的序列化。有的服务是采用的SOAP协议的WebService,它是基于XML的一种协议,内容大传输慢,效率低下。现在的Web服务中,大多数是使用json数据进行交互的,json的效率相比SOAP就更高一些。另外,大家应该都听过google的protobuf,由于它是二进制协议,而且对数据进行了压缩,性能是非常优越的。protobuf对数据压缩后,大小只有json的1/10,xml的1/20,但是性能却提高了5-100倍。protobuf的设计是值得借鉴的,它通过tag|leng|value三段对数据进行了非常紧凑的处理,解析和传输速度都特别快。
针对于大对象,我们有结构纬度的优化和时间维度的优化两种方法。从结构纬度来说,通过把对象切分成合适的粒度,可以把操作集中在小数据结构上,减少时间处理成本;通过把对象进行压缩、转换,或者提取热点数据,就可以避免大对象的存储和传输成本。从时间纬度来说,就可以通过冷热分离的手段,将常用的数据存放在高速设备中,减少数据处理的集合,加快处理速度。
到此,关于“如何使用高并发大对象处理”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注天达云网站,小编会继续努力为大家带来更多实用的文章!