Caching series of articles:

Cache actual combat (1) : 20 figure | 6 thousand words | cache actual combat (1)

Cache combat (2) : Redis distributed lock | from bronze to diamond five evolution scheme

Cache combat (3) : the king of distributed lock scheme – Redisson

preface

First,, I tried to make a GIF today. It took me an hour to make just one GIF, and it was ugly.The main contents of this paper are as follows:

The previous article covered performance tuning methods such as indexing tables, static and static separation, and reducing unnecessary log printing. But there is a very powerful way of optimization is not mentioned, that is to add cache, such as query small program advertising space configuration, because few people will go to change frequently, the advertising space configuration into the cache is more suitable. Let’s add caching to the open source Spring Cloud project PassJava to improve performance.

I uploaded the back end, front end, small program to the same warehouse, you can access through Github or code cloud. The address is as follows:

Making: github.com/Jackson0714…

Yards cloud: gitee.com/jayh2018/Pa…

Accompanying tutorial: www.passjava.cn

Before going into action, let’s take a look at the principles and problems of using caching.

A, caching,

1.1 Why Cache

20 years ago, the common system is stand-alone, such as ERP system, the performance requirements are not high, the use of caching is not common, but now, has stepped into the Internet era, high concurrency, high availability, high performance is always mentioned, and caching in this “three high” made great contributions.

We speed up access by putting some of the data in the cache, and then the database takes care of the storage.

So what data fits in the cache?

  • Immediacy. For example, query the latest logistics status information.

  • Data consistency requirements are not high. For example, store information, after modification, has been changed in the database, 5 minutes later in the cache is the latest, but does not affect the use of the function.

  • The number of visits is large and the update frequency is not high. For example, the advertising information on the front page, the number of visits, but it doesn’t change very often.

When we want to query data, the process of using caching is as follows:

1.2 Local Cache

The easiest way to use a cache is to use a local cache.

For example, now there is a demand, the front-end applet needs to query the topic type, and the topic type is placed on the front page of the applet. The page view is very high, but it is not often changing data, so you can put the topic type data in the cache.

The simplest way to use caching is to use local caching, which is to cache data in memory, using data structures such as HashMaps and arrays.

1.2.1 Do not use caching

Let’s take a look at the case without caching: the front-end request first passes through the gateway, then requests to the title micro service, then queries the database and returns the query results.

Let’s take a look at the core code.

A custom Rest API is used to query the list of question types, and the data is returned directly to the front end after being queried from the database.

@RequestMapping("/list")
public R list(a){
    // Query data from the database
    typeEntityList = ITypeService.list(); 
    return R.ok().put("typeEntityList", typeEntityList);
}
Copy the code

1.2.2 Using caching

Let’s take a look at the situation of using cache: the front end first passes through the gateway, and then goes to the topic micro service, first determines whether there is data in the cache, if not, then queries the database and updates the cache, and finally returns the query results.

Let’s create a HashMap to cache the list of topic types:

private Map<String, Object> cache = new HashMap<>();
Copy the code

Get the list of cached types first

List<TypeEntity> typeEntityListCache = (List<TypeEntity>) cache.get("typeEntityList");
Copy the code

If not in the cache, get it from the database first. Of course, this data will not be available the first time you query the cache.

// If there is no data in cache
if (typeEntityListCache == null) {
  System.out.println("The cache is empty");
  // Query data from the database
  List<TypeEntity> typeEntityList = ITypeService.list();
  // Put the data into the cache
  typeEntityListCache = typeEntityList;
  cache.put("typeEntityList", typeEntityList);
}
return R.ok().put("typeEntityList", typeEntityListCache);
Copy the code

Let’s use the Postman tool to look at the query result:

Request URL:https://github.com/Jackson0714/PassJava-PlatformCopy the code

A list of the subject types is returned, with 14 entries in total.

In the future, the data is already in the cache, so it will not query the data from the database.

What are the advantages of local caching from the example above?

  • Reduces database interactions and reduces disk I/O performance issues.
  • Avoid database deadlocks.
  • Speed up accordingly.

