Original: Taste of Little Sister (wechat official ID: XjjDog), welcome to share, please reserve the source. Any reprint that does not retain this statement is plagiarism.
Students who have been immersed in the high concurrency of the Internet for years will have some common rules when writing code: it is better to split requests into 10 one-second ones than to make a request that takes five seconds. It is better to split objects into 1000 10KB objects than to generate one 1MB object.
Why is that? It’s the fear of bigness.
Welcome to star: github.com/xjjdog/bcma… . It includes ToB complex business, Internet high concurrency business, cache applications; DDD, microservices guidance. Model driven, data driven. Understand the evolution of large-scale services, coding skills, Learning Linux, performance tuning. Docker/ K8S power, monitoring, log collection, middleware learning. Front-end technology, back-end practices, etc. Main technology: SpringBoot+JPA+Mybatis-plus+Antd+Vue3.
A “large object” is a generalized concept that may reside in a JVM, be traveling over a network, or exist in a database.
Why do large objects affect our application performance? There are three reasons.
- Large objects take up a lot of resources, and the garbage collector spends some of its energy collecting them.
- Large objects are exchanged between different devices, consuming network traffic and expensive I/O.
- Parsing and processing of large objects is time consuming, and the object responsibility is not focused, incurs additional performance overhead.
Next, XJjDog takes a step-by-step look at some strategies to shrink objects and focus operations from the structural latitude and time dimensions of the data.
1. The substring method of String
As we all know, strings are immutable in Java, and if you change the contents of a String, it will generate a new String.
If we want to use part of the string, we can use the substring method.
As shown, when we need a substring. Substring generates a new string constructed from the arrays.copyofrange function of the constructor.
This function is fine after JDK7, but there is a risk of memory leaks in JDK6. We can take a look at this example to see the problems that can arise with large object reuse.
This is a screenshot I took from the JDK official. As you can see, when it creates the substring, instead of just copying the desired object, it references the entire value. If the original string is too large, the memory will not be freed even if it is no longer used.
For example, an article may be several megabytes in size, and we only need the summary information in it, and do not need to maintain the entire large object.
String content = dao.getArticle(id);
String summary=content.substring(0.100);
articles.put(id,summary);
Copy the code
The lesson for us is this. If you create a large object and generate some additional information based on that object. Remember to remove references to the large object.
2. Expand the collection of large objects
Object expansion is a common phenomenon in Java. StringBuilder, StringBuffer, HashMap, ArrayList, etc. In a nutshell, Java collections, including lists, sets, queues, maps, etc., have uncontrollable data. When the capacity is insufficient, capacity expansion is performed.
Let’s take a look at the StringBuilder expansion code.
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);
}
Copy the code
When the capacity is insufficient, the memory is doubled and the source data is copied using arrays.copyof.
Here is the code for expanding HashMap, doubling its size. Its expansion action is much more complex, in addition to the impact of load factors, it also needs to rehash the original data. Because you can’t use the native arrays.copy method, speed is slow.
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);
}
Copy the code
The List code can be viewed by yourself, which is also obstructive. The expansion policy is 1.5 times the original length.
Because collections are used so often in your code, if you know what the upper limit of data items should be, you might want to set a reasonable initialization size. For example, a HashMap requires 1024 elements and needs to be expanded seven times, affecting application performance.
Note, however, that for collections with load factors such as HashMap (0.75), the initial size = number of required/load factor +1. If you don’t quite understand the underlying structure, you might as well leave it as default.
3. Keep the object granularity appropriate
I once encountered a business system with a very high amount of concurrency, which needed to frequently use the basic data of users. Because the user’s basic information is stored in another service, every time the user’s basic information is used, there needs to be a network interaction. What is more unacceptable is that even if only the user’s gender attribute is required, all user information needs to be queried and pulled once.
In order to speed up the data query speed, the data was initially cached and put into Redis. There was a big improvement in query performance, but you still had to query a lot of redundant data at a time.
The original Redis key was designed like this.
type: string
key: user_${userid}
value: json
Copy the code
There are two problems with this design: (1) to query the value of one of the fields, you need to query all the JSON data and parse by yourself. (2) Updating the value of a certain field requires updating the entire JSON string, which costs a lot.
With this large granularity of JSON information, you can optimize it in a fragmented manner, so that each update and query has a focused target.
Next, the data in Redis is designed as follows, using hash structure instead of JSON structure:
type: hash
key: user_${userid}
value: {sex:f, id:1223, age:23}
Copy the code
In this way, we can use hget command or HMget command to obtain the desired data and speed up the information flow.
4. Bitmaps make objects smaller
Can it be further optimized? For example, gender data of users is frequently used in our system to issue gifts, recommend friends of the opposite sex, and periodically cycle users to do some cleaning actions. Or, store some user status information, such as whether online, whether to check in, whether to send information recently, statistics active users and so on.
Operations on the yes and no values can be compressed using the Bitmap structure.
As the code shows, it can hold 32 Boolean values by judging each bit in int!
int a= 0b0001_0001_1111_1101_1001_0001_1111_1101;
Copy the code
A Bitmap is a data structure that uses bits to record data that is either 0 or 1. The related structure class in Java is java.util.bitset. The BitSet base is implemented using the long array, so its minimum size is 64.
A billion Boolean values, only 128MB of memory. The following is a 256MB gender logic that can cover 1 billion ids.
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";
}
Copy the code
This data, in memory, is still too large for the heap. Fortunately, Redis also supports Bitmap structures, which we can put into Redis if memory is stressed, and the logic is similar.
Given a machine with 1GB of memory and 6 billion ints, how do you quickly determine which data is duplicated? You can think about it by analogy.
Bitmap is one of the lower-level structures, and on top of it is a structure called Bloom Filter. The Bloom filter determines that a value does not exist, or might exist.
Compared to Bitmap, it has a layer of hash algorithms. Since it is a hash algorithm, there will be conflicts, so it is possible for multiple values to fall on the same bit.
Guava has a BloomFilter class, which can easily implement related functions.
5. Cold and hot separation of data
The above optimization method is essentially a way of turning large objects into small objects, and there are many similar ideas in software design. Like a newly published article, the frequent use of abstract data, there is no need to query the entire article content; The user’s feed needs to be visible only at the speed of the information, and the complete information is stored in a slower large storage.
In addition to horizontal structural latitude, data also has a vertical temporal dimension. The most effective way to optimize the time dimension is to separate hot and cold.
Hot data is the data that is close to the user and used frequently, while cold data is the data that is accessed very infrequently and is very old. The same complex SQL statement running on tens of millions of tables is not as effective as running on millions of tables. So, while your system starts out fast, it slows down over time as the amount of data increases.
Hot and cold separation is the separation of data into two parts. As shown in the figure, a full set of data is generally kept for some time-consuming statistical operations.
The following is a brief introduction to the three schemes of hot and cold separation.
(1) Double-write data. Insert, update, delete operations to hot and cold libraries in a unified transaction. Due to the different types of hot repositories (such as MySQL) and cold repositories (such as Hbase), this transaction is most likely a distributed transaction. This is possible at the beginning of the project, but distributed transactions are largely immutable if you are adapting legacy systems. I usually just scrap it.
(2) Write MQ distribution. With MQ’s publish-subscribe capability, data operations are sent to MQ instead of being dropped into the repository. Start the consumption process separately and drop the MQ data into the cold and hot repositories separately. The logic is very clear and the structure is elegant. For systems with a clearer structure and less sequential requirements, such as orders, MQ distribution can be used. But if you have a very large number of entities in your database, there is a complexity to this approach.
(3) Using binlog synchronization For MySQL, you can use the binlog method for synchronization. Using the Canal component, the latest Binlog data can be continuously retrieved, and with MQ, data can be synchronized to other data sources.
End
We can give two more examples of large objects.
Like the database index we commonly use, it is also a kind of data reorganization, acceleration. B+ Tree can effectively reduce the number of interactions between the database and disks. It uses a data structure similar to B+ Tree to index the most commonly used data and store the data in limited storage space.
There is also serialization, commonly used in RPC. Some services are Web Services that use the SOAP protocol, which is a protocol based on XML. However, large content transmission is slow and inefficient. Most Web services today interact with JSON data, which is much more efficient than SOAP. In addition, you should have heard of Google’s Protobuf, because it is a binary protocol, and the data is compressed, performance is very superior. Protobuf compresses data to 1/10 the size of JSON and 1/20 the size of XML, but improves performance by 5-100 times. Protobuf design is worthy of reference, it through the tag | leng | the value of the three sections of the data is very compact, parsing and transmission speed are particularly fast.
For large objects, we have two methods: structure latitude optimization and time dimension optimization. From the perspective of structure latitude, by cutting objects into appropriate granularity, operations can be concentrated on small data structures and time processing costs can be reduced. By compressing, transforming, or extracting hot data, you can avoid the storage and transport costs of large objects. In terms of time latitude, common data can be stored in high-speed equipment by means of cold and hot separation, reducing the set of data processing and speeding up the processing speed.
Welcome to star: github.com/xjjdog/bcma… . It includes ToB complex business, Internet high concurrency business, cache applications; DDD, microservices guidance. Model driven, data driven. Understand the evolution of large-scale services, coding skills, Learning Linux, performance tuning. Docker/ K8S power, monitoring, log collection, middleware learning. Front-end technology, back-end practices, etc. Main technology: SpringBoot+JPA+Mybatis-plus+Antd+Vue3.
Xjjdog is a public account that doesn’t allow programmers to get sidetracked. Focus on infrastructure and Linux. Ten years architecture, ten billion daily flow, and you discuss the world of high concurrency, give you a different taste. My personal wechat xjjdog0, welcome to add friends, further communication.