Of course, there are some issues with local caching:

  • Occupies local memory resources.
  • After the server is restarted, the cache is lost.
  • There may be inconsistencies between database data and cache data.
  • Data cached by multiple microservices on the same machine is inconsistent.

  • Data cached in a cluster environment is inconsistent.

Based on the problem of local cache, we introduce distributed cache Redis to solve it.

Second, cache Redis

2.1 Docker install Redis

First, you need to install Redis. I installed Redis through Docker. In addition, I have installed the Docker version of Redis on Ubuntu and Mac M1, you can refer to these two articles to install.

Ubuntu Install Redis on Docker

M1 Running Docker

2.2 Introducing Redis components

I used the passjava-question microservice, so I imported the Redis component in the configuration file pom.xml under the passjava-question module.

The file path is /passjava-question/pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Copy the code

2.3 test Redis

We can write a test method to test whether the imported Redis can store data, and whether the stored data can be detected.

We’re using the StringRedisTemplate library to manipulate Redis, so we can automatically load a StringRedisTemplate.

@Autowired
StringRedisTemplate stringRedisTemplate;
Copy the code

Then within the test method, test the storage method: ops.set(), and the query method: ops.get().

@Test
public void TestStringRedisTemplate(a) {
    // Initialize the Redis component
    ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
    // Store data
    ops.set("The wu is empty"."Wukong Chat structure _" + UUID.randomUUID().toString());
    // Query data
    String wukong = ops.get("The wu is empty");
    System.out.println(wukong);
}
Copy the code

The first argument to the set method is key, as in the example “Goku”.

The argument to the get method is also key.

Redis key = “wukong” cache value printed:

In addition, you can view it through the client tool, as shown below:

Redis Desktop Manager For Windows

http://www.pc6.com/softview/SoftView_450180.html
Copy the code

2.4 Use Redis to transform business logic

It is not difficult to replace hashmap with redis. Another thing to note:

The data queried from the database must be serialized into JSON strings before being stored in Redis. When querying data from Redis, the JSON string must also be deserialized into object instances.

public List<TypeEntity> getTypeEntityList(a) {
  // 1. Initialize redis components
  ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
  // 2. Query data from cache
  String typeEntityListCache = ops.get("typeEntityList");
  // 3. If there is no data in cache
  if (StringUtils.isEmpty(typeEntityListCache)) {
    System.out.println("The cache is empty");
    // 4. Query data from database
    List<TypeEntity> typeEntityListFromDb = this.list();
    // 5. Serialize the data queried from the database as a JSON string
    typeEntityListCache = JSON.toJSONString(typeEntityListFromDb);
    // 6. Store the serialized data in the cache
    ops.set("typeEntityList", typeEntityListCache);
    return typeEntityListFromDb;
  }
  // 7. If there is data in the cache, it is removed from the cache and deserialized as an instance object
  List<TypeEntity> typeEntityList = JSON.parseObject(typeEntityListCache, new TypeReference<List<TypeEntity>>(){});
  return typeEntityList;
}
Copy the code

The whole process is as follows:

  • 1. Initialize redis components.

  • 2. Query data from the cache.

  • 3. If no data exists in the cache, perform Steps 4, 5, and 6.

  • 4. Query data from the database

  • 5. Convert the data queried from the database to A JSON string

  • 6. Store the serialized data in the cache and return the queried data in the database.

  • 7. If there is data in the cache, it is removed from the cache and deserialized as an instance object

2.5 Testing Service Logic

Let’s use the Postman tool again:

Through multiple tests, the first request was slightly slower, and the next few were very fast. Note The performance is improved after the cache is used.

In addition, we use Redis client to see the results:

Redis key = typeEntityList, Redis Value is a JSON string containing a list of subject categories.

Cache penetration, avalanche and breakdown

There are several problems with using cache at high concurrency: cache penetration, avalanche, and breakdown.

3.1 Cache Penetration

3.1.1 Concept of Cache penetration

Cache penetration refers to a certain data does not exist, because the cache does not hit this data, the database will be queried, the database does not have this data, so the return result is NULL. If every query goes to the database, the cache is meaningless, just like penetrating the cache.

3.1.2 Risks

Using non-existent data to attack, the database pressure increases, and eventually the system crashes.

3.1.3 Solution

The result null is cached and a short expiration time is added.

3.2 Cache Avalanche

3.2.1 The concept of cache avalanche

Cache avalanche means that when we cache multiple data, we adopt the same expiration time, such as 00:00:00 expiration time. If the cache is invalid at the same time, and a large number of requests come in, the data is not cached, so they all query the database, the pressure of the database increases, and finally the avalanche will occur.

3.2.2 Risks

Try to find a time when a large number of keys will expire at the same time, and at some point a large number of attacks will be launched, increasing database pressure and eventually causing the system to crash.

3.2.3 Solution

Add a crush squeeze on the basis of the original effect time, such as 1-5 minutes random, reduce the cache expiration time repetition rate, avoid cache collective effect.

3.3 Cache Breakdown

3.3.1 Concept of cache breakdown

A key was set to expire, but just before it expired, a large number of requests came in, causing them to be queried in the database.

3.3.2 Solution

In the case of a large number of concurrent requests, only one request can obtain the lock of the query database. Other requests need to wait and release the lock after finding the lock. After other requests obtain the lock, check the cache first.

Four, lock to solve the cache breakdown

What about cache penetration, avalanche, and breakdown?

  • Empty results are cached to solve the cache penetration problem.
  • Set expiration time and add a random value for expiration offset to solve the cache avalanche problem.
  • Lock, solve the cache breakdown problem. Also note that locking has an impact on performance.

Here’s a code demonstration of how to solve the cache breakdown problem.

We need to lock with synchronized. Of course, this is the local locking approach, and we’ll talk about distributed locking in the next chapter.

public List<TypeEntity> getTypeEntityListByLock(a) {
  synchronized (this) {
    // 1. Query data from cache
    String typeEntityListCache = stringRedisTemplate.opsForValue().get("typeEntityList");
    if(! StringUtils.isEmpty(typeEntityListCache)) {// 2. If there is data in the cache, it is taken out of the cache, deserialized to the instance object, and the result is returned
      List<TypeEntity> typeEntityList = JSON.parseObject(typeEntityListCache, new TypeReference<List<TypeEntity>>(){});
      return typeEntityList;
    }
    // 3. If there is no data in the cache, query data from the database
    System.out.println("The cache is empty");
    List<TypeEntity> typeEntityListFromDb = this.list();
    // 4. Serialize the data queried from the database as a JSON string
    typeEntityListCache = JSON.toJSONString(typeEntityListFromDb);
    // 5. Store the serialized data in the cache and return the database query result
    stringRedisTemplate.opsForValue().set("typeEntityList", typeEntityListCache, 1, TimeUnit.DAYS);
    returntypeEntityListFromDb; }}Copy the code
  • 1. Query data from the cache.

  • 2. If there is data in the cache, it is removed from the cache, deserialized into an instance object, and the result is returned.

  • 3. If there is no data in the cache, query the data from the database.

  • 4. Serialize the data queried from the database as a JSON string.

  • 5. Store the serialized data in the cache and return the database query result.

5. Local lock problems

The local lock can only lock the thread of the current service. As shown in the figure below, multiple title microservices are deployed, and each microservice is locked with the local lock.

Local locks are fine in general, but can be problematic when used to lock inventory:

  • 1. The current total inventory is 100 and is cached in Redis.

  • 2. Inventory microservice A after subtracting inventory 1 with local lock, the total inventory is 99.

  • 3. Inventory microservice B after subtracting inventory 1 with local lock, the total inventory is 99.

  • 4. The inventory is still 99 after 2 deductions, so it’s 1 oversold.

So how to solve the local locking problem?

Cache Combat (Part I) : Combat distributed locking.